evOo-local/packages/adapters/src/media-sidecar.test.ts
Claude 270ea8068a
feat(media): inference emulator, sidecar client, folder source and factory
Add the Media Studio adapters behind the frozen provider contracts: a
deterministic offline inference emulator driven by file-name markers that
renders real redactions with ffmpeg, a synthetic emulator library, the
bearer-auth HTTP client for the local media sidecar with validated
responses, typed errors, busy retries and timeouts, a lazily paginated
folder source that never follows symlinks, and the configuration factory.
A shared conformance suite runs against the emulator and, in the Docker
integration job, against the sidecar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
2026-10-03 17:52:17 +00:00

223 lines
8.1 KiB
TypeScript

import { UnsupportedMediaOperationError } from "@rakazo/adapter-kit";
import { MediaInferenceOperationSchema } from "@rakazo/contracts";
import { describe, expect, it } from "vitest";
import { MediaSidecarError, SidecarMediaInferenceProvider } from "./media-sidecar.js";
const TOKEN = "test-token-0123456789abcdef0123456789abcdef";
const SCENES = {
shots: [{ startMs: 0, endMs: 1000, score: 1 }],
scenes: [{ startMs: 0, endMs: 1000 }],
};
interface Call {
url: string;
init: RequestInit;
}
function json(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function fakeFetch(responses: Array<Response | ((init: RequestInit) => Promise<Response>)>) {
const calls: Call[] = [];
const impl = (async (input: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(input), init: init ?? {} });
const next = responses.shift();
if (!next) throw new Error("unexpected request");
return typeof next === "function" ? next(init ?? {}) : next;
}) as typeof fetch;
return { impl, calls };
}
function client(fetchImpl: typeof fetch, extra: { retryBaseMs?: number } = {}) {
return new SidecarMediaInferenceProvider({
baseUrl: "http://127.0.0.1:8800/",
token: TOKEN,
fetch: fetchImpl,
retryBaseMs: 0,
...extra,
});
}
describe("SidecarMediaInferenceProvider", () => {
it("posts camelCase JSON with bearer auth and without the signal", async () => {
const { impl, calls } = fakeFetch([json(200, SCENES)]);
const controller = new AbortController();
const result = await client(impl).detectScenes({
path: "/data/media/a.mp4",
signal: controller.signal,
});
expect(result).toEqual(SCENES);
expect(calls).toHaveLength(1);
const [call] = calls;
expect(call?.url).toBe("http://127.0.0.1:8800/v1/detectScenes");
expect(call?.init.method).toBe("POST");
const headers = call?.init.headers as Record<string, string>;
expect(headers.authorization).toBe(`Bearer ${TOKEN}`);
expect(headers["content-type"]).toBe("application/json");
expect(JSON.parse(String(call?.init.body))).toEqual({ path: "/data/media/a.mp4" });
expect(call?.init.signal).toBeInstanceOf(AbortSignal);
});
it("reads capabilities with GET", async () => {
const capabilities = {
configured: true,
operations: Object.fromEntries(
MediaInferenceOperationSchema.options.map((operation) => [
operation,
{
available: operation !== "synthesizeIdentity",
experimental: false,
model: "m",
license: "Apache-2.0",
commercialUse: true,
},
]),
),
accelerator: "cpu",
hardwareEncoders: [],
};
const { impl, calls } = fakeFetch([json(200, capabilities)]);
await expect(client(impl).capabilities()).resolves.toEqual(capabilities);
expect(calls[0]?.url).toBe("http://127.0.0.1:8800/v1/capabilities");
expect(calls[0]?.init.method).toBe("GET");
});
it("maps 501 to UnsupportedMediaOperationError", async () => {
const { impl } = fakeFetch([
json(501, {
error: { code: "unsupported", message: "no licensed model", operation: "inpaintVideo" },
}),
]);
const error = await client(impl)
.inpaintVideo({ path: "/m/a.mp4", maskDir: "/m/masks", outPath: "/m/out.mp4" })
.catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(UnsupportedMediaOperationError);
expect((error as UnsupportedMediaOperationError).operation).toBe("inpaintVideo");
expect((error as Error).message).toContain("no licensed model");
});
it("retries 503 busy with backoff, at most three times", async () => {
const busy = () => json(503, { error: { code: "busy", message: "queue full" } });
const recovered = fakeFetch([busy(), busy(), json(200, SCENES)]);
await expect(client(recovered.impl).detectScenes({ path: "/m/a.mp4" })).resolves.toEqual(
SCENES,
);
expect(recovered.calls).toHaveLength(3);
const exhausted = fakeFetch([busy(), busy(), busy(), busy(), busy()]);
const error = await client(exhausted.impl)
.detectScenes({ path: "/m/a.mp4" })
.catch((caught: unknown) => caught);
expect(exhausted.calls).toHaveLength(4);
expect(error).toBeInstanceOf(MediaSidecarError);
expect((error as MediaSidecarError).code).toBe("busy");
expect((error as MediaSidecarError).status).toBe(503);
});
it("maps typed sidecar errors", async () => {
const { impl } = fakeFetch([
json(400, { error: { code: "invalid_path", message: "path is outside MEDIA_ROOT" } }),
json(500, "not json-shaped"),
]);
const provider = client(impl);
await expect(provider.detectScenes({ path: "/etc/passwd" })).rejects.toMatchObject({
name: "MediaSidecarError",
code: "invalid_path",
status: 400,
operation: "detectScenes",
});
await expect(provider.detectScenes({ path: "/m/a.mp4" })).rejects.toMatchObject({
code: "http_error",
status: 500,
});
});
it("passes caller aborts through as AbortError", async () => {
const controller = new AbortController();
const { impl } = fakeFetch([
(init) =>
new Promise<Response>((_, reject) => {
init.signal?.addEventListener("abort", () => reject(init.signal?.reason));
setTimeout(() => controller.abort(), 10);
}),
]);
await expect(
client(impl).trackFaces({ path: "/m/a.mp4", sampleFps: 5, signal: controller.signal }),
).rejects.toMatchObject({ name: "AbortError" });
const already = new AbortController();
already.abort();
const idle = fakeFetch([]);
await expect(
client(idle.impl).detectScenes({ path: "/m/a.mp4", signal: already.signal }),
).rejects.toMatchObject({ name: "AbortError" });
expect(idle.calls).toHaveLength(0);
});
it("reports a timeout as a typed error", async () => {
const { impl } = fakeFetch([
(init) =>
new Promise<Response>((_, reject) => {
init.signal?.addEventListener("abort", () => reject(init.signal?.reason));
}),
]);
const provider = new SidecarMediaInferenceProvider({
baseUrl: "http://127.0.0.1:8800",
token: TOKEN,
fetch: impl,
timeoutsMs: { detectScenes: 20 },
});
await expect(provider.detectScenes({ path: "/m/a.mp4" })).rejects.toMatchObject({
code: "timeout",
});
});
it("rejects malformed responses", async () => {
const { impl } = fakeFetch([
json(200, { shots: [{ startMs: -1, endMs: 10, score: 1 }], scenes: [] }),
json(200, {
frames: [{ tMs: 0, faces: [{ box: { x: 2, y: 0, w: 0.1, h: 0.1 }, confidence: 1 }] }],
}),
json(200, { model: "m", dim: 3, vectors: [[1, 2]] }),
new Response("<html>", { status: 200 }),
]);
const provider = client(impl);
await expect(provider.detectScenes({ path: "/m/a.mp4" })).rejects.toMatchObject({
code: "malformed_response",
});
await expect(provider.detectFaces({ path: "/m/a.mp4", sampleFps: 1 })).rejects.toMatchObject({
code: "malformed_response",
});
await expect(provider.embedFrames({ path: "/m/a.mp4", timesMs: [0] })).rejects.toMatchObject({
code: "malformed_response",
});
await expect(provider.detectScenes({ path: "/m/a.mp4" })).rejects.toMatchObject({
code: "malformed_response",
});
});
it("never puts the token into error messages", async () => {
const { impl } = fakeFetch([
json(401, { error: { code: "unauthorized", message: `bad token ${TOKEN}` } }),
json(500, { error: { code: "internal", message: `crashed with Bearer ${TOKEN}` } }),
async () => {
throw new TypeError(`connect ECONNREFUSED (auth ${TOKEN})`);
},
]);
const provider = client(impl);
for (let i = 0; i < 3; i++) {
const error = (await provider
.detectScenes({ path: "/m/a.mp4" })
.catch((caught: unknown) => caught)) as Error;
expect(error).toBeInstanceOf(MediaSidecarError);
expect(error.message).not.toContain(TOKEN);
expect(JSON.stringify(error)).not.toContain(TOKEN);
expect(String(error.stack)).not.toContain(TOKEN);
}
});
});