feat: expand commercial ai drama platform
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user