mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Nightly has been red since 07-28; per-PR CI and main are green. Two investigations traced all five failing classes to #649: extended-matrix required AI bundles it never installs (removed), nightly SYSTEM_DEPS drifted from ci.yml (added libreoffice + a doc-binaries composite for pandoc/pdfcpu), the generated-case classifier only skipped ffmpeg (widened to pdfcpu/soffice/pandoc + excluded repo-audit specs from the lean docker image), a settings spec capped loginAttemptLimit and 429-cascaded the serial bucket (restore via API), and type-to-search refused keystrokes under a route announcer's programmatic focus (guard added). A nightly dispatch on the branch confirmed all five classes green.
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
featureUnavailableDisposition,
|
|
GeneratedCaseAccounting,
|
|
isEngineUnavailableFailure,
|
|
} from "../../helpers/generated-case-accounting.js";
|
|
|
|
describe("isEngineUnavailableFailure", () => {
|
|
// The lean docker test image skips binary-gated tools by design, so every
|
|
// spawn engine gates the same way ffmpeg does: a missing binary is a skip,
|
|
// not a product failure. A real crash (exit code + stderr) still fails.
|
|
it.each([
|
|
"ffmpeg binary not found (set FFMPEG_PATH or install ffmpeg)",
|
|
"pdfcpu binary not found (set PDFCPU_PATH or install pdfcpu)",
|
|
"soffice binary not found (set SOFFICE_PATH or install LibreOffice)",
|
|
])("treats a missing-binary message as engine-unavailable: %s", (message) => {
|
|
expect(isEngineUnavailableFailure(new Error(message))).toBe(true);
|
|
});
|
|
|
|
it.each(["spawn pandoc ENOENT", "spawn /usr/local/bin/pdfcpu ENOENT", "spawn soffice ENOENT"])(
|
|
"treats a spawn ENOENT for a known engine as engine-unavailable: %s",
|
|
(message) => {
|
|
expect(isEngineUnavailableFailure(new Error(message))).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("keeps a real processing crash a failure", () => {
|
|
expect(
|
|
isEngineUnavailableFailure(new Error("pdfcpu exited with code 1: invalid page range")),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("GeneratedCaseAccounting", () => {
|
|
it("fails a tool that executes no generated cases", () => {
|
|
const accounting = new GeneratedCaseAccounting("resize");
|
|
|
|
expect(() => accounting.assertCovered()).toThrow(
|
|
"resize: generated coverage incomplete (attempted=0, accepted=0, rejected=0, skipped=0)",
|
|
);
|
|
});
|
|
|
|
it("fails a tool whose generated cases are all rejected", () => {
|
|
const accounting = new GeneratedCaseAccounting("resize");
|
|
accounting.attempt();
|
|
accounting.reject();
|
|
|
|
expect(() => accounting.assertCovered()).toThrow(
|
|
"resize: generated coverage incomplete (attempted=1, accepted=0, rejected=1, skipped=0)",
|
|
);
|
|
});
|
|
|
|
it("reports accepted and rejected cases after proving coverage", () => {
|
|
const accounting = new GeneratedCaseAccounting("resize");
|
|
accounting.attempt();
|
|
accounting.accept();
|
|
accounting.attempt();
|
|
accounting.reject();
|
|
|
|
expect(accounting.assertCovered()).toEqual({
|
|
attempted: 2,
|
|
accepted: 1,
|
|
rejected: 1,
|
|
skipped: 0,
|
|
skips: [],
|
|
});
|
|
});
|
|
|
|
it("records bounded machine-readable skip categories and reasons", () => {
|
|
const accounting = new GeneratedCaseAccounting("remove-background");
|
|
accounting.attempt();
|
|
accounting.skip("optional-feature", "background-removal bundle is not installed");
|
|
accounting.attempt();
|
|
accounting.accept();
|
|
|
|
expect(accounting.assertCovered()).toEqual({
|
|
attempted: 2,
|
|
accepted: 1,
|
|
rejected: 0,
|
|
skipped: 1,
|
|
skips: [
|
|
{
|
|
category: "optional-feature",
|
|
reason: "background-removal bundle is not installed",
|
|
count: 1,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("rejects impossible accounting where accepted exceeds attempted", () => {
|
|
const accounting = new GeneratedCaseAccounting("resize");
|
|
accounting.attempt();
|
|
accounting.accept();
|
|
accounting.accept();
|
|
|
|
expect(() => accounting.assertCovered()).toThrow(
|
|
"resize: generated accounting is not conserved (attempted=1, accepted=2, rejected=0, skipped=0)",
|
|
);
|
|
});
|
|
|
|
it("rejects campaigns with no accepted case even when all attempts have outcomes", () => {
|
|
const accounting = new GeneratedCaseAccounting("resize");
|
|
for (let index = 0; index < 99; index += 1) {
|
|
accounting.attempt();
|
|
accounting.reject();
|
|
}
|
|
accounting.attempt();
|
|
accounting.skip("missing-fixture", "one optional fixture was unavailable");
|
|
|
|
expect(() => accounting.assertCovered()).toThrow(
|
|
"resize: generated coverage incomplete (attempted=100, accepted=0, rejected=99, skipped=1)",
|
|
);
|
|
});
|
|
|
|
it("rejects an unbounded or unknown skip reason", () => {
|
|
const accounting = new GeneratedCaseAccounting("resize");
|
|
accounting.attempt();
|
|
|
|
expect(() =>
|
|
accounting.skip("missing-fixture", `fixture unavailable: ${"x".repeat(300)}`),
|
|
).toThrow("resize: generated skip reason must be 1-240 characters");
|
|
});
|
|
});
|
|
|
|
describe("featureUnavailableDisposition", () => {
|
|
it("turns an absent optional AI prerequisite into an explicit skip", () => {
|
|
expect(
|
|
featureUnavailableDisposition({
|
|
toolId: "remove-background",
|
|
statusCode: 501,
|
|
code: "FEATURE_NOT_INSTALLED",
|
|
requireAiFeatures: false,
|
|
}),
|
|
).toBe("skip");
|
|
});
|
|
|
|
it("fails a missing AI feature in an installed-feature campaign", () => {
|
|
expect(() =>
|
|
featureUnavailableDisposition({
|
|
toolId: "remove-background",
|
|
statusCode: 501,
|
|
code: "FEATURE_NOT_INSTALLED",
|
|
requireAiFeatures: true,
|
|
}),
|
|
).toThrow("remove-background: required AI feature returned 501 FEATURE_NOT_INSTALLED");
|
|
});
|
|
|
|
it("does not classify unrelated responses as prerequisite skips", () => {
|
|
expect(
|
|
featureUnavailableDisposition({
|
|
toolId: "resize",
|
|
statusCode: 422,
|
|
code: "PROCESSING_FAILED",
|
|
requireAiFeatures: false,
|
|
}),
|
|
).toBe("continue");
|
|
});
|
|
});
|