test(qa): align AI oracles with fixtures and honor QA_OUT_DIR (#686)

The ocr oracle demanded six pangram words when ocr-clean.png prints five; the ocr-pdf oracle expected a searchable PDF when the tool's contract is text extraction; media-30s.mp4 was eight seconds long. Fix all three, regenerate tool-contract.json from current schemas, record the new fixture in the manifest, and add a QA_OUT_DIR override so parallel machines stop clobbering each other's lane output. Fixes #677.
This commit is contained in:
SnapOtter
2026-07-30 10:32:07 +08:00
committed by GitHub
parent 142aeaf9c5
commit 6c2fa307f4
7 changed files with 28 additions and 23 deletions
+3 -3
View File
@@ -402,11 +402,11 @@
},
{
"path": "video/valid/media-30s.mp4",
"sourceUrl": "Big Buck Bunny (Blender Foundation, www.bigbuckbunny.org) via archive.org, 8s segment at 30s, 480x270",
"sourceUrl": "Big Buck Bunny (Blender Foundation, www.bigbuckbunny.org) via archive.org, 8s segment at 30s, 480x270, stream-looped to a true 30.1s (#677)",
"license": "CC-BY",
"toolsServed": [],
"sha256": "6ad37435eeb01cc694b918f8588255fead60eb01f2a06ca0dc904070064e9136",
"bytes": 247284
"sha256": "19c3256788660ee49766497dee364c38bf508050f505f949b8ad41714d7db4de",
"bytes": 941146
},
{
"path": "video/valid/speech-10s.mp4",
Binary file not shown.
@@ -570,17 +570,6 @@ export function expectJapaneseScript(text: string, minimumChars = 4): void {
);
}
/** PDF OCR: a real PDF that now carries a selectable text layer. */
export function expectSearchablePdf(output: Buffer): void {
assertOracle(output.subarray(0, 5).toString("latin1") === "%PDF-", "artifact is not a PDF");
assertOracle(output.length > 1000, "PDF artifact is implausibly small");
const body = output.toString("latin1");
assertOracle(
body.includes("/Font") || body.includes("BT\n") || body.includes("Tj"),
"PDF carries no text-drawing operators, so no OCR layer was added",
);
}
/** Animated cut-outs must stay animated: a single-frame result is a regression. */
export async function expectAnimatedFrames(output: Buffer, minimumFrames = 2): Promise<void> {
const meta = await sharp(output, { pages: -1 }).metadata();
+6 -1
View File
@@ -44,10 +44,15 @@ const PASSWORD = process.env.QA_PASSWORD ?? "";
const CONCURRENCY = Number(process.env.QA_CONCURRENCY ?? 4);
const PAIRWISE_CAP = Number(process.env.QA_PAIRWISE_CAP ?? 6);
const FORMAT_WITNESSES = Number(process.env.QA_FORMAT_WITNESSES ?? 3);
// QA_OUT_DIR lets several machines sweep in parallel without clobbering each
// other's lane-<mode>.jsonl (#677).
const OUT_DIR_OVERRIDE = process.env.QA_OUT_DIR;
/* biome-ignore-end lint/suspicious/noUndeclaredEnvVars: QA harness runs outside Turbo. */
const REPO = join(import.meta.dirname, "..", "..");
const OUT_DIR = join(REPO, "docs", "qa", "master-20260724", "evidence", "processing-ai", "final");
const OUT_DIR =
OUT_DIR_OVERRIDE ??
join(REPO, "docs", "qa", "master-20260724", "evidence", "processing-ai", "final");
/** Class-aware hard timeouts. Exceeding one is a finding, never a silent skip. */
const TIMEOUT_MS: Record<string, number> = { fast: 240_000, long: 480_000, ai: 900_000 };
+4 -1
View File
@@ -31,12 +31,15 @@ const BASE = process.env.QA_BASE_URL ?? "http://localhost:13492";
const USERNAME = process.env.QA_USERNAME ?? "admin";
const PASSWORD = process.env.QA_PASSWORD ?? "";
const PROJECT = process.env.QA_COMPOSE_PROJECT ?? "snapotter-qa-f4c2bde9";
const OUT_DIR_OVERRIDE = process.env.QA_OUT_DIR;
/* biome-ignore-end lint/suspicious/noUndeclaredEnvVars: QA harness runs outside Turbo. */
const APP = `${PROJECT}-app`;
const PG = `${PROJECT}-postgres`;
const REPO = join(import.meta.dirname, "..", "..");
const OUT_DIR = join(REPO, "docs", "qa", "master-20260724", "evidence", "processing-ai", "final");
const OUT_DIR =
OUT_DIR_OVERRIDE ??
join(REPO, "docs", "qa", "master-20260724", "evidence", "processing-ai", "final");
const PNG = resolveFixture(".png", "image") as string;
const JPG = resolveFixture(".jpg", "image") as string;
+1 -1
View File
@@ -3374,7 +3374,7 @@
},
{
"key": "sobelThreshold",
"values": [1, 10.5, 20, null]
"values": [1, 11, 20, null]
},
{
"key": "square",
+14 -6
View File
@@ -37,7 +37,6 @@ import {
expectRedPixelsReduced,
expectRegionRewritten,
expectSameSizeButChanged,
expectSearchablePdf,
expectSrtArtifact,
expectUpscaled,
} from "../helpers/installed-ai-output-oracles.js";
@@ -102,7 +101,11 @@ const SPEECH_WAV = `${FIXTURES}/audio/valid/speech-10s.wav`;
const SPEECH_MP4 = `${FIXTURES}/video/valid/speech-10s.mp4`;
/** The English OCR fixture's known answer, checked word by word. */
const OCR_ENGLISH_TERMS = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"] as const;
// ocr-clean.png literally reads "The quick brown fox 12345"; the oracle must
// demand the fixture's words, not the full pangram (#677).
const OCR_ENGLISH_TERMS = ["the", "quick", "brown", "fox", "12345"] as const;
// ocr-scanned.pdf is a chat screenshot about Redis scaling.
const OCR_PDF_TERMS = ["redis", "replicas", "cache", "bottleneck"] as const;
const CASES: ToolCase[] = [
{
@@ -398,10 +401,10 @@ const CASES: ToolCase[] = [
fixture: OCR_CLEAN,
contentType: "image/png",
settings: { quality: "fast", language: "en", enhance: false },
oracle: "recognizes >=6 of the 8 known English fixture words",
oracle: "recognizes >=4 of the 5 words printed in the fixture",
verify: (out) => {
const text = out.toString("utf8");
expectRecognizedTerms(text, OCR_ENGLISH_TERMS, 6);
expectRecognizedTerms(text, OCR_ENGLISH_TERMS, 4);
},
},
{
@@ -409,8 +412,13 @@ const CASES: ToolCase[] = [
fixture: `${FIXTURES}/document/valid/ocr-scanned.pdf`,
contentType: "application/pdf",
settings: { quality: "fast", language: "en", enhance: false },
oracle: "returns a PDF carrying text-drawing operators",
verify: (out) => expectSearchablePdf(out),
// The tool's contract is text extraction (it writes <name>_ocr.txt); a
// searchable-PDF output would be a feature, not this oracle (#677).
oracle: "extracts the known scanned text as plain text",
verify: (out) => {
const text = out.toString("utf8");
expectRecognizedTerms(text, OCR_PDF_TERMS, 3);
},
},
{
toolId: "transcribe-audio",