import assert from "node:assert/strict"; import { rm } from "node:fs/promises"; import { resolve } from "node:path"; import { dbRun, withTransaction } from "../server/db.mjs"; const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787"; const projectRoot = resolve(import.meta.dirname, ".."); const organizationId = "org-studio-lab"; const workspaceId = "ws-local-aidrama"; const projectId = "thunder-mouth"; const northstarScope = { "x-organization-id": "org-northstar", "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" }; async function request(path, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { "content-type": "application/json", ...(options.headers || {}) } }); const payload = await response.json().catch(() => ({})); return { response, payload }; } function expectOk(result, label) { assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`); return result.payload; } async function login(email) { const result = expectOk(await request("/api/auth/login", { method: "POST", body: JSON.stringify({ email, password: "Demo@123456" }) }), `${email} login`); return { authorization: `Bearer ${result.session.token}` }; } const ownerAuth = await login("producer@local.test"); const writerAuth = await login("writer@local.test"); const northstarAuth = await login("producer2@local.test"); const headers = { ...ownerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId }; const writerHeaders = { ...writerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId }; const northstarHeaders = { ...northstarAuth, ...northstarScope }; const createdStyleKitIds = []; const createdJobIds = []; try { const initial = expectOk(await request("/api/style-kits", { headers }), "list seeded style kits"); assert.ok(initial.kits.some((kit) => kit.id === "stylekit-studio-guoman-vertical"), "seeded studio style kit must be visible"); assert.equal(initial.effective.styleKit.id, "stylekit-studio-guoman-vertical", "thunder-mouth must inherit seeded style kit binding"); assert.equal(initial.effective.binding.projectId, projectId, "effective binding must belong to current project"); assert.equal(initial.effective.styleKit.voicePolicy.forbidRandomNativeVoice, true, "fixed voice policy must forbid random native voice as final"); const writerRead = await request("/api/style-kits", { headers: writerHeaders }); assert.equal(writerRead.response.status, 200, "writer should read effective style kit for generation"); const writerCreate = await request("/api/style-kits", { method: "POST", headers: writerHeaders, body: JSON.stringify({ name: "编剧不应创建", scopeMode: "workspace" }) }); assert.equal(writerCreate.response.status, 403, "writer must not manage style kits"); const styleKitId = `stylekit-smoke-${Date.now()}`; const created = expectOk(await request("/api/style-kits", { method: "POST", headers, body: JSON.stringify({ id: styleKitId, name: "Smoke 商用风格标准", scopeMode: "workspace", status: "active", description: "用于 smoke 验收的本地商用风格标准。", visualProfile: { styleFamily: "原创国漫 2D 动画", cameraLanguage: "稳定中景,单一完整画面", continuityLocks: ["character", "costume", "location", "prop", "weather", "camera"] }, subtitlePreset: { language: "zh-CN", position: "bottom-safe-area", maxCharsPerLine: 17 }, voicePolicy: { finalVoiceMode: "fixed-reference-required", forbidRandomNativeVoice: true, fallbackMouthPlan: ["侧脸", "反应镜头"] }, deliverySpec: { aspectRatio: "9:16", resolution: "1080x1920", fps: 24, clipDurationSec: [5, 10], exportFormat: "mp4" }, promptRules: { positivePrefix: "ONE SINGLE COMPLETE 9:16 CHINESE ANIMATION FRAME, smoke standard.", negativeTerms: ["split-screen", "comic panel", "collage", "contact sheet", "storyboard", "多格", "拼图", "分屏", "watermark"] }, singleFramePolicy: { enabled: true, imageOutputCount: 1, batchSize: 1, forbiddenTerms: ["split-screen", "collage", "contact sheet", "多格"] }, namingRules: { shotPattern: "{projectId}/smoke/shot-{shotNumber}-{version}", deliveryPattern: "{projectName}-smoke-{releaseVersion}" }, qaPolicy: { gates: ["single-frame", "continuity", "voice-rights", "subtitle-asr-alignment"], minimumReviewScore: 91, requireLockedAssets: true }, metadata: { source: "smoke-style-kits" } }) }), "create style kit"); createdStyleKitIds.push(styleKitId); assert.equal(created.styleKit.id, styleKitId, "created style kit id must be stable"); assert.ok(created.styleKit.promptRules.negativeTerms.includes("contact sheet"), "server must normalize single-frame negative terms"); assert.equal(created.styleKit.singleFramePolicy.batchSize, 1, "style kit must lock batch size to 1"); const bound = expectOk(await request(`/api/projects/${encodeURIComponent(projectId)}/style-kit`, { method: "POST", headers, body: JSON.stringify({ styleKitId, enforcement: "strict", notes: "smoke binds the workspace kit to the current project" }) }), "bind style kit to project"); assert.equal(bound.effective.styleKit.id, styleKitId, "effective style kit must switch after binding"); assert.equal(bound.effective.binding.enforcement, "strict", "binding enforcement must be persisted"); const effective = expectOk(await request("/api/style-kits/effective", { headers }), "read effective style kit"); assert.equal(effective.styleKit.id, styleKitId, "effective endpoint must return project binding"); const preview = expectOk(await request("/api/jobs/preview", { method: "POST", headers, body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform" }) }), "preview generation job with style kit"); assert.equal(preview.preview.styleKit.id, styleKitId, "generation preview contract must include effective style kit"); assert.equal(preview.preview.constraints.imageOutputCount, 1, "generation contract must keep image output count at 1"); assert.equal(preview.preview.constraints.batchSize, 1, "generation contract must keep batch size at 1"); assert.ok(preview.preview.constraints.negativePromptTerms.includes("contact sheet"), "generation contract must inject single-frame negative terms"); assert.equal(preview.preview.constraints.voicePolicy.forbidRandomNativeVoice, true, "generation contract must carry final voice policy"); const blockedPrompt = await request("/api/jobs/preview", { method: "POST", headers, body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform", prompt: "请生成 split-screen 对比图" }) }); assert.equal(blockedPrompt.response.status, 422, "positive prompt with split-screen must be blocked"); assert.equal(blockedPrompt.payload.error, "single_frame_contract_violation", "single-frame violation error code must be explicit"); const crossOrgBind = await request(`/api/projects/${encodeURIComponent("northstar-pilot")}/style-kit`, { method: "POST", headers: northstarHeaders, body: JSON.stringify({ styleKitId, enforcement: "strict" }) }); assert.equal(crossOrgBind.response.status, 404, "another organization must not bind this style kit"); const createdJob = expectOk(await request("/api/jobs", { method: "POST", headers, body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform", output: `qa/style-kit/${styleKitId}/frame.json` }) }), "create generation job with style kit"); createdJobIds.push(createdJob.job.id); assert.equal(createdJob.job.request.styleKit.id, styleKitId, "created job request_json must persist style kit"); assert.equal(createdJob.job.request.constraints.styleEnforcement, "project-binding", "created job must record style enforcement source"); console.log(`style kits smoke passed: ${styleKitId}`); } finally { await request(`/api/projects/${encodeURIComponent(projectId)}/style-kit`, { method: "POST", headers, body: JSON.stringify({ styleKitId: "stylekit-studio-guoman-vertical", enforcement: "strict", notes: "《雷雨口》商用 MVP 默认继承工作区国漫竖屏生产标准。" }) }).catch(() => {}); withTransaction(() => { for (const jobId of createdJobIds) { dbRun("DELETE FROM job_dependencies WHERE job_id = ? OR depends_on_job_id = ?", [jobId, jobId]); dbRun("DELETE FROM job_attempts WHERE job_id = ?", [jobId]); dbRun("DELETE FROM usage_events WHERE metadata_json LIKE ?", [`%${jobId}%`]); dbRun("DELETE FROM audit_logs WHERE target_type = 'generation_job' AND target_id = ?", [jobId]); dbRun("DELETE FROM generation_jobs WHERE id = ?", [jobId]); } for (const styleKitId of createdStyleKitIds) { dbRun("DELETE FROM project_style_kit_bindings WHERE style_kit_id = ?", [styleKitId]); dbRun("DELETE FROM audit_logs WHERE target_type = 'style_kit' AND target_id = ?", [styleKitId]); dbRun("DELETE FROM style_kits WHERE id = ?", [styleKitId]); } }); for (const jobId of createdJobIds) { await rm(resolve(projectRoot, "storage", "jobs", jobId), { recursive: true, force: true }); } }