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