feat: bootstrap commercial AI drama platform

This commit is contained in:
xz
2026-08-24 10:24:34 +08:00
commit ffb27d845b
100 changed files with 35314 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import { rm } from "node:fs/promises";
import { resolve } from "node:path";
import { dbRun } 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 = `smoke-lifecycle-${Date.now()}`;
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;
}
let created = false;
let jobId = "";
try {
const login = expectOk(await request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email: "producer@local.test", password: "Demo@123456" })
}), "owner login");
const headers = {
authorization: `Bearer ${login.session.token}`,
"x-organization-id": organizationId,
"x-workspace-id": workspaceId,
"x-project-id": "thunder-mouth"
};
const writerLogin = expectOk(await request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email: "writer@local.test", password: "Demo@123456" })
}), "writer login");
const creation = expectOk(await request("/api/projects", {
method: "POST",
headers,
body: JSON.stringify({ id: projectId, name: `生命周期验收 ${projectId.slice(-6)}`, type: "AI 漫剧" })
}), "create lifecycle project");
created = true;
assert.equal(creation.project.status, "draft", "new projects must begin in draft status");
const projectHeaders = { ...headers, "x-project-id": projectId };
const writerProjectHeaders = { authorization: `Bearer ${writerLogin.session.token}`, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
const writerArchive = await request(`/api/projects/${projectId}/lifecycle`, { method: "POST", headers: writerProjectHeaders, body: JSON.stringify({ action: "archive" }) });
assert.equal(writerArchive.response.status, 403, "ordinary writers must not change project lifecycle");
assert.equal(writerArchive.payload.error, "permission_denied", "lifecycle denial must be a backend permission denial");
const paused = expectOk(await request(`/api/projects/${projectId}/lifecycle`, { method: "POST", headers: projectHeaders, body: JSON.stringify({ action: "pause" }) }), "pause project");
assert.equal(paused.project.status, "paused", "pause action must transition draft to paused");
const resumed = expectOk(await request(`/api/projects/${projectId}/lifecycle`, { method: "POST", headers: projectHeaders, body: JSON.stringify({ action: "resume" }) }), "resume project");
assert.equal(resumed.project.status, "production", "resume action must transition to production");
const project = expectOk(await request("/api/project", { headers: projectHeaders }), "read lifecycle project");
const job = await request("/api/jobs", {
method: "POST",
headers: projectHeaders,
body: JSON.stringify({ adapter: "owned-image", kind: "生命周期归档保护任务", shotId: project.project.shots[0].id, output: `qa/${projectId}/frame.json` })
});
assert.equal(job.response.status, 201, `job setup failed: ${job.payload.detail || job.payload.error || ""}`);
jobId = job.payload.job.id;
const blockedArchive = await request(`/api/projects/${projectId}/lifecycle`, { method: "POST", headers: projectHeaders, body: JSON.stringify({ action: "archive" }) });
assert.equal(blockedArchive.response.status, 409, "project archive must reject active or blocked jobs");
assert.equal(blockedArchive.payload.error, "project_has_active_jobs", "archive rejection must identify active jobs");
dbRun("UPDATE generation_jobs SET status = 'cancelled', updated_at = ? WHERE id = ?", [new Date().toISOString(), jobId]);
dbRun("UPDATE job_attempts SET status = 'cancelled', finished_at = ? WHERE job_id = ? AND status IN ('blocked', 'queued', 'running')", [new Date().toISOString(), jobId]);
const archived = expectOk(await request(`/api/projects/${projectId}/lifecycle`, { method: "POST", headers: projectHeaders, body: JSON.stringify({ action: "archive" }) }), "archive project");
assert.equal(archived.project.status, "archived", "archive action must persist archived status");
assert.ok(archived.project.archived_at, "archive action must persist archive timestamp");
const archivedRead = expectOk(await request("/api/project", { headers: projectHeaders }), "read archived project");
assert.equal(archivedRead.project.status, "archived", "archived project remains readable");
const archivedQa = await request("/api/qa", { headers: projectHeaders });
assert.equal(archivedQa.response.status, 200, "archived project QA history must remain readable");
const archivedCompliance = await request("/api/platform/compliance", { headers: projectHeaders });
assert.equal(archivedCompliance.response.status, 200, "archived project compliance evidence must remain readable");
const archivedQaRun = await request("/api/production/qa/run", { method: "POST", headers: projectHeaders, body: "{}" });
assert.equal(archivedQaRun.response.status, 409, "archived project must reject new QA writes");
const archivedJob = await request("/api/jobs", { method: "POST", headers: projectHeaders, body: JSON.stringify({ adapter: "owned-image", kind: "归档项目禁止生成", output: `qa/${projectId}/blocked.json` }) });
assert.equal(archivedJob.response.status, 409, "archived project must reject new jobs at the backend");
assert.equal(archivedJob.payload.error, "project_archived", "archived job rejection must be explicit");
const archivedAsset = await request("/api/assets", { method: "POST", headers: projectHeaders, body: JSON.stringify({ name: "归档项目禁止资产修改", kind: "reference" }) });
assert.equal(archivedAsset.response.status, 409, "archived project must reject asset mutations at the backend");
const restored = expectOk(await request(`/api/projects/${projectId}/lifecycle`, { method: "POST", headers: projectHeaders, body: JSON.stringify({ action: "restore" }) }), "restore project");
assert.equal(restored.project.status, "production", "restore must return to the pre-archive status");
assert.equal(restored.project.archived_at, null, "restore must clear archive timestamp");
console.log(`project lifecycle smoke passed: ${projectId}`);
} finally {
if (created) {
dbRun("DELETE FROM usage_events WHERE project_id = ?", [projectId]);
dbRun("DELETE FROM projects WHERE id = ?", [projectId]);
await rm(resolve(projectRoot, "exports", projectId), { recursive: true, force: true });
if (jobId) await rm(resolve(projectRoot, "storage", "jobs", jobId), { recursive: true, force: true });
}
}