Files
ai-drama-platform/scripts/smoke-knowledge-governance.mjs
T

188 lines
9.0 KiB
JavaScript

import assert from "node:assert/strict";
import { dbRun, withTransaction } from "../server/db.mjs";
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
const runId = `smoke-knowledge-governance-${Date.now()}`;
const documentId = `${runId}-doc`;
const scope = {
"x-organization-id": "org-studio-lab",
"x-workspace-id": "ws-local-aidrama",
"x-project-id": "thunder-mouth"
};
const createdScriptIds = [];
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;
}
async function login(email) {
const result = expectOk(await request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password: "Demo@123456" })
}), `${email} login`);
return { authorization: `Bearer ${result.session.token}` };
}
function cleanup() {
withTransaction(() => {
for (const scriptId of createdScriptIds) dbRun("DELETE FROM script_documents WHERE id = ?", [scriptId]);
dbRun("DELETE FROM usage_events WHERE metadata_json LIKE ?", [`%${documentId}%`]);
dbRun("DELETE FROM audit_logs WHERE target_id = ?", [documentId]);
dbRun("DELETE FROM knowledge_governance_reviews WHERE document_id = ?", [documentId]);
dbRun("DELETE FROM knowledge_document_versions WHERE document_id = ?", [documentId]);
dbRun("DELETE FROM knowledge_chunks WHERE document_id = ?", [documentId]);
dbRun("DELETE FROM knowledge_documents WHERE id = ?", [documentId]);
});
}
try {
const ownerHeaders = { ...(await login("producer@local.test")), ...scope };
const writerHeaders = { ...(await login("writer@local.test")), ...scope };
const uniqueTerm = `治理暗号${runId.slice(-6)}`;
const imported = expectOk(await request("/api/knowledge/library/import", {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({
id: documentId,
title: `治理验收素材 ${runId.slice(-6)}`,
sourceType: "novel",
scopeMode: "project",
rightsStatus: "needs-evidence",
provenance: { sourceLabel: "", rightsOwner: "", evidenceRef: "" },
tags: "smoke,版权待审",
content: [
"第一章 未清来源",
`唐夏在雨夜提到三体改编和${uniqueTerm},这段文字必须触发已知 IP 复核。`,
"",
"唐夏:如果口型同步不稳,就只拍我的侧脸。",
"陈宇:先保留蓝伞和地铁口,不要生成分屏或多格。"
].join("\n")
})
}), "import governance document");
assert.equal(imported.document.id, documentId, "导入必须返回指定文档");
assert.equal(imported.document.rightsStatus, "needs-evidence", "缺省版权状态必须保留");
assert.equal(imported.document.riskStatus, "review", "缺证据和已知 IP 应进入复核状态");
assert.ok(imported.document.risk.issues.some((issue) => issue.code === "known_ip_reference"), "必须检测已知 IP 风险");
assert.equal(imported.document.currentVersionNumber, 1, "导入必须创建 v1");
const deniedApprove = await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/review`, {
method: "POST",
headers: writerHeaders,
body: JSON.stringify({ decision: "approved", rightsStatus: "approved", evidenceRef: "contract://writer-denied" })
});
assert.equal(deniedApprove.response.status, 403, "普通编剧不能批准商用版权");
const rejected = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/review`, {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({ decision: "rejected", rightsStatus: "rejected", notes: "未提供来源证明" })
}), "reject document");
assert.equal(rejected.document.rightsStatus, "rejected", "驳回后版权状态必须为 rejected");
const blockedMaterialize = await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/materialize`, {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({ title: "被驳回素材不应入剧本" })
});
assert.equal(blockedMaterialize.response.status, 409, "被驳回素材不能进入剧本工厂");
const updated = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}`, {
method: "PATCH",
headers: ownerHeaders,
body: JSON.stringify({
title: `原创治理验收素材 ${runId.slice(-6)}`,
sourceType: "novel",
rightsStatus: "approved",
provenance: {
sourceLabel: "原创小说授权",
author: "本地测试作者",
rightsOwner: "本地测试组织",
evidenceRef: `contract://knowledge-governance/${runId}`,
licenseNote: "原创素材,可用于本地商业生产验收。"
},
tags: "原创,商用已批,smoke",
content: [
"第一章 清洁来源",
`唐夏把${uniqueTerm}写在蓝伞内侧,陈宇在地铁口远景镜头里看见伞柄反光。`,
"",
"唐夏:不要正脸长对白,用侧脸和反应镜头。",
"陈宇:我会把上一段真实末帧接进下一段。"
].join("\n")
})
}), "update clean document");
assert.ok(updated.document.currentVersionNumber >= 2, "内容更新必须生成新版本");
assert.equal(updated.document.rightsStatus, "approved", "更新后应保留已批准状态");
assert.equal(updated.document.riskStatus, "pass", "清洁原创素材应通过本地扫描");
const approved = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/review`, {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({
decision: "approved",
rightsStatus: "approved",
evidenceRef: `contract://knowledge-governance/${runId}`,
provenance: updated.document.provenance,
tags: "原创,商用已批,smoke",
notes: "原创素材证据完整"
})
}), "approve clean document");
assert.equal(approved.review.decision, "approved", "审批记录必须写入 approved");
assert.equal(approved.document.riskStatus, "pass", "批准后的清洁素材应保持通过");
const materialized = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/materialize`, {
method: "POST",
headers: ownerHeaders,
body: JSON.stringify({ title: `治理验收入剧本 ${runId.slice(-6)}` })
}), "materialize approved document");
createdScriptIds.push(materialized.importedScript.id);
assert.ok(materialized.importedScript.content.includes(uniqueTerm), "批准素材入剧本必须保留正文");
const searchable = expectOk(await request(`/api/knowledge/search?q=${encodeURIComponent(uniqueTerm)}&scopeMode=project`, { headers: ownerHeaders }), "search before archive");
assert.ok(searchable.results.some((item) => item.documentId === documentId), "恢复前素材必须可检索");
const archived = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/archive`, {
method: "POST",
headers: ownerHeaders,
body: "{}"
}), "archive document");
assert.equal(archived.document.status, "archived", "归档状态必须返回 archived");
const hidden = expectOk(await request(`/api/knowledge/search?q=${encodeURIComponent(uniqueTerm)}&scopeMode=project`, { headers: ownerHeaders }), "search after archive");
assert.equal(hidden.results.some((item) => item.documentId === documentId), false, "归档素材不能被检索引用");
const restored = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/restore`, {
method: "POST",
headers: ownerHeaders,
body: "{}"
}), "restore document");
assert.equal(restored.document.status, "indexed", "恢复后必须回到索引状态");
const versions = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/versions`, { headers: ownerHeaders }), "list versions");
assert.ok(versions.versions.length >= 2, "版本列表至少包含 v1 和 v2");
const v1 = versions.versions.find((version) => version.versionNumber === 1);
assert.ok(v1, "版本列表必须包含 v1");
const rolledBack = expectOk(await request(`/api/knowledge/library/${encodeURIComponent(documentId)}/versions/${encodeURIComponent(v1.id)}/restore`, {
method: "POST",
headers: ownerHeaders,
body: "{}"
}), "restore v1");
assert.ok(rolledBack.document.currentVersionNumber > updated.document.currentVersionNumber, "恢复历史版本必须生成新的当前版本");
assert.ok(rolledBack.document.content.includes("三体"), "恢复 v1 后正文应回到历史内容");
assert.equal(rolledBack.document.riskStatus, "review", "恢复含 IP 的历史版本必须重新进入复核");
console.log(`knowledge governance smoke passed: document=${documentId}, versions=${rolledBack.versions.length}`);
} finally {
cleanup();
}