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 }); } }