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"); }); });