fix artifact download response contract
This commit is contained in:
@@ -1,17 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { serviceRoutes } from "../src/routes/services.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: Request[] = [];
|
||||
const artifactTypes: Record<string, { mime: string; bytes: number[] }> = {
|
||||
session_dual_wav: { mime: "audio/wav", bytes: [0x52, 0x49, 0x46, 0x46] },
|
||||
session_mixed_mp3: { mime: "audio/mpeg", bytes: [0x49, 0x44, 0x33, 0x04] },
|
||||
session_video: { mime: "video/webm", bytes: [0x1a, 0x45, 0xdf, 0xa3] },
|
||||
session_video_mp4: { mime: "video/mp4", bytes: [0x00, 0x00, 0x00, 0x18] },
|
||||
};
|
||||
|
||||
const storageServer = createServer((request, response) => {
|
||||
const artifactType = request.url?.replace(/^\/stored\//, "") ?? "";
|
||||
const artifact = artifactTypes[artifactType];
|
||||
if (!artifact) { response.writeHead(404); response.end(); return; }
|
||||
response.writeHead(200, { "content-type": artifact.mime, "content-length": artifact.bytes.length });
|
||||
response.end(Buffer.from(artifact.bytes));
|
||||
});
|
||||
await new Promise<void>((resolve) => storageServer.listen(0, "127.0.0.1", resolve));
|
||||
const storagePort = (storageServer.address() as AddressInfo).port;
|
||||
const storageBaseUrl = `http://127.0.0.1:${storagePort}`;
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const request = new Request(input, init);
|
||||
requests.push(request);
|
||||
const path = new URL(request.url).pathname;
|
||||
if (path.endsWith("/api/v1/artifacts/sess-1/session_video")) {
|
||||
return new Response(JSON.stringify({ url: "http://s3.test/obj", mime_type: "video/webm" }), { headers: { "content-type": "application/json" } });
|
||||
const metadataMatch = path.match(/\/api\/v1\/artifacts\/sess-1\/([^/]+)$/);
|
||||
if (metadataMatch && artifactTypes[metadataMatch[1]]) {
|
||||
const artifact = artifactTypes[metadataMatch[1]];
|
||||
return new Response(JSON.stringify({ url: `${storageBaseUrl}/stored/${metadataMatch[1]}`, mime_type: artifact.mime }), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (path === "/obj") return new Response(new Uint8Array([1, 2, 3]), { headers: { "content-type": "video/webm", "content-length": "3" } });
|
||||
if (path.startsWith("/stored/")) return originalFetch(input, init);
|
||||
if (path.endsWith("/api/v1/sessions/sess-1/video/upload-url")) {
|
||||
return new Response(JSON.stringify({ url: "http://s3.test/put", mime_type: "video/webm" }), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
@@ -21,20 +42,23 @@ globalThis.fetch = (async (input, init) => {
|
||||
|
||||
const app = serviceRoutes({ skipAuth: true });
|
||||
try {
|
||||
const artifacts = [
|
||||
["session_dual_wav", "audio/wav", [0x52, 0x49, 0x46, 0x46], "wav"],
|
||||
["session_mixed_mp3", "audio/mpeg", [0x49, 0x44, 0x33, 0x04], "mp3"],
|
||||
["session_video", "video/webm", [0x1a, 0x45, 0xdf, 0xa3], "webm"],
|
||||
["session_video_mp4", "video/mp4", [0x00, 0x00, 0x00, 0x18], "mp4"],
|
||||
] as const;
|
||||
for (const product of ["interview", "roleplay"] as const) {
|
||||
const download = await app.request(`http://backend.test/${product}/artifacts/sess-1/session_video/download`);
|
||||
assert.equal(download.status, 200);
|
||||
const downloadedBytes = new Uint8Array(await download.arrayBuffer());
|
||||
assert.deepEqual(Array.from(downloadedBytes), [1, 2, 3]);
|
||||
assert.equal(download.headers.get("content-type"), "video/webm");
|
||||
assert.equal(new TextDecoder().decode(downloadedBytes).includes("s3.test"), false);
|
||||
|
||||
const uploadBody = new Uint8Array([9, 8, 7]);
|
||||
const upload = await app.request(`http://backend.test/${product}/sessions/sess-1/video/upload`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "video/webm" },
|
||||
body: uploadBody,
|
||||
});
|
||||
for (const [artifactType, mime, expectedBytes, extension] of artifacts) {
|
||||
const download = await app.request(`http://backend.test/${product}/artifacts/sess-1/${artifactType}/download`);
|
||||
assert.equal(download.status, 200);
|
||||
const downloadedBytes = new Uint8Array(await download.arrayBuffer());
|
||||
assert.deepEqual(Array.from(downloadedBytes), expectedBytes);
|
||||
assert.equal(download.headers.get("content-type"), mime);
|
||||
assert.match(download.headers.get("content-disposition") ?? "", new RegExp(`^attachment; filename="${artifactType}\\.${extension}"$`));
|
||||
assert.ok(downloadedBytes.byteLength > 0);
|
||||
}
|
||||
const upload = await app.request(`http://backend.test/${product}/sessions/sess-1/video/upload`, { method: "PUT", headers: { "content-type": "video/webm" }, body: new Uint8Array([9, 8, 7]) });
|
||||
assert.equal(upload.status, 204);
|
||||
const put = requests.at(-1)!;
|
||||
assert.equal(put.method, "PUT");
|
||||
@@ -43,6 +67,7 @@ try {
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
storageServer.close();
|
||||
}
|
||||
|
||||
console.log("artifact transport gateway tests passed");
|
||||
|
||||
@@ -97,6 +97,7 @@ if (!databaseAvailable) {
|
||||
category: "mission" as const,
|
||||
occurredAt: new Date(Date.now() + index),
|
||||
dedupeKey: `${userId}:curator.task.completed:${task.id}`,
|
||||
correlation: { task_id: task.id },
|
||||
payload: { taskId: task.id },
|
||||
})));
|
||||
|
||||
|
||||
+18
-2
@@ -788,6 +788,20 @@ async function proxySocialRequest(req: Request, rest: string, userId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function artifactExtension(mimeType: string | undefined): string {
|
||||
const normalized = mimeType?.split(";", 1)[0].trim().toLowerCase();
|
||||
const extensions: Record<string, string> = {
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/mp3": "mp3",
|
||||
"audio/wav": "wav",
|
||||
"audio/x-wav": "wav",
|
||||
"video/webm": "webm",
|
||||
"video/mp4": "mp4",
|
||||
"application/json": "json",
|
||||
};
|
||||
return extensions[normalized ?? ""] ?? "bin";
|
||||
}
|
||||
|
||||
async function streamServiceArtifact(input: { service: "interview" | "roleplay"; sessionId: string; artifactType: string; userId: string; request: Request }) {
|
||||
const metadata = input.service === "interview"
|
||||
? await interviewService.artifact(input.sessionId, input.artifactType, input.userId)
|
||||
@@ -806,8 +820,10 @@ async function streamServiceArtifact(input: { service: "interview" | "roleplay";
|
||||
const value = upstream.headers.get(name);
|
||||
if (value) responseHeaders.set(name, value);
|
||||
}
|
||||
if (!responseHeaders.has("content-type") && isRecord(metadata) && typeof metadata.mime_type === "string") responseHeaders.set("content-type", metadata.mime_type);
|
||||
responseHeaders.set("content-disposition", `inline; filename="${input.artifactType}"`);
|
||||
const mimeType = responseHeaders.get("content-type") ?? (isRecord(metadata) ? getString(metadata.mime_type) : undefined);
|
||||
if (mimeType && !responseHeaders.has("content-type")) responseHeaders.set("content-type", mimeType);
|
||||
const filename = `${input.artifactType}.${artifactExtension(mimeType)}`;
|
||||
responseHeaders.set("content-disposition", `attachment; filename="${filename}"`);
|
||||
responseHeaders.set("cache-control", "private, max-age=300");
|
||||
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user