Files
OmniRoute/tests/unit/guardrails/videoBridgeSubtitleProbe.test.ts
Diego Rodrigues de Sa e Souza 60dc242178 feat(video): derive embedded subtitle provenance in the protected broker (#11659) (#12011)
FU-05 subtitle adapter (Refs #11659 — deliberately not Closes: the adapter is not yet wired into the live describeVideoPart path, that composition point is sibling #12009 which just landed): server-owned, loopback-only ffprobe/ffmpeg subtitle extraction that legitimately earns the "embedded" provenance label, mirroring the existing frame-extraction lifecycle. Broker route now also serves ?subtitles=1, stamped with the shared broker fingerprint so the client-side adapter can verify the payload actually came from the trusted process. Bounded, ReDoS-safe WebVTT parser.
2026-08-29 19:33:27 -03:00

318 lines
12 KiB
TypeScript
Raw Blame History

import assert from "node:assert/strict";
import test from "node:test";
import {
parseBoundedWebVtt,
probeEmbeddedVideoSubtitles,
resolveVideoSubtitleSubdeadlineMs,
VIDEO_SUBTITLE_SUBDEADLINE_MS,
} from "../../../src/lib/guardrails/videoBridgeSubtitleProbe.ts";
import { currentVideoBridgeBrokerFingerprint } from "../../../src/lib/guardrails/videoBridgeBrokerAuth.ts";
import type { VideoSubtitleBrokerResponse } from "../../../src/lib/guardrails/videoBridgeBrokerClient.ts";
const VALID_WEBVTT = "WEBVTT\n\n00:00:01.000 --> 00:00:04.000\nHello world\n";
function brokerResponse(
overrides: Partial<VideoSubtitleBrokerResponse> = {}
): VideoSubtitleBrokerResponse {
return {
durationSeconds: 10,
fingerprint: currentVideoBridgeBrokerFingerprint(),
formatName: "mp4",
streams: [{ codecName: "webvtt", streamIndex: 3, webvtt: VALID_WEBVTT }],
...overrides,
};
}
// ─── resolveVideoSubtitleSubdeadlineMs ─────────────────────────────────────
test("the 10s subtitle subdeadline is clamped by the remaining request deadline", () => {
assert.equal(resolveVideoSubtitleSubdeadlineMs(60_000), VIDEO_SUBTITLE_SUBDEADLINE_MS);
assert.equal(resolveVideoSubtitleSubdeadlineMs(2_000), 2_000);
assert.equal(resolveVideoSubtitleSubdeadlineMs(-5), 0);
assert.equal(resolveVideoSubtitleSubdeadlineMs(Number.NaN), VIDEO_SUBTITLE_SUBDEADLINE_MS);
});
// ─── parseBoundedWebVtt ─────────────────────────────────────────────────────
test("parses a well-formed WebVTT cue, stripping tags and joining wrapped lines", () => {
const cues = parseBoundedWebVtt(
"WEBVTT\n\n1\n00:00:01.500 --> 00:00:03.250\n<b>Hello</b>\nworld\n",
10
);
assert.deepEqual(cues, [{ endSeconds: 3.25, startSeconds: 1.5, text: "Hello world" }]);
});
test("returns no cues for text missing the WEBVTT magic header", () => {
assert.deepEqual(parseBoundedWebVtt("00:00:01.000 --> 00:00:02.000\nnot really vtt\n", 10), []);
});
test("drops a cue whose end does not exceed its start, or whose start is beyond the duration", () => {
const backwards = parseBoundedWebVtt("WEBVTT\n\n00:00:05.000 --> 00:00:02.000\ntext\n", 10);
assert.deepEqual(backwards, []);
const beyondDuration = parseBoundedWebVtt("WEBVTT\n\n00:00:20.000 --> 00:00:22.000\ntext\n", 10);
assert.deepEqual(beyondDuration, []);
});
test("drops a cue containing the UTF-8 replacement character (invalid encoding)", () => {
const cues = parseBoundedWebVtt(
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\ncorrupted <20> text\n",
10
);
assert.deepEqual(cues, []);
});
test("drops a cue with an individual line beyond the 4096 code-unit bound", () => {
const oversizedLine = "a".repeat(4_097);
const cues = parseBoundedWebVtt(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\n${oversizedLine}\n`, 10);
assert.deepEqual(cues, []);
});
test("multiple cues come back sorted by start time regardless of source order", () => {
const cues = parseBoundedWebVtt(
"WEBVTT\n\n00:00:05.000 --> 00:00:06.000\nsecond\n\n00:00:01.000 --> 00:00:02.000\nfirst\n",
10
);
assert.deepEqual(
cues.map((cue) => cue.text),
["first", "second"]
);
});
test("a cue identifier line before the timing line is tolerated", () => {
const cues = parseBoundedWebVtt("WEBVTT\n\ncue-1\n00:00:01.000 --> 00:00:02.000\ntext\n", 10);
assert.equal(cues.length, 1);
});
// ─── probeEmbeddedVideoSubtitles: outcome contract ─────────────────────────
test("valid: a single verified stream with usable cues yields a cacheable success outcome", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => brokerResponse() }
);
assert.equal(outcome.outcome, "success");
assert.equal(outcome.cacheable, true);
if (outcome.outcome === "success") {
assert.deepEqual(outcome.cues, [
{ confidence: 1, endSeconds: 4, source: "embedded", startSeconds: 1, text: "Hello world" },
]);
}
});
test("absent: the broker finds zero allowlisted subtitle streams", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => brokerResponse({ streams: [] }) }
);
assert.deepEqual(outcome, { cacheable: true, outcome: "absent" });
});
test("absent: a structurally read stream that yields zero usable cues after parsing", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{
probeBroker: async () =>
brokerResponse({
streams: [{ codecName: "subrip", streamIndex: 2, webvtt: "WEBVTT\n\n" }],
}),
}
);
assert.deepEqual(outcome, { cacheable: true, outcome: "absent" });
});
test("malformed: a single stream whose WebVTT never yields a valid cue is not cacheable success/absent confusion", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{
probeBroker: async () =>
brokerResponse({
streams: [{ codecName: "webvtt", streamIndex: 1, webvtt: "not webvtt at all" }],
}),
}
);
// Structurally garbage content is treated the same as "no usable subtitles" (stable fact,
// safe to cache) rather than a transient/infra failure — it will not resolve on retry.
assert.deepEqual(outcome, { cacheable: true, outcome: "absent" });
});
test("oversized: a broker response exceeding the client-side WebVTT length bound is a transient failure", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{
probeBroker: async () => {
throw new Error("Video extraction broker returned invalid subtitle metadata");
},
}
);
assert.equal(outcome.outcome, "transient_failure");
assert.equal(outcome.cacheable, false);
});
test("invalid encoding: a corrupted cue is dropped but a clean cue in the same stream still succeeds", async () => {
const webvtt =
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\ncorrupted <20> text\n\n00:00:03.000 --> 00:00:04.000\nclean text\n";
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => brokerResponse({ streams: [{ codecName: "webvtt", streamIndex: 1, webvtt }] }) }
);
assert.equal(outcome.outcome, "success");
if (outcome.outcome === "success") {
assert.equal(outcome.cues.length, 1);
assert.equal(outcome.cues[0].text, "clean text");
}
});
test("timeout: the broker never resolves within the (shrunk) subdeadline, resolving to transient_failure", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 10 },
{
// Simulates what extractVideoSubtitlesViaBroker does internally with
// AbortSignal.timeout(options.timeoutMs): the real transport rejects once its own
// internal timer elapses, independent of whether the caller's own signal ever fires.
probeBroker: (_bytes, options) =>
new Promise((_resolve, reject) => {
const timer = setTimeout(
() => reject(new Error("Video extraction request aborted")),
options.timeoutMs
);
options.signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(new Error("Video extraction request aborted"));
});
}),
}
);
assert.equal(outcome.outcome, "transient_failure");
assert.equal(outcome.cacheable, false);
});
test("abort: a caller-driven abort propagates as a rejection instead of a resolved outcome", async () => {
const controller = new AbortController();
const pending = probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000, signal: controller.signal },
{
probeBroker: (_bytes, options) =>
new Promise((_resolve, reject) => {
options.signal?.addEventListener("abort", () => reject(new Error("aborted")));
}),
}
);
controller.abort();
await assert.rejects(() => pending);
});
test("abort: an already-aborted signal rejects before the broker is ever called", async () => {
const controller = new AbortController();
controller.abort();
let called = false;
await assert.rejects(() =>
probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000, signal: controller.signal },
{
probeBroker: async () => {
called = true;
return brokerResponse();
},
}
)
);
assert.equal(called, false);
});
test("multi-stream selection: the first stream without usable cues is skipped in favor of the second", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{
probeBroker: async () =>
brokerResponse({
streams: [
{ codecName: "webvtt", streamIndex: 1, webvtt: "WEBVTT\n\n" },
{
codecName: "subrip",
streamIndex: 4,
webvtt: "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nsecond stream wins\n",
},
],
}),
}
);
assert.equal(outcome.outcome, "success");
if (outcome.outcome === "success") {
assert.equal(outcome.cues[0].text, "second stream wins");
}
});
test("broker forgery: a response with a mismatched fingerprint never yields embedded provenance", async () => {
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => brokerResponse({ fingerprint: "00000000-0000-0000-0000-000000000000" }) }
);
assert.equal(outcome.outcome, "transient_failure");
assert.equal(outcome.cacheable, false);
assert.equal("cues" in outcome, false);
});
test("broker forgery: a response missing the fingerprint field entirely is rejected the same way", async () => {
const forged = brokerResponse() as unknown as Record<string, unknown>;
delete forged.fingerprint;
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => forged as unknown as VideoSubtitleBrokerResponse }
);
assert.equal(outcome.outcome, "transient_failure");
});
test("cache poisoning guard: every transient_failure carries cacheable:false; success/absent carry true", async () => {
const success = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => brokerResponse() }
);
const absent = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{ probeBroker: async () => brokerResponse({ streams: [] }) }
);
const transient = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 30_000 },
{
probeBroker: async () => {
throw new Error("broker unavailable");
},
}
);
assert.equal(success.cacheable, true);
assert.equal(absent.cacheable, true);
assert.equal(transient.cacheable, false);
});
test("deadline already exhausted before the broker is called resolves to a transient failure, not a hang", async () => {
let called = false;
const outcome = await probeEmbeddedVideoSubtitles(
Buffer.from("video"),
{ requestDeadlineRemainingMs: 0 },
{
probeBroker: async () => {
called = true;
return brokerResponse();
},
}
);
assert.equal(outcome.outcome, "transient_failure");
assert.equal(called, false);
});