Files

75 lines
5.5 KiB
JavaScript

import assert from "node:assert/strict";
import { createServer } from "node:http";
import { execFileSync } from "node:child_process";
import { mkdir, rm } from "node:fs/promises";
import { resolve } from "node:path";
import { 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 scope = { "x-organization-id": "org-studio-lab", "x-workspace-id": "ws-local-aidrama", "x-project-id": "thunder-mouth" };
const fixtureRelative = "storage/jobs/api-media-smoke/fixture.mp4";
const fixtureAbsolute = resolve(projectRoot, fixtureRelative);
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 };
}
const login = await request("/api/auth/login", { method: "POST", body: JSON.stringify({ email: "producer@local.test", password: "Demo@123456" }) });
assert.equal(login.response.ok, true, "media API smoke login must pass");
const headers = { authorization: `Bearer ${login.payload.session.token}`, ...scope };
const connector = dbGet("SELECT * FROM model_connectors WHERE id = 'owned-i2v'");
const originalConnector = { endpoint: connector.endpoint, status: connector.status, errorMessage: connector.error_message };
const server = createServer(async (req, res) => {
if (req.method !== "POST") { res.writeHead(405); res.end(); return; }
const jobId = req.headers["x-ai-drama-job-id"];
assert.ok(jobId, "adapter request must carry job id");
const outputRelative = `storage/jobs/${jobId}/output.mp4`;
await mkdir(resolve(projectRoot, `storage/jobs/${jobId}`), { recursive: true });
execFileSync("cp", [fixtureAbsolute, resolve(projectRoot, outputRelative)]);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ outputPath: outputRelative, mimeType: "video/mp4", provider: "local-smoke" }));
});
await new Promise((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address();
const endpoint = `http://127.0.0.1:${address.port}/generate`;
const createdJobIds = [];
try {
await mkdir(resolve(projectRoot, "storage/jobs/api-media-smoke"), { recursive: true });
execFileSync("ffmpeg", ["-y", "-v", "error", "-f", "lavfi", "-i", "color=c=0x8b3d59:s=320x568:d=1", "-c:v", "libx264", "-pix_fmt", "yuv420p", fixtureAbsolute], { stdio: "pipe" });
dbRun("UPDATE model_connectors SET endpoint = ?, status = 'ready', error_message = '', updated_at = ? WHERE id = 'owned-i2v'", [endpoint, new Date().toISOString()]);
const created = await request("/api/jobs", { method: "POST", headers, body: JSON.stringify({ adapter: "owned-i2v", kind: "视频片段", shotId: "shot-01", output: "storage/jobs/api-media-smoke/request-output.mp4" }) });
assert.equal(created.response.ok, true, `job create failed: ${created.payload.detail || ""}`);
const jobId = created.payload.job.id;
createdJobIds.push(jobId);
const run = await request(`/api/jobs/${encodeURIComponent(jobId)}/run`, { method: "POST", headers, body: "{}" });
assert.equal(run.response.ok, true, `job run failed: ${run.payload.detail || ""}`);
assert.equal(run.payload.job.status, "completed", "HTTP generation job must complete");
assert.equal(run.payload.job.result.artifacts?.[0]?.status, "inspected", "completed job must return inspected media evidence");
assert.ok(run.payload.job.result.artifacts?.[0]?.sha256, "completed job must return SHA-256 evidence");
const artifacts = await request(`/api/production/media-artifacts?jobId=${encodeURIComponent(jobId)}`, { headers });
assert.equal(artifacts.response.ok, true, "media artifact API must be readable");
assert.equal(artifacts.payload.artifacts?.[0]?.job_id, jobId, "artifact must be linked to the generation job");
const artifactId = artifacts.payload.artifacts[0].id;
const contentResponse = await fetch(`${api}/api/production/media-artifacts/${encodeURIComponent(artifactId)}/content`, { headers });
assert.equal(contentResponse.ok, true, "media artifact content must be readable within the project scope");
assert.match(contentResponse.headers.get("content-type") || "", /video\/mp4/, "artifact preview must preserve video content type");
assert.ok((await contentResponse.arrayBuffer()).byteLength > 0, "artifact preview body must not be empty");
const frameResponse = await fetch(`${api}/api/production/media-artifacts/${encodeURIComponent(artifactId)}/content?frame=last`, { headers });
assert.equal(frameResponse.ok, true, "actual last-frame preview must be readable");
assert.match(frameResponse.headers.get("content-type") || "", /image\/jpeg/, "last-frame preview must be JPEG");
console.log(`api media smoke passed: ${jobId} -> ${artifacts.payload.artifacts[0].last_frame_path}`);
} finally {
for (const jobId of createdJobIds) {
dbRun("DELETE FROM media_artifacts WHERE job_id = ?", [jobId]);
dbRun("DELETE FROM generation_jobs WHERE id = ?", [jobId]);
await rm(resolve(projectRoot, `storage/jobs/${jobId}`), { recursive: true, force: true });
}
dbRun("UPDATE model_connectors SET endpoint = ?, status = ?, error_message = ?, updated_at = ? WHERE id = 'owned-i2v'", [originalConnector.endpoint, originalConnector.status, originalConnector.errorMessage, new Date().toISOString()]);
await new Promise((resolveClose) => server.close(resolveClose));
await rm(resolve(projectRoot, "storage/jobs/api-media-smoke"), { recursive: true, force: true });
}