feat: add organization policy center
This commit is contained in:
@@ -9,6 +9,7 @@ const scripts = [
|
||||
"smoke:identity",
|
||||
"smoke:system-users",
|
||||
"smoke:commercial-governance",
|
||||
"smoke:organization-policies",
|
||||
"smoke:invitations",
|
||||
"smoke:commercial-ops",
|
||||
"smoke:commercial-approvals",
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { dbRun, withTransaction } from "../server/db.mjs";
|
||||
|
||||
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
|
||||
const projectRoot = resolve(import.meta.dirname, "..");
|
||||
const organizationId = "org-studio-lab";
|
||||
const workspaceId = "ws-local-aidrama";
|
||||
const projectId = "thunder-mouth";
|
||||
const createdJobIds = [];
|
||||
const evaluationSubjectIds = [];
|
||||
|
||||
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}` };
|
||||
}
|
||||
|
||||
const ownerAuth = await login("producer@local.test");
|
||||
const writerAuth = await login("writer@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 };
|
||||
|
||||
let originalRetentionPolicy = null;
|
||||
|
||||
try {
|
||||
const initial = expectOk(await request(`/api/organizations/${organizationId}/policies`, { headers }), "list organization policies");
|
||||
assert.ok(initial.policies.length >= 8, "组织策略必须包含默认商业硬门");
|
||||
assert.equal(initial.effective.generation.imageOutputCount, 1, "组织策略必须锁定单张图片输出");
|
||||
assert.equal(initial.effective.generation.batchSize, 1, "组织策略必须锁定 batchSize=1");
|
||||
assert.equal(initial.effective.voice.forbidRandomNativeVoice, true, "组织策略必须禁止随机原生声线作为最终方案");
|
||||
assert.equal(initial.effective.model.externalApprovalRequired, true, "组织策略必须要求外部连接器审批");
|
||||
|
||||
const writerRead = await request(`/api/organizations/${organizationId}/policies`, { headers: writerHeaders });
|
||||
assert.equal(writerRead.response.status, 200, "编剧可以读取组织策略用于生产自检");
|
||||
|
||||
const writerPatch = await request(`/api/organizations/${organizationId}/policies/retention.production_evidence`, {
|
||||
method: "PATCH",
|
||||
headers: writerHeaders,
|
||||
body: JSON.stringify({ value: { temporaryMediaDays: 90 } })
|
||||
});
|
||||
assert.equal(writerPatch.response.status, 403, "编剧不能修改组织策略");
|
||||
|
||||
const disableHardGate = await request(`/api/organizations/${organizationId}/policies/generation.single_frame_contract`, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify({ status: "disabled", enforcement: "off" })
|
||||
});
|
||||
assert.equal(disableHardGate.response.status, 409, "一图一完整单画面硬门不能被关闭");
|
||||
assert.equal(disableHardGate.payload.error, "policy_hard_gate_locked", "硬门降级错误码必须稳定");
|
||||
|
||||
originalRetentionPolicy = initial.policies.find((policy) => policy.policyKey === "retention.production_evidence");
|
||||
const patched = expectOk(await request(`/api/organizations/${organizationId}/policies/retention.production_evidence`, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...originalRetentionPolicy,
|
||||
value: { ...originalRetentionPolicy.value, temporaryMediaDays: 45 }
|
||||
})
|
||||
}), "update editable retention policy");
|
||||
assert.equal(patched.policies.find((policy) => policy.policyKey === "retention.production_evidence").value.temporaryMediaDays, 45, "可编辑策略必须落库");
|
||||
|
||||
const passSubjectId = `policy-pass-${Date.now()}`;
|
||||
evaluationSubjectIds.push(passSubjectId);
|
||||
const passEvaluation = expectOk(await request(`/api/organizations/${organizationId}/policies/evaluate`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
subjectType: "generation_request",
|
||||
subjectId: passSubjectId,
|
||||
kind: "单画面关键帧",
|
||||
prompt: "国漫 2D 动画风格,雨夜地铁口,一个完整单画面。",
|
||||
costMode: "local",
|
||||
endpoint: "http://127.0.0.1:8000/v1",
|
||||
imageOutputCount: 1,
|
||||
batchSize: 1,
|
||||
randomNativeVoice: false,
|
||||
rightsEvidenceApproved: true
|
||||
})
|
||||
}), "evaluate passing request");
|
||||
assert.equal(passEvaluation.evaluation.result, "pass", "本地单画面生成请求应通过组织策略");
|
||||
|
||||
const blockedSubjectId = `policy-block-${Date.now()}`;
|
||||
evaluationSubjectIds.push(blockedSubjectId);
|
||||
const blockedEvaluation = expectOk(await request(`/api/organizations/${organizationId}/policies/evaluate`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
subjectType: "generation_request",
|
||||
subjectId: blockedSubjectId,
|
||||
kind: "单画面关键帧",
|
||||
prompt: "请生成 split-screen 对比图和漫画多格拼图。",
|
||||
costMode: "local",
|
||||
imageOutputCount: 2,
|
||||
batchSize: 2,
|
||||
randomNativeVoice: true,
|
||||
rightsEvidenceApproved: true
|
||||
})
|
||||
}), "evaluate blocked request");
|
||||
assert.equal(blockedEvaluation.evaluation.result, "block", "分屏/多图/随机声线请求必须被组织策略阻断");
|
||||
assert.ok(blockedEvaluation.evaluation.blockers.some((item) => item.policyKey === "generation.single_frame_contract"), "阻断结果必须包含单画面硬门");
|
||||
assert.ok(blockedEvaluation.evaluation.blockers.some((item) => item.policyKey === "voice.fixed_voice_required"), "阻断结果必须包含固定声线硬门");
|
||||
|
||||
const cloudSubjectId = `policy-cloud-${Date.now()}`;
|
||||
evaluationSubjectIds.push(cloudSubjectId);
|
||||
const cloudEvaluation = expectOk(await request(`/api/organizations/${organizationId}/policies/evaluate`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
subjectType: "connector_request",
|
||||
subjectId: cloudSubjectId,
|
||||
kind: "图生视频",
|
||||
prompt: "原创国漫单画面",
|
||||
costMode: "cloud",
|
||||
endpoint: "https://api.example.com/v1",
|
||||
imageOutputCount: 1,
|
||||
batchSize: 1,
|
||||
randomNativeVoice: false,
|
||||
rightsEvidenceApproved: true
|
||||
})
|
||||
}), "evaluate cloud connector request");
|
||||
assert.equal(cloudEvaluation.evaluation.result, "block", "默认商业策略不能直接放行 cloud 成本模式");
|
||||
|
||||
const preview = expectOk(await request("/api/jobs/preview", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform" })
|
||||
}), "preview job with organization policy");
|
||||
assert.equal(preview.preview.organizationPolicy.generation.batchSize, 1, "生成预览合同必须包含组织策略快照");
|
||||
assert.equal(preview.preview.policyEvaluation.result, "pass", "生成预览必须返回策略评估结果");
|
||||
assert.ok(preview.preview.constraints.negativePromptTerms.includes("contact sheet"), "生成合同必须注入组织级单画面禁止词");
|
||||
|
||||
const created = expectOk(await request("/api/jobs", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform", output: "qa/policy-contract/frame.json" })
|
||||
}), "create job with organization policy");
|
||||
createdJobIds.push(created.job.id);
|
||||
assert.equal(created.job.request.organizationPolicy.generation.imageOutputCount, 1, "创建任务必须持久化组织策略快照");
|
||||
assert.equal(created.job.request.policyEvaluation.result, "pass", "创建任务必须持久化组织策略评估");
|
||||
|
||||
const afterCreate = expectOk(await request(`/api/organizations/${organizationId}/policies`, { headers }), "read policy evaluations after job");
|
||||
assert.ok(afterCreate.evaluations.some((item) => item.subjectId === created.job.id), "创建任务后必须写入策略评估 ledger");
|
||||
|
||||
console.log(`organization policy smoke passed: ${api}`);
|
||||
} finally {
|
||||
if (originalRetentionPolicy) {
|
||||
await request(`/api/organizations/${organizationId}/policies/retention.production_evidence`, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify(originalRetentionPolicy)
|
||||
}).catch(() => {});
|
||||
}
|
||||
withTransaction(() => {
|
||||
for (const jobId of createdJobIds) {
|
||||
dbRun("DELETE FROM job_dependencies WHERE job_id = ? OR depends_on_job_id = ?", [jobId, jobId]);
|
||||
dbRun("DELETE FROM job_attempts WHERE job_id = ?", [jobId]);
|
||||
dbRun("DELETE FROM usage_events WHERE metadata_json LIKE ?", [`%${jobId}%`]);
|
||||
dbRun("DELETE FROM audit_logs WHERE target_id = ?", [jobId]);
|
||||
dbRun("DELETE FROM organization_policy_evaluations WHERE subject_id = ?", [jobId]);
|
||||
dbRun("DELETE FROM generation_jobs WHERE id = ?", [jobId]);
|
||||
}
|
||||
for (const subjectId of evaluationSubjectIds) {
|
||||
dbRun("DELETE FROM organization_policy_evaluations WHERE subject_id = ?", [subjectId]);
|
||||
dbRun("DELETE FROM audit_logs WHERE target_id = ?", [subjectId]);
|
||||
}
|
||||
});
|
||||
for (const jobId of createdJobIds) {
|
||||
await rm(resolve(projectRoot, "storage", "jobs", jobId), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user