Merge scanner readiness work
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { confirmedReviewCorpus, ocrEvalCorpus, seedCorpus, validateConfirmedReviewCorpus } from ".";
|
||||
|
||||
describe("confirmed review corpus", () => {
|
||||
it("contains only human-confirmed review labels", () => {
|
||||
expect(validateConfirmedReviewCorpus()).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not duplicate eval case ids", () => {
|
||||
const ids = ocrEvalCorpus.map((entry) => entry.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it("is included in the full OCR eval corpus", () => {
|
||||
expect(ocrEvalCorpus).toHaveLength(seedCorpus.length + confirmedReviewCorpus.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { OcrEvalCase } from "../ocrEvalHarness";
|
||||
|
||||
export interface ConfirmedReviewEvalCase extends OcrEvalCase {
|
||||
confirmed: true;
|
||||
meta: NonNullable<OcrEvalCase["meta"]> & {
|
||||
source: "review-sample";
|
||||
note: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Human-confirmed review samples belong here after their `expect` values were
|
||||
// checked against the real artifact. Do not paste unconfirmed exporter output
|
||||
// directly from outputs/review-eval-candidates/.
|
||||
export const confirmedReviewCorpus: ConfirmedReviewEvalCase[] = [];
|
||||
@@ -0,0 +1,23 @@
|
||||
import { confirmedReviewCorpus, type ConfirmedReviewEvalCase } from "./confirmedReviewCorpus";
|
||||
import { seedCorpus } from "./seedCorpus";
|
||||
import type { OcrEvalCase } from "../ocrEvalHarness";
|
||||
|
||||
export { confirmedReviewCorpus, seedCorpus };
|
||||
export type { ConfirmedReviewEvalCase };
|
||||
|
||||
export const ocrEvalCorpus: OcrEvalCase[] = [...seedCorpus, ...confirmedReviewCorpus];
|
||||
|
||||
export function validateConfirmedReviewCorpus(corpus: readonly ConfirmedReviewEvalCase[] = confirmedReviewCorpus) {
|
||||
const errors: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of corpus) {
|
||||
if (seen.has(entry.id)) errors.push(`${entry.id}: duplicate id`);
|
||||
seen.add(entry.id);
|
||||
if (entry.confirmed !== true) errors.push(`${entry.id}: confirmed must be true`);
|
||||
if (entry.meta.source !== "review-sample") errors.push(`${entry.id}: meta.source must be review-sample`);
|
||||
if (!entry.meta.note?.trim()) errors.push(`${entry.id}: meta.note must describe the review source/reason`);
|
||||
if (Object.keys(entry.expect).length === 0) errors.push(`${entry.id}: expect must label at least one field`);
|
||||
if (Object.keys(entry.ocr).length === 0) errors.push(`${entry.id}: ocr must contain at least one field`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const { formatSummary, parseWaitSeconds, validatePreflight } = require("../../scripts/live-preflight.cjs") as {
|
||||
formatSummary: (result: unknown) => string;
|
||||
parseWaitSeconds: (value: string) => number;
|
||||
validatePreflight: (input: unknown) => { ok: boolean; errors: string[]; signature: string; isElevated?: boolean; genshinFound?: boolean };
|
||||
};
|
||||
|
||||
function health(signature = "sig-current") {
|
||||
return { ok: true, appBuild: { signature } };
|
||||
}
|
||||
|
||||
function status(runtime = { isElevated: true, genshinFound: true, targetProcess: "GenshinImpact.exe", foregroundProcess: "GenshinImpact.exe" }) {
|
||||
return { ok: true, status: { runtimeInfo: runtime } };
|
||||
}
|
||||
|
||||
describe("live preflight script", () => {
|
||||
it("accepts a matching elevated Genshin runtime", () => {
|
||||
const result = validatePreflight({ health: health(), status: status(), expected: "sig-current" });
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.signature).toBe("sig-current");
|
||||
});
|
||||
|
||||
it("rejects stale runtime signatures", () => {
|
||||
const result = validatePreflight({ health: health("old"), status: status(), expected: "sig-current" });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.errors.join("\n")).toContain("does not match");
|
||||
});
|
||||
|
||||
it("rejects non-elevated or missing Genshin runtime by default", () => {
|
||||
const result = validatePreflight({
|
||||
health: health(),
|
||||
status: status({ isElevated: false, genshinFound: false, targetProcess: "", foregroundProcess: "explorer.exe" }),
|
||||
expected: "sig-current",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.errors).toContain("Runtime is not elevated.");
|
||||
expect(result.errors).toContain("Genshin process/window was not found.");
|
||||
});
|
||||
|
||||
it("formats a concise human summary", () => {
|
||||
const summary = formatSummary(validatePreflight({ health: health(), status: status(), expected: "sig-current" }));
|
||||
expect(summary).toContain("live preflight: PASS");
|
||||
expect(summary).toContain("elevated: yes");
|
||||
expect(summary).toContain("genshin: yes");
|
||||
});
|
||||
|
||||
it("parses optional wait seconds strictly", () => {
|
||||
expect(parseWaitSeconds("")).toBe(0);
|
||||
expect(parseWaitSeconds("120")).toBe(120);
|
||||
expect(() => parseWaitSeconds("-1")).toThrow("--wait");
|
||||
expect(() => parseWaitSeconds("1.5")).toThrow("--wait");
|
||||
expect(() => parseWaitSeconds("soon")).toThrow("--wait");
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatReport, runOcrEval } from "./ocrEvalHarness";
|
||||
import { seedCorpus } from "./corpus/seedCorpus";
|
||||
import { ocrEvalCorpus } from "./corpus";
|
||||
|
||||
// Regression gate: the seed corpus is verified ground truth, so the parser must
|
||||
// read every labeled field correctly. A drop here means an OCR/parser change
|
||||
// regressed a previously-correct read - look at the printed failures. If a
|
||||
// change intentionally alters a correct output, update the corpus label in the
|
||||
// same commit (the label is the source of truth, not the code).
|
||||
// Regression gate: every case in the combined corpus is verified ground truth,
|
||||
// so the parser must read every labeled field correctly. A drop here means an
|
||||
// OCR/parser change regressed a previously-correct read - look at the printed
|
||||
// failures. If a change intentionally alters a correct output, update the corpus
|
||||
// label in the same commit (the label is the source of truth, not the code).
|
||||
|
||||
describe("OCR eval harness", () => {
|
||||
const report = runOcrEval(seedCorpus);
|
||||
const report = runOcrEval(ocrEvalCorpus);
|
||||
|
||||
it("prints the accuracy report", () => {
|
||||
// Surfaced in test output for humans; not an assertion.
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("\n" + formatReport(report) + "\n");
|
||||
expect(report.totalCases).toBe(seedCorpus.length);
|
||||
expect(report.totalCases).toBe(ocrEvalCorpus.length);
|
||||
});
|
||||
|
||||
it("reads every labeled field on the seed corpus correctly", () => {
|
||||
it("reads every labeled field on the eval corpus correctly", () => {
|
||||
const failureSummary = report.failures
|
||||
.map((failure) => {
|
||||
const wrong = failure.fields
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { deflateSync } from "node:zlib";
|
||||
import { pngBufferToBitmap } from "../../electron/services/pngBitmap";
|
||||
import { lockSignalRatio } from "../lib/lockDetection";
|
||||
|
||||
function chunk(type: string, data: Buffer) {
|
||||
const result = Buffer.alloc(12 + data.length);
|
||||
result.writeUInt32BE(data.length, 0);
|
||||
result.write(type, 4, 4, "ascii");
|
||||
data.copy(result, 8);
|
||||
return result;
|
||||
}
|
||||
|
||||
function rgbPng1x1(r: number, g: number, b: number) {
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(1, 0);
|
||||
ihdr.writeUInt32BE(1, 4);
|
||||
ihdr[8] = 8;
|
||||
ihdr[9] = 2;
|
||||
const raw = Buffer.from([0, r, g, b]);
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||
chunk("IHDR", ihdr),
|
||||
chunk("IDAT", deflateSync(raw)),
|
||||
chunk("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
describe("pngBufferToBitmap", () => {
|
||||
it("decodes RGB PNG pixels for lock detection", () => {
|
||||
const bitmap = pngBufferToBitmap(rgbPng1x1(235, 92, 90));
|
||||
expect(bitmap.width).toBe(1);
|
||||
expect(bitmap.height).toBe(1);
|
||||
expect(lockSignalRatio(bitmap)).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
function writeCandidatePayload(dir: string) {
|
||||
const inputPath = path.join(dir, "review-eval-candidates.json");
|
||||
writeFileSync(
|
||||
inputPath,
|
||||
JSON.stringify({
|
||||
summary: {},
|
||||
candidates: [
|
||||
{
|
||||
id: "review-2026-07-08T15-02-39-765Z-152",
|
||||
savedAt: "2026-07-08T15:02:39.765Z",
|
||||
reason: "automatic:parser-notes:p1:r2c3",
|
||||
resolution: "1920x1080",
|
||||
ocr: {
|
||||
"artifact-name": "Pristine Plume of the Blessed",
|
||||
"artifact-slot": "Plume of Death",
|
||||
"artifact-main-stat-label": "ATK",
|
||||
"artifact-level": "+20",
|
||||
"artifact-substats": "- Energy Recharge+10.4%",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
describe("prepare confirmed review case script", () => {
|
||||
it("creates a confirmed corpus snippet from a candidate and explicit labels", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-confirmed-review-"));
|
||||
try {
|
||||
const inputPath = writeCandidatePayload(dir);
|
||||
const expectPath = path.join(dir, "expect.json");
|
||||
const outputPath = path.join(dir, "snippet.ts");
|
||||
writeFileSync(
|
||||
expectPath,
|
||||
`\uFEFF${JSON.stringify({
|
||||
name: "Pristine Plume of the Blessed",
|
||||
slot: "Plume of Death",
|
||||
level: 20,
|
||||
mainStat: "ATK",
|
||||
setName: "Silken Moon's Serenade",
|
||||
})}`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
execFileSync(
|
||||
"node",
|
||||
[
|
||||
"scripts/prepare-confirmed-review-case.cjs",
|
||||
`--input=${inputPath}`,
|
||||
"--candidate=review-2026-07-08T15-02-39-765Z-152",
|
||||
`--expect-file=${expectPath}`,
|
||||
`--out=${outputPath}`,
|
||||
],
|
||||
{ cwd: process.cwd(), stdio: "pipe" },
|
||||
);
|
||||
|
||||
const snippet = readFileSync(outputPath, "utf8");
|
||||
expect(snippet).toContain("confirmed: true");
|
||||
expect(snippet).toContain("Pristine Plume of the Blessed");
|
||||
expect(snippet).toContain('"artifact-name": "Pristine Plume of the Blessed"');
|
||||
expect(snippet).toContain("sourceCandidate=review-2026-07-08T15-02-39-765Z-152");
|
||||
expect(snippet).not.toContain("confirmed: false");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects missing explicit labels", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-confirmed-review-"));
|
||||
try {
|
||||
const inputPath = writeCandidatePayload(dir);
|
||||
expect(() =>
|
||||
execFileSync(
|
||||
"node",
|
||||
["scripts/prepare-confirmed-review-case.cjs", `--input=${inputPath}`, "--candidate=review-2026-07-08T15-02-39-765Z-152"],
|
||||
{ cwd: process.cwd(), stdio: "pipe" },
|
||||
),
|
||||
).toThrow();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
function reviewRecord(savedAt: string, reason: string, ocr: Array<{ id: string; text: string }>, locked?: boolean) {
|
||||
return {
|
||||
savedAt,
|
||||
sample: {
|
||||
reason,
|
||||
capture: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
ocr,
|
||||
locked,
|
||||
},
|
||||
parsed: {
|
||||
name: "Pristine Plume of the Blessed",
|
||||
slot: ocr.some((entry) => entry.id === "artifact-slot") ? "Plume of Death" : "Unknown Slot",
|
||||
level: 20,
|
||||
mainStat: "ATK",
|
||||
mainValue: "311",
|
||||
setName: "Silken Moon's Serenade",
|
||||
equipped: ocr.some((entry) => entry.id === "artifact-footer") ? "Aino" : "Not detected",
|
||||
substats: ["Energy Recharge+10.4%"],
|
||||
confidence: 0.9,
|
||||
notes: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const completeOcr = [
|
||||
{ id: "artifact-name", text: "Pristine Plume of the Blessed" },
|
||||
{ id: "artifact-slot", text: "Plume of Death" },
|
||||
{ id: "artifact-main-stat-label", text: "ATK" },
|
||||
{ id: "artifact-level", text: "+20" },
|
||||
{ id: "artifact-substats", text: "- Energy Recharge+10.4%" },
|
||||
{ id: "artifact-footer", text: "Equipped: Aino" },
|
||||
];
|
||||
|
||||
describe("review eval candidate exporter", () => {
|
||||
it("exports deduplicated candidates with stats and stale markers", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-review-candidates-"));
|
||||
try {
|
||||
const inputPath = path.join(dir, "review-samples.jsonl");
|
||||
const outDir = path.join(dir, "out");
|
||||
const staleOcr = completeOcr.filter((entry) => entry.id !== "artifact-slot");
|
||||
writeFileSync(
|
||||
inputPath,
|
||||
[
|
||||
JSON.stringify(reviewRecord("2026-07-08T15:00:00.000Z", "automatic:missing-crops-or-ocr:p1:r0c0", completeOcr)),
|
||||
JSON.stringify(reviewRecord("2026-07-08T15:00:00.000Z", "automatic:missing-crops-or-ocr:p1:r0c0", completeOcr)),
|
||||
JSON.stringify(reviewRecord("2026-07-08T15:01:00.000Z", "automatic:capture-rejected:p1:r0c1", staleOcr, true)),
|
||||
"{not json",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
execFileSync("node", ["scripts/export-review-eval-candidates.cjs", `--input=${inputPath}`, `--out=${outDir}`, "--limit=10"], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
const payload = JSON.parse(readFileSync(path.join(outDir, "review-eval-candidates.json"), "utf8"));
|
||||
const markdown = readFileSync(path.join(outDir, "review-eval-candidates.md"), "utf8");
|
||||
|
||||
expect(payload.summary.recordsRead).toBe(4);
|
||||
expect(payload.summary.invalidRecords).toBe(1);
|
||||
expect(payload.summary.uniqueCandidates).toBe(2);
|
||||
expect(payload.summary.exportedCandidates).toBe(2);
|
||||
expect(payload.summary.exportStats.completeFastFields).toBe(1);
|
||||
expect(payload.summary.exportStats.likelyStaleCaptures).toBe(1);
|
||||
expect(payload.summary.exportStats.equippedFooterCandidates).toBe(2);
|
||||
expect(payload.summary.exportStats.lockedTrueCandidates).toBe(1);
|
||||
expect(payload.candidates[0].missingFastFields).toEqual([]);
|
||||
expect(payload.candidates[0].parsed.equipped).toBe("Aino");
|
||||
expect(payload.candidates[0].reviewPrompt.expectedFields.equipped).toBe("Aino");
|
||||
expect(payload.candidates[1].likelyStaleCapture).toBe(true);
|
||||
expect(payload.candidates[1].locked).toBe(true);
|
||||
expect(markdown).toContain("## Export stats");
|
||||
expect(markdown).toContain("- artifact-slot: 1");
|
||||
expect(markdown).toContain("equipped=Aino");
|
||||
expect(markdown).toContain("Locked=true candidates: 1");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import type { OcrEvalCase } from "./ocrEvalHarness";
|
||||
// itself). The intended flow is:
|
||||
// 1. reviewSampleToEvalCase() extracts the OCR + the parser's current guess.
|
||||
// 2. A human confirms or corrects `expect` in the produced case.
|
||||
// 3. The corrected case is committed into src/eval/corpus/.
|
||||
// 3. The corrected case is committed into src/eval/corpus/confirmedReviewCorpus.ts.
|
||||
// The `confirmed` flag records whether step 2 happened.
|
||||
|
||||
export interface ReviewSampleEvalCase extends OcrEvalCase {
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
function validAssessment() {
|
||||
return {
|
||||
createdAt: "2026-07-08T12:00:00.000Z",
|
||||
goal100Decision: "qualified-comparison: winner=ik-traineddata",
|
||||
goal100: {
|
||||
limit: 100,
|
||||
comparisonComplete: true,
|
||||
winnerEngine: "ik-traineddata",
|
||||
winnerQualified: true,
|
||||
winnerActiveAverageMsPerParsed: 820,
|
||||
winnerActiveProjectedMsFor100: 82000,
|
||||
winnerMissRate: 0,
|
||||
winnerReviewRate: 0.04,
|
||||
engines: [
|
||||
{ engine: "ik-traineddata", qualified: true, missRate: 0, reviewRate: 0.04 },
|
||||
{ engine: "current", qualified: true, missRate: 0, reviewRate: 0.06 },
|
||||
],
|
||||
},
|
||||
limits: [
|
||||
{
|
||||
limit: 20,
|
||||
comparisonComplete: true,
|
||||
winnerEngine: "current",
|
||||
winnerQualified: true,
|
||||
winnerActiveAverageMsPerParsed: 390,
|
||||
winnerActiveProjectedMsFor100: 39000,
|
||||
winnerMissRate: 0,
|
||||
winnerReviewRate: 0.05,
|
||||
engines: [
|
||||
{ engine: "current", qualified: true, missRate: 0, reviewRate: 0.05 },
|
||||
{ engine: "ik-traineddata", qualified: true, missRate: 0, reviewRate: 0.1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function writeAssessment(dir: string, payload: unknown) {
|
||||
const inputPath = path.join(dir, "scan-performance-assessment.json");
|
||||
writeFileSync(inputPath, JSON.stringify(payload), "utf8");
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
describe("scan assessment validator", () => {
|
||||
it("accepts a qualified complete 100-artifact comparison", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
const output = execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(JSON.parse(output).ok).toBe(true);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a matching expected winner", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
const output = execFileSync(
|
||||
"node",
|
||||
["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--expect-winner=ik-traineddata"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
expect(JSON.parse(output).ok).toBe(true);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prints an opt-in human summary", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
const output = execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(output).toContain("scan assessment: PASS");
|
||||
expect(output).toContain(`input: ${inputPath}`);
|
||||
expect(output).toContain("createdAt: 2026-07-08T12:00:00.000Z");
|
||||
expect(output).toContain("limit: 100");
|
||||
expect(output).toContain("winner: ik-traineddata");
|
||||
expect(output).toContain("activeAvg: 820ms/artifact");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts a qualified complete 20-artifact comparison", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
const output = execFileSync(
|
||||
"node",
|
||||
["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary", "--limit=20"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
expect(output).toContain("scan assessment: PASS");
|
||||
expect(output).toContain("limit: 20");
|
||||
expect(output).toContain("winner: current");
|
||||
expect(output).toContain("activeAvg: 390ms/artifact");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects invalid requested limits", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
let stdout = "";
|
||||
try {
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary", "--limit=0"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
stdout = String((error as { stdout?: string }).stdout || "");
|
||||
}
|
||||
expect(stdout).toContain("scan assessment: FAIL");
|
||||
expect(stdout).toContain("--limit must be a positive integer");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a requested limit that is missing from the assessment", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
let stdout = "";
|
||||
try {
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary", "--limit=45"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
stdout = String((error as { stdout?: string }).stdout || "");
|
||||
}
|
||||
expect(stdout).toContain("scan assessment: FAIL");
|
||||
expect(stdout).toContain("Missing limit=45 assessment");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the validated npm scripts wired through preflight and the correct assessment limit", () => {
|
||||
const packageJson = JSON.parse(readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
|
||||
expect(packageJson.scripts["scan:goal:compare:validated"]).toBe(
|
||||
"npm run scan:live:preflight && npm run scan:goal:compare && npm run scan:assessment:validate -- --latest --summary",
|
||||
);
|
||||
expect(packageJson.scripts["scan:goal:compare:validated:wait"]).toBe(
|
||||
"npm run scan:live:preflight:wait && npm run scan:goal:compare && npm run scan:assessment:validate -- --latest --summary",
|
||||
);
|
||||
expect(packageJson.scripts["scan:iterate:compare"]).toBe(
|
||||
"powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\live-soak.ps1 -Limits 20 -ScanEngine compare -BenchmarkOcr",
|
||||
);
|
||||
expect(packageJson.scripts["scan:iterate:compare:validated"]).toBe(
|
||||
"npm run scan:live:preflight && npm run scan:iterate:compare && npm run scan:assessment:validate -- --latest --summary --limit=20",
|
||||
);
|
||||
expect(packageJson.scripts["scan:iterate:compare:validated:wait"]).toBe(
|
||||
"npm run scan:live:preflight:wait && npm run scan:iterate:compare && npm run scan:assessment:validate -- --latest --summary --limit=20",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a mismatched expected winner", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
expect(() =>
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--expect-winner=current"], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "pipe",
|
||||
}),
|
||||
).toThrow();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prints errors in summary mode for rejected assessments", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const inputPath = writeAssessment(dir, validAssessment());
|
||||
let stdout = "";
|
||||
try {
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--expect-winner=current", "--summary"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
stdout = String((error as { stdout?: string }).stdout || "");
|
||||
}
|
||||
expect(stdout).toContain("scan assessment: FAIL");
|
||||
expect(stdout).toContain("Expected winner");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a single-engine 100-artifact run", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const payload = validAssessment();
|
||||
payload.goal100Decision = "not-comparable: current and ik-traineddata were not both run";
|
||||
payload.goal100.comparisonComplete = false;
|
||||
payload.goal100.engines = [{ engine: "current", qualified: true, missRate: 0, reviewRate: 0 }];
|
||||
const inputPath = writeAssessment(dir, payload);
|
||||
expect(() =>
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "pipe",
|
||||
}),
|
||||
).toThrow();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an unqualified winner", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const payload = validAssessment();
|
||||
payload.goal100Decision = "not-qualified: 100-artifact winner failed quality gates";
|
||||
payload.goal100.winnerQualified = false;
|
||||
payload.goal100.engines[0].qualified = false;
|
||||
payload.goal100.engines[0].reviewRate = 0.3;
|
||||
const inputPath = writeAssessment(dir, payload);
|
||||
expect(() =>
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "pipe",
|
||||
}),
|
||||
).toThrow();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a winner with missing quality rates", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const payload = validAssessment();
|
||||
delete (payload.goal100.engines[0] as { missRate?: number }).missRate;
|
||||
delete (payload.goal100.engines[0] as { reviewRate?: number }).reviewRate;
|
||||
const inputPath = writeAssessment(dir, payload);
|
||||
let stdout = "";
|
||||
try {
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
stdout = String((error as { stdout?: string }).stdout || "");
|
||||
}
|
||||
expect(stdout).toContain("scan assessment: FAIL");
|
||||
expect(stdout).toContain("Winner missRate must be a finite number");
|
||||
expect(stdout).toContain("Winner reviewRate must be a finite number");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a winner with missing summary quality rates", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const payload = validAssessment();
|
||||
delete (payload.goal100 as { winnerMissRate?: number }).winnerMissRate;
|
||||
delete (payload.goal100 as { winnerReviewRate?: number }).winnerReviewRate;
|
||||
const inputPath = writeAssessment(dir, payload);
|
||||
let stdout = "";
|
||||
try {
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
stdout = String((error as { stdout?: string }).stdout || "");
|
||||
}
|
||||
expect(stdout).toContain("scan assessment: FAIL");
|
||||
expect(stdout).toContain("Winner summary missRate must be a finite number");
|
||||
expect(stdout).toContain("Winner summary reviewRate must be a finite number");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a winner with missing speed timing", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const payload = validAssessment();
|
||||
delete (payload.goal100 as { winnerActiveAverageMsPerParsed?: number }).winnerActiveAverageMsPerParsed;
|
||||
delete (payload.goal100 as { winnerActiveProjectedMsFor100?: number }).winnerActiveProjectedMsFor100;
|
||||
const inputPath = writeAssessment(dir, payload);
|
||||
let stdout = "";
|
||||
try {
|
||||
execFileSync("node", ["scripts/validate-scan-assessment.cjs", `--input=${inputPath}`, "--summary"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch (error) {
|
||||
stdout = String((error as { stdout?: string }).stdout || "");
|
||||
}
|
||||
expect(stdout).toContain("scan assessment: FAIL");
|
||||
expect(stdout).toContain("Winner active average timing must be a positive finite number");
|
||||
expect(stdout).toContain("Winner projected100 timing must be a positive finite number");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("validates the latest assessment under a live-soak root", () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "gaa-assessment-"));
|
||||
try {
|
||||
const older = path.join(dir, "2026-07-08T10-00-00");
|
||||
const newer = path.join(dir, "2026-07-08T11-00-00");
|
||||
mkdirSync(older);
|
||||
mkdirSync(newer);
|
||||
writeAssessment(older, { goal100Decision: "not-run: missing 100-artifact assessment" });
|
||||
writeAssessment(newer, validAssessment());
|
||||
|
||||
const output = execFileSync("node", ["scripts/validate-scan-assessment.cjs", "--latest", `--root=${dir}`], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
});
|
||||
const parsed = JSON.parse(output);
|
||||
expect(parsed.ok).toBe(true);
|
||||
expect(parsed.inputPath).toContain("2026-07-08T11-00-00");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user