Files
ai-drama-platform/scripts/smoke-ops.mjs
T

119 lines
6.0 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 scope = {
"x-organization-id": "org-studio-lab",
"x-workspace-id": "ws-local-aidrama",
"x-project-id": "thunder-mouth"
};
const inviteEmail = `ops-${runId}@local.test`;
const outputPrefix = `qa/ops/${runId}/`;
function cleanup() {
const users = dbAll("SELECT id FROM users WHERE email = ?", [inviteEmail]);
const userIds = users.map((user) => user.id);
const jobs = dbAll("SELECT id FROM generation_jobs WHERE output_path LIKE ?", [`${outputPrefix}%`]);
const jobIds = jobs.map((job) => job.id);
withTransaction(() => {
if (jobIds.length) {
const placeholders = jobIds.map(() => "?").join(",");
dbRun(`DELETE FROM media_artifacts WHERE job_id IN (${placeholders})`, jobIds);
dbRun(`DELETE FROM job_dependencies WHERE job_id IN (${placeholders}) OR depends_on_job_id IN (${placeholders})`, [...jobIds, ...jobIds]);
dbRun(`DELETE FROM job_attempts WHERE job_id IN (${placeholders})`, jobIds);
dbRun("DELETE FROM usage_events WHERE metadata_json LIKE ?", [`%${runId}%`]);
dbRun(`DELETE FROM audit_logs WHERE target_id IN (${placeholders})`, jobIds);
dbRun(`DELETE FROM generation_jobs WHERE id IN (${placeholders})`, jobIds);
}
dbRun("DELETE FROM notification_deliveries WHERE request_json LIKE ?", [`%\"runId\":${runId}%`]);
for (const userId of userIds) {
dbRun("DELETE FROM invitations WHERE email = ? OR accepted_user_id = ?", [inviteEmail, userId]);
dbRun("UPDATE audit_logs SET actor_user_id = NULL WHERE actor_user_id = ?", [userId]);
dbRun("UPDATE usage_events SET user_id = NULL WHERE user_id = ?", [userId]);
dbRun("DELETE FROM users WHERE id = ?", [userId]);
}
dbRun("DELETE FROM invitations WHERE email = ?", [inviteEmail]);
});
}
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;
}
try {
const login = expectOk(await request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email: "producer@local.test", password: "Demo@123456" })
}), "owner login");
const headers = { authorization: `Bearer ${login.session.token}`, ...scope };
const invite = expectOk(await request("/api/organizations/org-studio-lab/invitations", {
method: "POST",
headers,
body: JSON.stringify({ email: inviteEmail, roleKey: "writer", workspaceId: "ws-local-aidrama" })
}), "create invitation");
assert.ok(invite.invitation?.inviteToken, "invitation must return a one-time registration token");
const preview = expectOk(await request(`/api/invitations/preview?token=${encodeURIComponent(invite.invitation.inviteToken)}`), "preview invitation");
assert.equal(preview.invitation.email, inviteEmail, "invitation preview email mismatch");
const registration = expectOk(await request("/api/auth/register", {
method: "POST",
body: JSON.stringify({ inviteToken: invite.invitation.inviteToken, displayName: "运营测试用户", password: "Ops@123456" })
}), "register invited user");
assert.ok(registration.session?.token, "registration must create a session");
assert.equal(registration.context.currentOrganization.id, "org-studio-lab", "registered user must enter invited organization");
const firstJob = expectOk(await request("/api/jobs", {
method: "POST",
headers,
body: JSON.stringify({ adapter: "owned-image", kind: "依赖测试关键帧", shotId: "shot-01", output: `${outputPrefix}first.json` })
}), "create first job");
const dependentJob = expectOk(await request("/api/jobs", {
method: "POST",
headers,
body: JSON.stringify({ adapter: "owned-image", kind: "依赖测试任务", shotId: "shot-02", dependsOnJobIds: [firstJob.job.id], output: `${outputPrefix}dependent.json` })
}), "create dependent job");
assert.equal(dependentJob.job.dependencies.length, 1, "dependent job must expose dependency records");
assert.equal(dependentJob.job.status, "blocked", "dependent job must wait for incomplete dependency");
const dependencyRun = await request(`/api/jobs/${encodeURIComponent(dependentJob.job.id)}/run`, { method: "POST", headers, body: "{}" });
assert.equal(dependencyRun.response.status, 409, "incomplete dependency must block execution");
assert.equal(dependencyRun.payload.error, "job_dependencies_unresolved", "dependency block must have a stable error code");
const storage = expectOk(await request("/api/usage/storage", { headers }), "storage usage");
assert.ok(Number.isFinite(storage.storage.usedBytes), "storage usage must return measured bytes");
assert.ok(Number.isFinite(storage.storage.limitBytes), "storage usage must return a byte quota");
const notificationTest = expectOk(await request("/api/system/notifications/test", {
method: "POST",
headers,
body: JSON.stringify({ event: "job.failed", payload: { source: "smoke-ops", runId } })
}), "notification test");
assert.ok(notificationTest.deliveries?.length >= 1, "notification test must create delivery records");
const compose = expectOk(await request("/api/production/compose", {
method: "POST",
headers,
body: JSON.stringify({ dryRun: true, clips: ["storage/jobs/missing-a.mp4", "storage/jobs/missing-b.mp4"], outputPath: `storage/compositions/${runId}.mp4` })
}), "compose dry-run");
assert.equal(compose.composition.dryRun, true, "compose dry-run must not claim a rendered video");
assert.equal(compose.composition.tool, "ffmpeg", "local composition must use ffmpeg");
console.log(`ops smoke passed: ${api}`);
} finally {
cleanup();
}