feat: bootstrap commercial AI drama platform
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user