128 lines
7.9 KiB
JavaScript
128 lines
7.9 KiB
JavaScript
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-isolation-${Date.now()}`;
|
|
const projectName = `隔离验收项目 ${projectId.slice(-6)}`;
|
|
const emptyOrganizationId = `smoke-empty-org-${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 emptyOrganizationCreated = false;
|
|
let isolatedAssetPath = "";
|
|
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 original = expectOk(await request("/api/project", { headers }), "read original project");
|
|
const originalShotIds = new Set((original.project.shots || []).map((shot) => shot.id));
|
|
const originalCharacterIds = new Set((original.project.characters || []).map((asset) => asset.id));
|
|
|
|
const creation = expectOk(await request("/api/projects", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ id: projectId, name: projectName, type: "AI 漫剧" })
|
|
}), "create isolated project");
|
|
assert.equal(creation.project.id, projectId, "project factory must return the created project");
|
|
created = true;
|
|
|
|
const projectHeaders = { ...headers, "x-project-id": projectId };
|
|
const scoped = expectOk(await request("/api/project", { headers: projectHeaders }), "read isolated project");
|
|
assert.equal(scoped.project.id, projectId, "project response must stay inside requested project");
|
|
assert.equal(scoped.project.series.title, projectName, "series title must come from the new project");
|
|
assert.notEqual(scoped.project.series.title, original.project.series.title, "new project must not inherit the seeded series title");
|
|
assert.equal(scoped.project.characters.length, 0, "new project must not inherit character locks");
|
|
assert.equal(scoped.project.locations.length, 0, "new project must not inherit location locks");
|
|
assert.equal(scoped.project.props.length, 0, "new project must not inherit prop locks");
|
|
assert.equal(scoped.project.productionJobs.length, 0, "new project must start with an empty job list");
|
|
assert.ok(scoped.project.episode.id, "new project must initialize a first episode");
|
|
assert.equal(scoped.project.shots.length, 1, "new project must initialize exactly one starter shot");
|
|
assert.ok(!scoped.project.shots.some((shot) => originalShotIds.has(shot.id)), "starter shot must not reuse another project's shot");
|
|
assert.ok(!scoped.project.characters.some((asset) => originalCharacterIds.has(asset.id)), "assets must not cross project boundaries");
|
|
|
|
const isolatedAsset = expectOk(await request("/api/assets/upload", {
|
|
method: "POST",
|
|
headers: projectHeaders,
|
|
body: JSON.stringify({ data: Buffer.from(`isolated-asset-${projectId}`).toString("base64"), fileName: "isolated.txt", kind: "reference", mimeType: "text/plain" })
|
|
}), "upload isolated asset");
|
|
isolatedAssetPath = isolatedAsset.asset.currentVersion.storage_path;
|
|
const isolatedAssets = expectOk(await request("/api/assets", { headers: projectHeaders }), "read isolated assets");
|
|
assert.equal(isolatedAssets.assets.length, 1, "新项目资产列表只能包含本项目刚上传的资产");
|
|
const originalAssetContent = await request(`/api/assets/${encodeURIComponent([...originalCharacterIds][0])}/content`, { headers: projectHeaders });
|
|
assert.equal(originalAssetContent.response.status, 404, "跨项目读取资产内容必须返回 404");
|
|
|
|
const graph = expectOk(await request("/api/production/graph", { headers: projectHeaders }), "read isolated production graph");
|
|
assert.equal(graph.graph.series.title, projectName, "production graph must use the new series");
|
|
assert.equal(graph.graph.shots.length, 1, "production graph must contain the new starter shot only");
|
|
assert.equal(graph.graph.characters.length, 0, "production graph must not include seeded characters");
|
|
|
|
const job = expectOk(await request("/api/jobs", {
|
|
method: "POST",
|
|
headers: projectHeaders,
|
|
body: JSON.stringify({ adapter: "owned-image", kind: "隔离验收关键帧", shotId: scoped.project.shots[0].id, output: `qa/${projectId}/frame.json` })
|
|
}), "create isolated job");
|
|
const afterJob = expectOk(await request("/api/project", { headers: projectHeaders }), "read isolated project after job");
|
|
assert.equal(afterJob.jobs.length, 1, "new project job list must contain only its own job");
|
|
assert.equal(afterJob.jobs[0].id, job.job.id, "job must be scoped to the new project");
|
|
|
|
const exports = expectOk(await request("/api/exports/write", { method: "POST", headers: projectHeaders, body: "{}" }), "write isolated exports");
|
|
assert.ok(exports.exportRoot.endsWith(`/exports/${projectId}`), "exports must use a project-specific root");
|
|
assert.ok(exports.files.every((file) => file.startsWith(exports.exportRoot)), "every export file must stay in the project root");
|
|
|
|
const originalAfter = expectOk(await request("/api/project", { headers }), "read original project after isolation test");
|
|
assert.ok(originalAfter.project.characters.length > 0, "original project's assets must remain intact");
|
|
assert.ok(originalAfter.project.productionJobs.length >= original.project.productionJobs.length, "original project jobs must remain separate");
|
|
assert.ok(originalAfter.project.shots.some((shot) => originalShotIds.has(shot.id)), "original project's shots must remain intact");
|
|
|
|
const emptyOrganization = expectOk(await request("/api/organizations", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ id: emptyOrganizationId, name: `空租户 ${emptyOrganizationId.slice(-6)}`, workspaceName: "空生产空间" })
|
|
}), "create empty organization");
|
|
emptyOrganizationCreated = true;
|
|
const emptyScope = { authorization: headers.authorization, "x-organization-id": emptyOrganization.organization.id };
|
|
const emptyContext = expectOk(await request("/api/context", { headers: emptyScope }), "read empty organization context");
|
|
assert.equal(emptyContext.context.currentProject, null, "new organization must not invent a current project");
|
|
const emptyProject = expectOk(await request("/api/project", { headers: emptyScope }), "read empty organization project shell");
|
|
assert.equal(emptyProject.project.id, "", "empty organization must return an empty project shell");
|
|
assert.equal(emptyProject.project.episode.id, "", "empty organization must not invent an episode");
|
|
assert.equal(emptyProject.project.shots.length, 0, "empty organization must not inherit a starter shot");
|
|
assert.equal(emptyProject.project.characters.length, 0, "empty organization must not inherit character locks");
|
|
|
|
console.log(`project isolation smoke passed: ${projectId}`);
|
|
} finally {
|
|
if (created) {
|
|
dbRun("DELETE FROM projects WHERE id = ?", [projectId]);
|
|
await rm(resolve(projectRoot, "exports", projectId), { recursive: true, force: true });
|
|
if (isolatedAssetPath) await rm(resolve(projectRoot, isolatedAssetPath), { force: true });
|
|
await rm(resolve(projectRoot, "storage", "assets", projectId), { recursive: true, force: true });
|
|
}
|
|
if (emptyOrganizationCreated) dbRun("DELETE FROM organizations WHERE id = ?", [emptyOrganizationId]);
|
|
}
|