From 775c71baab707a06bf9a3d9ac0692b95fbefdaed Mon Sep 17 00:00:00 2001 From: ugin <157204743+ugin-man@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:15:37 +0900 Subject: [PATCH] fix: preserve explicit solver failures at the iteration limit --- lib/BaseSolver.ts | 12 +++- tests/explicit-failure-at-limit.test.ts | 76 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 tests/explicit-failure-at-limit.test.ts diff --git a/lib/BaseSolver.ts b/lib/BaseSolver.ts index e071bf8..b4f7da9 100644 --- a/lib/BaseSolver.ts +++ b/lib/BaseSolver.ts @@ -52,10 +52,18 @@ export class BaseSolver { this.failed = true throw e } - if (!this.solved && this.iterations >= this.MAX_ITERATIONS) { + if ( + !this.solved && + !this.failed && + this.iterations >= this.MAX_ITERATIONS + ) { this.tryFinalAcceptance() } - if (!this.solved && this.iterations >= this.MAX_ITERATIONS) { + if ( + !this.solved && + !this.failed && + this.iterations >= this.MAX_ITERATIONS + ) { this.error = `${this.getSolverName()} ran out of iterations` this.failed = true } diff --git a/tests/explicit-failure-at-limit.test.ts b/tests/explicit-failure-at-limit.test.ts new file mode 100644 index 0000000..edd0874 --- /dev/null +++ b/tests/explicit-failure-at-limit.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from "bun:test" +import { BaseSolver } from "../lib/BaseSolver" + +test("an explicit failure at the iteration limit keeps its error and skips acceptance", () => { + class FailedSolver extends BaseSolver { + override MAX_ITERATIONS = 1 + acceptanceCalls = 0 + + override _step() { + this.error = "No feasible route" + this.failed = true + } + + override tryFinalAcceptance() { + this.acceptanceCalls++ + this.solved = true + } + } + const solver = new FailedSolver() + solver.solve() + + expect(solver.failed).toBe(true) + expect(solver.solved).toBe(false) + expect(solver.acceptanceCalls).toBe(0) + expect(solver.error).toBe("No feasible route") +}) + +test("an explicit failure is not replaced by an iteration exhaustion error", () => { + class FailedSolver extends BaseSolver { + override MAX_ITERATIONS = 1 + + override _step() { + this.error = "Invalid geometry" + this.failed = true + } + } + const solver = new FailedSolver() + solver.solve() + + expect(solver.error).toBe("Invalid geometry") + expect(solver.failed).toBe(true) + expect(solver.solved).toBe(false) +}) + +test("a failure reported by final acceptance keeps its specific error", () => { + class RejectedSolver extends BaseSolver { + override MAX_ITERATIONS = 1 + + override tryFinalAcceptance() { + this.error = "Candidate violates clearance" + this.failed = true + } + } + const solver = new RejectedSolver() + solver.solve() + + expect(solver.error).toBe("Candidate violates clearance") + expect(solver.failed).toBe(true) + expect(solver.solved).toBe(false) +}) + +test("an unfinished solver can still accept a solution at the iteration limit", () => { + class AcceptedSolver extends BaseSolver { + override MAX_ITERATIONS = 1 + + override tryFinalAcceptance() { + this.solved = true + } + } + const solver = new AcceptedSolver() + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(solver.error).toBeNull() +})