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
223 lines
8.1 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|