import assert from "node:assert/strict"; import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import { dbAll, dbGet, dbRun } 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 runId = Date.now(); const sourceRoot = `storage/smoke-release-${runId}`; const deliveryLabel = `smoke-release-delivery-${runId}`; const batchId = `smoke-release-batch-${runId}`; const channelName = `smoke-release-channel-${runId}`; const version = `smoke-${runId}`; const sourcePath = `${sourceRoot}/clip.mp4`; const lastFramePath = `${sourceRoot}/actual-last-frame.jpg`; const manifestPath = `${sourceRoot}/manifest.json`; 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 expectStatus(result, status, label) { assert.equal(result.response.status, status, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`); return result.payload; } function expectOk(result, label) { assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`); return result.payload; } const producerLogin = expectOk(await request("/api/auth/login", { method: "POST", body: JSON.stringify({ email: "producer@local.test", password: "Demo@123456" }) }), "producer login"); const reviewerLogin = expectOk(await request("/api/auth/login", { method: "POST", body: JSON.stringify({ email: "review@local.test", password: "Demo@123456" }) }), "reviewer login"); const producerHeaders = { authorization: `Bearer ${producerLogin.session.token}`, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId }; const reviewerHeaders = { authorization: `Bearer ${reviewerLogin.session.token}`, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId }; const projectShot = dbGet("SELECT id FROM shots WHERE episode_id IN (SELECT e.id FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN series sr ON sr.id = se.series_id WHERE sr.project_id = ?) ORDER BY shot_number LIMIT 1", [projectId]); assert.ok(projectShot?.id, "smoke project must have a starter shot"); const producerUser = dbGet("SELECT id FROM users WHERE email = ?", ["producer@local.test"]); assert.ok(producerUser?.id, "producer user must exist"); const channelPath = `/api/production/delivery-channels`; let channelId = ""; let releaseId = ""; let createdDeliveryId = ""; let seeded = false; try { const channels = expectOk(await request(channelPath, { headers: reviewerHeaders }), "reviewer can view channels"); assert.ok(channels.channels.some((channel) => channel.kind === "local-file"), "workspace must expose the default local-file channel"); expectStatus(await request(channelPath, { method: "POST", headers: reviewerHeaders, body: JSON.stringify({ name: "reviewer must not create", kind: "local-file" }) }), 403, "reviewer cannot create a channel"); expectStatus(await request(channelPath, { method: "POST", headers: producerHeaders, body: JSON.stringify({ name: "public webhook must be rejected", kind: "local-webhook", endpoint: "https://example.com/release" }) }), 400, "public webhook is rejected"); const channel = expectStatus(await request(channelPath, { method: "POST", headers: producerHeaders, body: JSON.stringify({ name: channelName, kind: "local-file", endpoint: "storage/releases", requireApproval: true }) }), 201, "producer creates local channel"); channelId = channel.channel.id; const delivery = expectStatus(await request("/api/production/deliveries", { method: "POST", headers: producerHeaders, body: JSON.stringify({ version, channel: deliveryLabel }) }), 201, "producer creates delivery draft"); createdDeliveryId = delivery.delivery.id; assert.equal(delivery.delivery.status, "draft", "new delivery must start as draft"); await mkdir(resolve(projectRoot, sourceRoot), { recursive: true }); await writeFile(resolve(projectRoot, sourcePath), Buffer.from("fake local video evidence")); await writeFile(resolve(projectRoot, lastFramePath), Buffer.from("fake actual last frame evidence")); await writeFile(resolve(projectRoot, manifestPath), JSON.stringify({ schema: "smoke-manifest", deliveryId: createdDeliveryId }, null, 2)); const timestamp = new Date().toISOString(); dbRun("INSERT INTO delivery_batches(id, organization_id, workspace_id, project_id, delivery_id, batch_number, label, status, manifest_path, source_json, result_json, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, 'smoke active batch', 'active', ?, '{}', ?, ?, ?, ?)", [batchId, organizationId, workspaceId, projectId, delivery.delivery.id, manifestPath, JSON.stringify({ blockers: [], manifestWritten: true }), producerUser.id, timestamp, timestamp]); dbRun("INSERT INTO delivery_batch_items(id, batch_id, shot_id, sequence_number, source_path, actual_last_frame_path, source_sha256, metadata_json, created_at) VALUES (?, ?, ?, 1, ?, ?, ?, '{}', ?)", [`smoke-release-item-${runId}`, batchId, projectShot.id, sourcePath, lastFramePath, "a".repeat(64), timestamp]); dbRun("UPDATE deliveries SET status = 'approved', active_batch_id = ?, approved_by = ?, approved_at = ?, updated_at = ? WHERE id = ?", [batchId, producerUser.id, timestamp, timestamp, delivery.delivery.id]); seeded = true; const release = expectStatus(await request(`/api/production/deliveries/${encodeURIComponent(delivery.delivery.id)}/releases`, { method: "POST", headers: producerHeaders, body: JSON.stringify({ channelId, submit: true, idempotencyKey: `release-${runId}` }) }), 201, "producer submits release"); releaseId = release.release.id; assert.equal(release.release.status, "submitted", "new release request must be submitted"); const reviewerReleases = expectOk(await request(`/api/production/deliveries/${encodeURIComponent(delivery.delivery.id)}/releases`, { headers: reviewerHeaders }), "reviewer can view release records"); assert.equal(reviewerReleases.releases.length, 1, "reviewer must see the submitted release"); expectStatus(await request(`/api/production/releases/${encodeURIComponent(releaseId)}/decision`, { method: "POST", headers: reviewerHeaders, body: JSON.stringify({ status: "approved" }) }), 403, "reviewer cannot approve release"); const approved = expectOk(await request(`/api/production/releases/${encodeURIComponent(releaseId)}/decision`, { method: "POST", headers: producerHeaders, body: JSON.stringify({ status: "approved", note: "smoke approval" }) }), "producer approves release"); assert.equal(approved.release.status, "approved", "release must become approved"); const published = expectOk(await request(`/api/production/releases/${encodeURIComponent(releaseId)}/publish`, { method: "POST", headers: producerHeaders, body: "{}" }), "producer publishes release"); assert.equal(published.release.status, "published", "release must become published"); assert.ok(published.release.output_path, "published release must return an output path"); await access(resolve(projectRoot, published.release.output_path)); const publishedDocument = await readFile(resolve(projectRoot, published.release.output_path), "utf8"); assert.match(publishedDocument, /ai-drama-platform\.delivery-release\.v1/, "release.json must contain the release schema"); const repeated = expectOk(await request(`/api/production/releases/${encodeURIComponent(releaseId)}/publish`, { method: "POST", headers: producerHeaders, body: "{}" }), "repeated publish is idempotent"); assert.equal(repeated.idempotent, true, "published release retry must be idempotent"); const crossOrganization = await request(`/api/production/deliveries/${encodeURIComponent(delivery.delivery.id)}/releases`, { headers: { ...producerHeaders, "x-organization-id": "org-northstar", "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" } }); assert.ok([403, 404].includes(crossOrganization.response.status), `cross organization release access must be hidden or denied: ${crossOrganization.response.status}`); const audit = dbGet("SELECT COUNT(*) AS count FROM audit_logs WHERE target_id = ? AND action IN ('delivery.release.submitted', 'delivery.release.approved', 'delivery.release.published')", [releaseId]); assert.equal(Number(audit?.count || 0), 3, "release lifecycle must write three audit records"); console.log(`release workflow smoke passed: ${releaseId}`); } finally { if (releaseId) dbRun("DELETE FROM audit_logs WHERE target_id = ?", [releaseId]); if (seeded) { dbRun("DELETE FROM delivery_releases WHERE delivery_id = ?", [createdDeliveryId]); dbRun("DELETE FROM delivery_batch_items WHERE batch_id = ?", [batchId]); dbRun("DELETE FROM delivery_batches WHERE id = ?", [batchId]); dbRun("DELETE FROM deliveries WHERE id = ?", [createdDeliveryId]); } if (channelId) dbRun("DELETE FROM delivery_channels WHERE id = ?", [channelId]); dbRun("DELETE FROM audit_logs WHERE target_id = ?", [createdDeliveryId]); await rm(resolve(projectRoot, sourceRoot), { recursive: true, force: true }); }