| import { describe, it, expect } from "vitest"; |
| import { |
| decideAutoFix, |
| type AutoFixMemory, |
| type AutoFixSignals, |
| } from "../src/lib/autofix"; |
|
|
| |
| |
|
|
| const M0: AutoFixMemory = { |
| attempts: 0, |
| active: false, |
| lastVfsHash: null, |
| lastErrorSig: null, |
| }; |
|
|
| function signals(p: Partial<AutoFixSignals>): AutoFixSignals { |
| return { |
| actionableCount: 1, |
| actionableAllMissingAsset: false, |
| booted: false, |
| vfsHash: "h1", |
| errorSig: "e1", |
| maxAttempts: 3, |
| ...p, |
| }; |
| } |
|
|
| describe("decideAutoFix - termination & robustness", () => { |
| it("succeeds when nothing is actionable", () => { |
| const { decision } = decideAutoFix(M0, signals({ actionableCount: 0 })); |
| expect(decision).toEqual({ kind: "success" }); |
| }); |
|
|
| it("retries a fresh, actionable, changing error (incrementing attempts)", () => { |
| const { decision, memory } = decideAutoFix(M0, signals({})); |
| expect(decision).toEqual({ kind: "retry", attempt: 1 }); |
| expect(memory.attempts).toBe(1); |
| expect(memory.active).toBe(true); |
| }); |
|
|
| it("stops (no-progress) when the VFS is unchanged between attempts", () => { |
| |
| const first = decideAutoFix(M0, signals({ vfsHash: "same" })); |
| const second = decideAutoFix( |
| first.memory, |
| signals({ vfsHash: "same", errorSig: "e2" }), |
| ); |
| expect(second.decision).toEqual({ kind: "stop", reason: "no-progress" }); |
| }); |
|
|
| it("stops (no-progress/stuck) when the SAME error-set persists despite edits", () => { |
| const first = decideAutoFix(M0, signals({ vfsHash: "v1", errorSig: "E" })); |
| |
| const second = decideAutoFix( |
| first.memory, |
| signals({ vfsHash: "v2", errorSig: "E" }), |
| ); |
| expect(second.decision).toEqual({ kind: "stop", reason: "no-progress" }); |
| }); |
|
|
| it("respects maxAttempts", () => { |
| const mem: AutoFixMemory = { ...M0, attempts: 3, active: true }; |
| const { decision } = decideAutoFix(mem, signals({ maxAttempts: 3 })); |
| expect(decision).toEqual({ kind: "stop", reason: "max-attempts" }); |
| }); |
|
|
| it("terminates on oscillation A->B->A within maxAttempts", () => { |
| |
| |
| let mem = M0; |
| const seq = ["A", "B", "A", "B", "A", "B"]; |
| let steps = 0; |
| for (let i = 0; i < seq.length; i++) { |
| const { decision, memory } = decideAutoFix( |
| mem, |
| signals({ vfsHash: `v${i}`, errorSig: seq[i], maxAttempts: 3 }), |
| ); |
| mem = memory; |
| steps++; |
| if (decision.kind !== "retry") { |
| expect(decision).toEqual({ kind: "stop", reason: "max-attempts" }); |
| break; |
| } |
| } |
| expect(steps).toBeLessThanOrEqual(4); |
| }); |
|
|
| it("PROPERTY: for any input sequence, the loop terminates within maxAttempts and never throws", () => { |
| |
| let seed = 0x9e3779b9; |
| const rnd = () => { |
| seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; |
| return seed / 0xffffffff; |
| }; |
| for (let trial = 0; trial < 2000; trial++) { |
| const maxAttempts = 1 + Math.floor(rnd() * 5); |
| let mem: AutoFixMemory = { ...M0 }; |
| let calls = 0; |
| let terminated = false; |
| |
| for (let i = 0; i < maxAttempts + 5; i++) { |
| calls++; |
| const s = signals({ |
| actionableCount: rnd() < 0.15 ? 0 : 1 + Math.floor(rnd() * 3), |
| actionableAllMissingAsset: rnd() < 0.3, |
| booted: rnd() < 0.5, |
| vfsHash: rnd() < 0.5 ? "stable" : `v${Math.floor(rnd() * 4)}`, |
| errorSig: `e${Math.floor(rnd() * 3)}`, |
| maxAttempts, |
| }); |
| const { decision, memory } = decideAutoFix(mem, s); |
| mem = memory; |
| expect(mem.attempts).toBeLessThanOrEqual(maxAttempts); |
| if (decision.kind !== "retry") { |
| terminated = true; |
| break; |
| } |
| } |
| |
| expect(terminated).toBe(true); |
| expect(calls).toBeLessThanOrEqual(maxAttempts + 1); |
| } |
| }); |
|
|
| |
| it("booted gate: booted + only missing-asset artifacts -> success (no auto-fix)", () => { |
| const { decision } = decideAutoFix( |
| M0, |
| signals({ |
| actionableCount: 1, |
| actionableAllMissingAsset: true, |
| booted: true, |
| }), |
| ); |
| expect(decision).toEqual({ kind: "success" }); |
| }); |
| }); |
|
|