176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { minimizeInput } from "../../src/minimize/engine";
|
|
import {
|
|
createPredicate,
|
|
type RegexEvaluator,
|
|
} from "../../src/minimize/predicates";
|
|
import {
|
|
bundlePredicateAdapter,
|
|
minimizeBundle,
|
|
} from "../../src/minimize/bundle";
|
|
|
|
const unusedRegex: RegexEvaluator = async () => ({
|
|
matched: false,
|
|
elapsedMs: 0,
|
|
timedOut: false,
|
|
});
|
|
|
|
describe("minimization engine", () => {
|
|
it("removes XML structure while preserving a literal marker", async () => {
|
|
const input = `<case><noise a="1">discard</noise><payload><message>BOOM</message><also>discard</also></payload></case>`;
|
|
const predicate = createPredicate(
|
|
{ kind: "contains", needle: "BOOM" },
|
|
unusedRegex,
|
|
);
|
|
const result = await minimizeInput(
|
|
input,
|
|
{ structure: "xml", maxTests: 300, maxSeconds: 5 },
|
|
predicate,
|
|
new AbortController().signal,
|
|
);
|
|
expect(result.minimized).toContain("BOOM");
|
|
expect(result.minimized.length).toBeLessThan(input.length);
|
|
expect(
|
|
new DOMParser()
|
|
.parseFromString(result.minimized, "application/xml")
|
|
.querySelector("parsererror"),
|
|
).toBeNull();
|
|
expect(result.steps.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("keeps the original JSON Schema failure signature", async () => {
|
|
const input = JSON.stringify({
|
|
request: { id: 0, label: "noise" },
|
|
unrelated: [1, 2, 3],
|
|
});
|
|
const predicate = createPredicate(
|
|
{
|
|
kind: "json-schema-fails",
|
|
preserveFailureSignature: true,
|
|
schema: JSON.stringify({
|
|
type: "object",
|
|
required: ["request"],
|
|
properties: {
|
|
request: {
|
|
type: "object",
|
|
required: ["id"],
|
|
properties: { id: { type: "integer", minimum: 1 } },
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
unusedRegex,
|
|
);
|
|
const result = await minimizeInput(
|
|
input,
|
|
{ structure: "json", maxTests: 500, maxSeconds: 5 },
|
|
predicate,
|
|
new AbortController().signal,
|
|
);
|
|
expect(JSON.parse(result.minimized)).toEqual({ request: { id: 0 } });
|
|
});
|
|
|
|
it("minimizes malformed syntax and reports budget exhaustion", async () => {
|
|
const predicate = createPredicate({ kind: "invalid-json" }, unusedRegex);
|
|
const result = await minimizeInput(
|
|
'{"large": [1,2,}',
|
|
{ structure: "json", maxTests: 1, maxSeconds: 5 },
|
|
predicate,
|
|
new AbortController().signal,
|
|
);
|
|
expect(result.exhausted).toBe(true);
|
|
expect(result.tests).toBe(1);
|
|
});
|
|
|
|
it("rejects a baseline that does not reproduce and honours cancellation", async () => {
|
|
await expect(
|
|
minimizeInput(
|
|
"safe",
|
|
{ structure: "text", maxTests: 20, maxSeconds: 2 },
|
|
createPredicate({ kind: "contains", needle: "BOOM" }, unusedRegex),
|
|
new AbortController().signal,
|
|
),
|
|
).rejects.toThrow(/does not satisfy/u);
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
await expect(
|
|
minimizeInput(
|
|
"BOOM",
|
|
{ structure: "text", maxTests: 20, maxSeconds: 2 },
|
|
createPredicate({ kind: "contains", needle: "BOOM" }, unusedRegex),
|
|
controller.signal,
|
|
),
|
|
).rejects.toMatchObject({ name: "AbortError" });
|
|
});
|
|
|
|
it("uses isolated regex outcomes and fails closed on unsafe schemas/XSLT", async () => {
|
|
const slow = createPredicate(
|
|
{ kind: "regex-slow", pattern: "a+", flags: "u", thresholdMs: 20 },
|
|
async () => ({ matched: false, elapsedMs: 20, timedOut: false }),
|
|
);
|
|
expect(await slow("aaaa", new AbortController().signal)).toBe(true);
|
|
expect(() =>
|
|
createPredicate(
|
|
{
|
|
kind: "json-schema-fails",
|
|
schema: '{"$ref":"https://invalid/schema.json"}',
|
|
},
|
|
unusedRegex,
|
|
),
|
|
).toThrow(/unsupported.*\$ref/iu);
|
|
expect(() =>
|
|
createPredicate(
|
|
{
|
|
kind: "xslt-throws",
|
|
stylesheet:
|
|
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:include href="https://invalid/x.xsl"/></xsl:stylesheet>',
|
|
},
|
|
unusedRegex,
|
|
),
|
|
).toThrow(/include/u);
|
|
});
|
|
|
|
it("samples a flaky predicate according to an explicit stability policy", async () => {
|
|
const observations = new Map<string, number>();
|
|
const result = await minimizeInput(
|
|
"noise BOOM noise",
|
|
{
|
|
structure: "text",
|
|
maxTests: 300,
|
|
maxSeconds: 5,
|
|
stability: { samples: 3, requiredPasses: 2 },
|
|
},
|
|
async (candidate) => {
|
|
const count = (observations.get(candidate) ?? 0) + 1;
|
|
observations.set(candidate, count);
|
|
return candidate.includes("BOOM") && count % 3 !== 0;
|
|
},
|
|
new AbortController().signal,
|
|
);
|
|
expect(result.minimized).toContain("BOOM");
|
|
expect(result.stability).toEqual({ samples: 3, requiredPasses: 2 });
|
|
expect(result.unstableCandidates).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("removes files and then minimizes retained bundle content", async () => {
|
|
const predicate = bundlePredicateAdapter(
|
|
createPredicate({ kind: "contains", needle: "BOOM" }, unusedRegex),
|
|
"concatenated",
|
|
);
|
|
const result = await minimizeBundle(
|
|
[
|
|
{ path: "noise.txt", content: "discard all of this" },
|
|
{ path: "case/input.txt", content: "prefix BOOM suffix" },
|
|
],
|
|
{ maxTests: 400, maxSeconds: 5 },
|
|
predicate,
|
|
new AbortController().signal,
|
|
);
|
|
expect(result.minimized).toHaveLength(1);
|
|
expect(result.minimized[0]).toMatchObject({ path: "case/input.txt" });
|
|
expect(result.minimized[0]?.content).toContain("BOOM");
|
|
expect(result.steps.some((step) => step.stage === "files")).toBe(true);
|
|
expect(result.steps.some((step) => step.stage === "content")).toBe(true);
|
|
});
|
|
});
|