Files
ai-drama-platform/scripts/smoke-contracts.mjs
T
2026-09-01 14:38:42 +08:00

198 lines
10 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 runId = Date.now();
const organizationId = "org-studio-lab";
const workspaceId = "ws-local-aidrama";
const projectId = "thunder-mouth";
const northstarOrganizationId = "org-northstar";
const customerCode = `SMOKE-CUST-${runId}`;
const contractNumber = `SMOKE-CTR-${runId}`;
const expiredContractNumber = `SMOKE-EXP-${runId}`;
let customerId = "";
let contractId = "";
let expiredContractId = "";
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;
}
function expectStatus(result, status, label) {
assert.equal(result.response.status, status, `${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 futureIso(days) {
return new Date(Date.now() + days * 86400000).toISOString();
}
function pastIso(days) {
return new Date(Date.now() - days * 86400000).toISOString();
}
function contractBody(number, title, overrides = {}) {
return {
customerId,
contractNumber: number,
title,
contractType: "production_license",
status: "active",
approvalStatus: "approved",
effectiveAt: pastIso(10),
expiresAt: futureIso(365),
signedAt: pastIso(9),
currency: "CNY",
amount: 0,
licenseScope: {
channels: ["local-file", "private-delivery-portal"],
territories: ["CN"],
deliverables: ["vertical-video", "delivery-manifest"],
platforms: ["private-preview", "internal-review"],
exclusive: false,
commercialUse: true,
language: ["zh-CN"]
},
rights: {
originalStory: true,
derivativeProduction: true,
aiGeneratedAssetsAllowed: true,
voiceCloneAllowed: false,
fixedVoiceRequired: true,
singleFramePolicyRequired: true
},
deliveryTerms: { customerPortalAllowed: true, requireClearanceCertificate: true, maxAccessDays: 14 },
evidence: { contractRef: `smoke://contract/${number}`, rightsEvidenceRef: `smoke://rights/${number}`, signedCopyPath: `storage/contracts/smoke/${number}.json` },
risk: { paidCloudAllowed: false, publicModelAllowed: false, rightsReview: "passed" },
approvalNote: "smoke test contract",
...overrides
};
}
const ownerAuth = await login("producer@local.test");
const writerAuth = await login("writer@local.test");
const northstarAuth = await login("producer2@local.test");
const headers = { ...ownerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
const writerHeaders = { ...writerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
const northstarHeaders = { ...northstarAuth, "x-organization-id": northstarOrganizationId, "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" };
try {
const seeded = expectOk(await request(`/api/organizations/${organizationId}/contracts`, { headers }), "owner lists seeded contracts");
assert.ok(seeded.customers.some((customer) => customer.id === "cust-xinghe-release"), "seeded customer must be visible");
assert.ok(seeded.contracts.some((contract) => contract.id === "contract-xh-rain-night-2026"), "seeded contract must be visible");
assert.ok(seeded.projects.some((project) => project.id === projectId), "current project must be selectable for binding");
const seededClearance = expectOk(await request(`/api/organizations/${organizationId}/contracts/clearance?projectId=${encodeURIComponent(projectId)}&channelKind=private-delivery-portal`, { headers }), "seeded contract clearance");
assert.equal(seededClearance.clearance.status, "pass", "seeded thunder-mouth contract clearance must pass");
assert.ok(seededClearance.clearance.bindings.some((binding) => binding.contract.id === "contract-xh-rain-night-2026"), "clearance must include seeded contract binding");
expectStatus(await request(`/api/organizations/${organizationId}/contracts`, { headers: writerHeaders }), 403, "writer cannot list contracts");
expectStatus(await request(`/api/organizations/${organizationId}/contracts/contract-ns-pilot-2026/bind-project`, {
method: "POST",
headers,
body: JSON.stringify({ projectId, usageMode: "supplemental-rights" })
}), 404, "studio owner cannot bind northstar contract");
const northstar = expectOk(await request(`/api/organizations/${northstarOrganizationId}/contracts`, { headers: northstarHeaders }), "northstar lists own contracts");
assert.ok(northstar.contracts.some((contract) => contract.id === "contract-ns-pilot-2026"), "northstar seeded contract must be visible inside its organization");
assert.ok(!northstar.contracts.some((contract) => contract.id === "contract-xh-rain-night-2026"), "northstar must not see studio contracts");
const customer = expectOk(await request(`/api/organizations/${organizationId}/customers`, {
method: "POST",
headers,
body: JSON.stringify({
name: `烟测发行客户 ${runId}`,
legalName: `烟测发行客户 ${runId} 有限公司`,
code: customerCode,
customerType: "distributor",
status: "active",
industry: "AI 短剧发行",
region: "CN",
billingEmail: "smoke@example.local",
tags: ["smoke", "contracts"],
notes: "合同工作台 smoke 自动创建"
})
}), "create customer");
customerId = customer.customer.id;
assert.equal(customer.customer.code, customerCode, "customer code must be persisted");
const contact = expectOk(await request(`/api/organizations/${organizationId}/customers/${encodeURIComponent(customerId)}/contacts`, {
method: "POST",
headers,
body: JSON.stringify({ name: "烟测制片窗口", role: "交付验收", email: "acceptance@example.local", isPrimary: true })
}), "create customer contact");
assert.equal(contact.customer.contacts[0].isPrimary, true, "primary contact must be persisted");
const contract = expectOk(await request(`/api/organizations/${organizationId}/contracts`, {
method: "POST",
headers,
body: JSON.stringify(contractBody(contractNumber, `烟测商用授权 ${runId}`))
}), "create active approved contract");
contractId = contract.contract.id;
assert.equal(contract.contract.contractNumber, contractNumber, "contract number must be persisted");
const bound = expectOk(await request(`/api/organizations/${organizationId}/contracts/${encodeURIComponent(contractId)}/bind-project`, {
method: "POST",
headers,
body: JSON.stringify({ projectId, usageMode: "supplemental-rights", status: "active", notes: "smoke supplemental rights" })
}), "bind active contract to project");
assert.equal(bound.binding.projectId, projectId, "binding project must be persisted");
assert.equal(bound.clearance.status, "pass", "valid supplemental contract must not block clearance");
const expired = expectOk(await request(`/api/organizations/${organizationId}/contracts`, {
method: "POST",
headers,
body: JSON.stringify(contractBody(expiredContractNumber, `烟测过期授权 ${runId}`, { expiresAt: pastIso(1) }))
}), "create expired contract");
expiredContractId = expired.contract.id;
expectOk(await request(`/api/organizations/${organizationId}/contracts/${encodeURIComponent(expiredContractId)}/bind-project`, {
method: "POST",
headers,
body: JSON.stringify({ projectId, usageMode: "supplemental-rights", status: "active", notes: "smoke expired rights" })
}), "bind expired contract");
const blocked = expectOk(await request(`/api/organizations/${organizationId}/contracts/clearance?projectId=${encodeURIComponent(projectId)}&channelKind=private-delivery-portal`, { headers }), "expired contract blocks clearance");
assert.equal(blocked.clearance.status, "blocked", "expired active binding must block project contract clearance");
assert.ok(blocked.clearance.blockers.some((item) => item.code === "contract_expired"), "clearance blockers must include contract_expired");
const events = dbAll("SELECT event_type FROM contract_license_events WHERE organization_id = ? AND (contract_id = ? OR contract_id = ?)", [organizationId, contractId, expiredContractId]);
assert.ok(events.some((event) => event.event_type === "contract.created"), "contract creation must write license event");
assert.ok(events.some((event) => event.event_type === "contract.project_binding.created"), "project binding must write license event");
console.log(`contracts smoke passed: ${contractId} / ${expiredContractId}`);
} finally {
withTransaction(() => {
for (const id of [contractId, expiredContractId].filter(Boolean)) {
dbRun("DELETE FROM contract_license_events WHERE contract_id = ?", [id]);
dbRun("DELETE FROM contract_project_bindings WHERE contract_id = ?", [id]);
dbRun("DELETE FROM audit_logs WHERE target_id = ? OR metadata_json LIKE ?", [id, `%${id}%`]);
dbRun("DELETE FROM production_contracts WHERE id = ?", [id]);
}
if (customerId) {
dbRun("DELETE FROM customer_contacts WHERE customer_id = ?", [customerId]);
dbRun("DELETE FROM contract_license_events WHERE customer_id = ?", [customerId]);
dbRun("DELETE FROM audit_logs WHERE target_id = ? OR metadata_json LIKE ?", [customerId, `%${customerId}%`]);
dbRun("DELETE FROM customers WHERE id = ?", [customerId]);
}
dbRun("DELETE FROM customers WHERE organization_id = ? AND code = ?", [organizationId, customerCode]);
dbRun("DELETE FROM production_contracts WHERE organization_id = ? AND contract_number IN (?, ?)", [organizationId, contractNumber, expiredContractNumber]);
});
}