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(); }