Files
ai-drama-platform/scripts/smoke-model-routing.mjs

173 lines
8.4 KiB
JavaScript

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 scope = {
"x-organization-id": "org-studio-lab",
"x-workspace-id": "ws-local-aidrama",
"x-project-id": "thunder-mouth"
};
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 };
}
async function login(email = "producer@local.test") {
const result = await request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password: "Demo@123456" })
});
assert.equal(result.response.ok, true, `模型路由 smoke 登录失败:${result.payload.detail || result.payload.error || ""}`);
return { authorization: `Bearer ${result.payload.session.token}`, ...scope };
}
const headers = await login();
const writerHeaders = await login("writer@local.test");
const createdJobIds = [];
let workflowRunId = "";
async function preview(body) {
const result = await request("/api/jobs/preview", { method: "POST", headers, body: JSON.stringify(body) });
assert.equal(result.response.ok, true, `任务预览失败:${result.payload.detail || result.payload.error || ""}`);
return result.payload;
}
function cleanupGeneratedRecords() {
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_type = 'generation_job' AND target_id = ?", [jobId]);
dbRun("DELETE FROM generation_jobs WHERE id = ?", [jobId]);
}
if (workflowRunId) {
dbRun("DELETE FROM audit_logs WHERE target_type = 'workflow_run' AND target_id = ?", [workflowRunId]);
dbRun("DELETE FROM workflow_runs WHERE id = ?", [workflowRunId]);
}
});
}
try {
const imageRouteResolution = await request("/api/platform/model-routes/resolve", {
method: "POST",
headers,
body: JSON.stringify({
workflowKey: "ai-manhua-drama",
operationKey: "image-keyframe",
kind: "单画面关键帧",
adapter: "comfyui-optional",
localOnly: true
})
});
assert.equal(imageRouteResolution.response.status, 200, `模型路由试算失败:${imageRouteResolution.payload.detail || imageRouteResolution.payload.error || ""}`);
assert.equal(imageRouteResolution.payload.resolution.source, "routing-policy", "模型路由试算必须优先命中业务路由");
assert.equal(imageRouteResolution.payload.resolution.route.routeId, "route-image-keyframe", "关键帧试算没有命中单画面路由");
assert.equal(imageRouteResolution.payload.resolution.connector.id, "owned-image", "试算不应被前端连接器选择绕过");
assert.equal(imageRouteResolution.payload.resolution.guard.status, "blocked", "未探活的本地关键帧连接器应被准入预检阻断");
assert.ok(imageRouteResolution.payload.resolution.guard.reasons.some((reason) => reason.code === "connector_not_ready"), "试算结果必须说明连接器尚未就绪");
const mixedRouteResolution = await request("/api/platform/model-routes/resolve", {
method: "POST",
headers,
body: JSON.stringify({
adapter: "newapi-audio-production",
kind: "固定 TTS 配音",
localOnly: true
})
});
assert.equal(mixedRouteResolution.response.status, 200, "混合连接器试算接口失败");
assert.equal(mixedRouteResolution.payload.resolution.guard.status, "blocked", "local-only 预检必须阻断混合/外部连接器");
assert.ok(mixedRouteResolution.payload.resolution.guard.reasons.some((reason) => reason.code === "local_only_violation"), "试算结果必须说明 local-only 冲突");
const deniedResolution = await request("/api/platform/model-routes/resolve", {
method: "POST",
headers: writerHeaders,
body: JSON.stringify({ workflowKey: "ai-manhua-drama", operationKey: "image-keyframe" })
});
assert.equal(deniedResolution.response.status, 403, "普通编剧不应访问模型路由试算器");
const imagePreview = await preview({
kind: "单画面关键帧",
shotId: "shot-01",
adapter: "comfyui-optional"
});
assert.equal(imagePreview.resolution.source, "routing-policy", "关键帧任务必须由路由策略解析");
assert.equal(imagePreview.resolution.route.routeId, "route-image-keyframe", "关键帧没有命中单画面路由");
assert.equal(imagePreview.resolution.route.modelKey, "qwen-image-2d-lock", "关键帧没有解析到 Qwen 单画面模型");
assert.equal(imagePreview.resolution.adapter.id, "owned-image", "路由策略不应被前端连接器选择绕过");
assert.equal(imagePreview.preview.job.modelEntryId, "catalog-qwen-image-2d", "请求合同必须记录模型目录条目");
const auditionPreview = await preview({
kind: "单句 TTS 试听",
shotId: "shot-01",
adapter: "local-tts"
});
assert.equal(auditionPreview.resolution.source, "legacy-adapter", "单句试听应保留本地试听连接器,不进入正式配音路由");
assert.equal(auditionPreview.resolution.adapter.id, "local-tts", "单句试听没有使用本地 TTS 连接器");
const formalTtsPreview = await preview({
kind: "固定 TTS 配音",
shotId: "shot-01",
adapter: "owned-model-platform"
});
assert.equal(formalTtsPreview.resolution.route.routeId, "route-voice-tts", "固定配音没有命中声音路由");
assert.equal(formalTtsPreview.resolution.route.requiresApproval, true, "混合固定配音必须要求审批");
const blockedVoice = await request("/api/jobs", {
method: "POST",
headers,
body: JSON.stringify({
kind: "固定 TTS 配音",
shotId: "shot-01",
adapter: "owned-model-platform",
approveExternal: true
})
});
assert.equal(blockedVoice.response.status, 422, "未授权角色声线必须阻断固定 TTS 任务");
assert.equal(blockedVoice.payload.error, "voice_reference_not_approved", "固定 TTS 的声音授权错误码不正确");
const createdImage = await request("/api/jobs", {
method: "POST",
headers,
body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform" })
});
assert.equal(createdImage.response.status, 201, `关键帧任务创建失败:${createdImage.payload.detail || createdImage.payload.error || ""}`);
createdJobIds.push(createdImage.payload.job.id);
assert.equal(createdImage.payload.job.routing.routeId, "route-image-keyframe", "已创建任务没有保存路由证据");
assert.equal(createdImage.payload.job.request.routing.modelKey, "qwen-image-2d-lock", "请求合同没有保存实际模型 Key");
assert.equal(createdImage.payload.job.cost_policy, "local", "路由成本策略没有写入任务");
const workflow = await request("/api/workflows/templates/workflow-global-ai-manhua-drama-v1/instantiate", {
method: "POST",
headers,
body: JSON.stringify({ mode: "queue", shotId: "shot-01", adapter: "comfyui-optional", approveExternal: true })
});
assert.equal(workflow.response.status, 201, `工作流入队失败:${workflow.payload.detail || workflow.payload.error || ""}`);
workflowRunId = workflow.payload.run.id;
createdJobIds.push(...workflow.payload.jobs);
assert.equal(workflow.payload.run.status, "blocked", "存在未授权固定声线时,工作流必须保持阻塞");
assert.equal(workflow.payload.jobs.length, 2, "未授权固定 TTS 步骤不得创建任务,前序图像和视频任务仍应保留");
for (const jobId of workflow.payload.jobs) {
const detail = await request(`/api/jobs/${encodeURIComponent(jobId)}`, { headers });
assert.equal(detail.response.ok, true, "工作流任务详情读取失败");
assert.ok(detail.payload.job.routing?.routeId, "工作流创建的任务必须记录模型路由");
}
console.log("model routing smoke passed: route resolution, local audition, voice-rights gate, workflow queue");
} finally {
cleanupGeneratedRecords();
for (const jobId of createdJobIds) {
await rm(resolve(projectRoot, "storage", "jobs", jobId), { recursive: true, force: true });
}
}