Files
ai-drama-platform/scripts/smoke-asset-governance.mjs

207 lines
8.6 KiB
JavaScript

import assert from "node:assert/strict";
import { dbAll, dbRun, withTransaction } from "../server/db.mjs";
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
const scope = {
"x-organization-id": "org-studio-lab",
"x-workspace-id": "ws-local-aidrama",
"x-project-id": "thunder-mouth"
};
const createdAssetIds = [];
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 };
}
async function assertOk(path, options = {}) {
const result = await request(path, options);
assert.equal(result.response.ok, true, `${options.method || "GET"} ${path}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`);
return result.payload;
}
async function login(email) {
const result = await assertOk("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password: "Demo@123456" })
});
return { authorization: `Bearer ${result.session.token}`, ...scope };
}
function cleanup() {
withTransaction(() => {
for (const assetId of createdAssetIds) {
dbRun("DELETE FROM asset_governance_reviews WHERE asset_id = ?", [assetId]);
dbRun("DELETE FROM asset_bindings WHERE asset_id = ?", [assetId]);
dbRun("DELETE FROM asset_versions WHERE asset_id = ?", [assetId]);
dbRun("DELETE FROM assets WHERE id = ?", [assetId]);
dbRun("DELETE FROM audit_logs WHERE target_id = ? OR metadata_json LIKE ?", [assetId, `%${assetId}%`]);
}
});
}
try {
const art = await login("art@local.test");
const owner = await login("producer@local.test");
const reviewer = await login("review@local.test");
const reviewerAssets = await request("/api/assets", { headers: reviewer });
assert.equal(reviewerAssets.response.status, 403, "审片角色不能因为 voice:approve 越权读取全资产库");
const risky = await assertOk("/api/assets", {
method: "POST",
headers: art,
body: JSON.stringify({
name: `smoke-risk-${Date.now()}`,
kind: "character",
subtitle: "疑似既有 IP 风险",
detail: "外形参考蜘蛛侠,生成分屏故事板拼图",
lock: "用于测试风险阻断",
provenance: { sourceType: "third-party-reference", sourceName: "网络下载截图", sourceRef: "" },
tags: ["smoke", "风险样本"]
})
});
createdAssetIds.push(risky.asset.id);
assert.equal(risky.asset.currentVersion.risk.status, "blocked", "疑似 IP 和多画面素材必须被扫描为 blocked");
const riskyScan = await assertOk(`/api/assets/${encodeURIComponent(risky.asset.id)}/governance/scan`, {
method: "POST",
headers: art,
body: JSON.stringify({ rightsStatus: "submitted" })
});
assert.equal(riskyScan.scan.status, "blocked", "手动风险扫描必须返回阻断状态");
const riskyApproval = await request(`/api/assets/${encodeURIComponent(risky.asset.id)}/rights`, {
method: "POST",
headers: owner,
body: JSON.stringify({
rightsStatus: "approved",
provenance: { sourceType: "licensed", sourceName: "smoke-risk-license", sourceRef: "local-license/risk" },
evidence: { reference: "local-license/risk" },
licenseScope: "commercial"
})
});
assert.equal(riskyApproval.response.status, 422, "blocked 风险资产不能被批准商用");
assert.equal(riskyApproval.payload.error, "asset_governance_blocked", "风险阻断错误码必须明确");
const clean = await assertOk("/api/assets", {
method: "POST",
headers: art,
body: JSON.stringify({
name: `smoke-clean-prop-${Date.now()}`,
kind: "prop",
subtitle: "原创道具",
detail: "原创蓝色防水背包,单一完整画面引用",
lock: "斜挎在角色左肩,不遮挡脸",
provenance: { sourceType: "original", sourceName: "smoke 原创道具设定", sourceRef: "local-original/prop" },
tags: ["smoke", "原创", "道具锁"]
})
});
createdAssetIds.push(clean.asset.id);
assert.notEqual(clean.asset.currentVersion.risk.status, "blocked", "原创资产初始扫描不应阻塞");
const artSubmit = await assertOk(`/api/assets/${encodeURIComponent(clean.asset.id)}/rights`, {
method: "POST",
headers: art,
body: JSON.stringify({
rightsStatus: "submitted",
provenance: { sourceType: "original", sourceName: "smoke 原创道具设定", sourceRef: "local-original/prop" },
evidence: { reference: "local-evidence/prop-original", notes: "smoke 提交证据" },
licenseScope: "project"
})
});
assert.equal(artSubmit.asset.currentVersion.rights_status, "submitted", "资产编辑者必须能提交证据");
const artApprove = await request(`/api/assets/${encodeURIComponent(clean.asset.id)}/rights`, {
method: "POST",
headers: art,
body: JSON.stringify({
rightsStatus: "approved",
provenance: { sourceType: "original", sourceName: "smoke 原创道具设定", sourceRef: "local-original/prop" },
evidence: { reference: "local-evidence/prop-original" },
licenseScope: "commercial"
})
});
assert.equal(artApprove.response.status, 403, "资产编辑者不能做最终授权批准");
const noEvidence = await assertOk("/api/assets", {
method: "POST",
headers: art,
body: JSON.stringify({
name: `smoke-no-evidence-${Date.now()}`,
kind: "reference",
subtitle: "缺证据审批测试",
detail: "原创单画面参考",
lock: "仅用于 smoke",
provenance: { sourceType: "original", sourceName: "smoke 无证据资产", sourceRef: "local-original/no-evidence" },
tags: ["smoke"]
})
});
createdAssetIds.push(noEvidence.asset.id);
const missingEvidence = await request(`/api/assets/${encodeURIComponent(noEvidence.asset.id)}/rights`, {
method: "POST",
headers: owner,
body: JSON.stringify({
rightsStatus: "approved",
provenance: { sourceType: "original", sourceName: "smoke 无证据资产", sourceRef: "local-original/no-evidence" },
evidence: {},
licenseScope: "commercial"
})
});
assert.equal(missingEvidence.response.status, 400, "缺少授权证据不能批准");
assert.equal(missingEvidence.payload.error, "rights_evidence_required", "缺证据错误码必须明确");
const approved = await assertOk(`/api/assets/${encodeURIComponent(clean.asset.id)}/rights`, {
method: "POST",
headers: owner,
body: JSON.stringify({
rightsStatus: "approved",
provenance: { sourceType: "original", sourceName: "smoke 原创道具设定", sourceRef: "local-original/prop" },
evidence: { reference: "local-evidence/prop-original", notes: "smoke 批准商用" },
licenseScope: "commercial"
})
});
assert.equal(approved.asset.currentVersion.rights_status, "approved", "合规/管理员必须能批准干净资产");
assert.equal(approved.asset.currentVersion.licenseScope, "commercial", "授权范围必须持久化");
assert.ok(approved.asset.governanceReviews.length >= 2, "提交和批准都必须写入审核记录");
const gateAsset = await assertOk("/api/assets", {
method: "POST",
headers: art,
body: JSON.stringify({
name: `smoke-unapproved-prop-${Date.now()}`,
kind: "prop",
subtitle: "未授权生成前拦截",
detail: "原创临时道具",
lock: "用于测试生成门禁",
provenance: { sourceType: "original", sourceName: "smoke 临时道具", sourceRef: "local-original/unapproved" },
tags: ["smoke", "未授权"]
})
});
createdAssetIds.push(gateAsset.asset.id);
await assertOk(`/api/assets/${encodeURIComponent(gateAsset.asset.id)}/bindings`, {
method: "POST",
headers: art,
body: JSON.stringify({ shotId: "shot-01", usageRole: "prop" })
});
const visualJob = await request("/api/jobs", {
method: "POST",
headers: art,
body: JSON.stringify({ adapter: "owned-image", kind: "单画面关键帧", shotId: "shot-01", output: `storage/jobs/smoke-asset-governance-${Date.now()}/output` })
});
assert.equal(visualJob.response.status, 422, "绑定未授权道具后,图片生成必须被资产治理门禁拦截");
assert.equal(visualJob.payload.error, "asset_governance_gate_failed", "生成门禁错误码必须明确");
const residue = dbAll(`SELECT id FROM assets WHERE id IN (${createdAssetIds.map(() => "?").join(",")})`, createdAssetIds);
assert.equal(residue.length, createdAssetIds.length, "清理前测试资产应仍在库中,确保测试过程真实写入");
console.log("asset governance smoke passed: RBAC, scan, approval ledger, generation gate");
} finally {
cleanup();
}