From 84543ed574cf452a8dc27db895304986e0dda0bc Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:02:26 +1000 Subject: [PATCH] fix(engine): floor percentComplete at 0 so it stays in its documented 0-100 range (#6773) buildProgressSnapshot clamped only the upper bound (Math.min(100, ...)). LoopProgressState.iteration is an unvalidated caller-supplied number, so a negative iteration (an upstream bookkeeping bug) produced a negative percentComplete, contradicting the field's documented 0-100 range. Clamp both ends with Math.max(0, Math.min(100, ...)) and add a regression test asserting a negative iteration floors to 0. Closes #6773 --- packages/loopover-engine/src/loop-progress.ts | 6 +++++- test/unit/loop-progress.test.ts | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/loopover-engine/src/loop-progress.ts b/packages/loopover-engine/src/loop-progress.ts index cc557c0175..73e9fada3d 100644 --- a/packages/loopover-engine/src/loop-progress.ts +++ b/packages/loopover-engine/src/loop-progress.ts @@ -40,8 +40,12 @@ export type ProgressSnapshot = { /** Build a customer-facing progress snapshot from already-computed loop state (#4800). Pure. */ export function buildProgressSnapshot(state: LoopProgressState): ProgressSnapshot { const maxIterations = state.maxIterations ?? null; + // Clamp BOTH ends (#6773): `iteration` is an unvalidated caller-supplied number, so a negative one (an + // upstream bookkeeping bug) would otherwise produce a negative percent, contradicting the documented 0-100. const percentComplete = - maxIterations !== null && maxIterations > 0 ? Math.min(100, Math.round((state.iteration / maxIterations) * 100)) : null; + maxIterations !== null && maxIterations > 0 + ? Math.max(0, Math.min(100, Math.round((state.iteration / maxIterations) * 100))) + : null; return { phase: state.phase, status: state.status, diff --git a/test/unit/loop-progress.test.ts b/test/unit/loop-progress.test.ts index bffa76f003..b4e67244c9 100644 --- a/test/unit/loop-progress.test.ts +++ b/test/unit/loop-progress.test.ts @@ -28,6 +28,13 @@ describe("buildProgressSnapshot (#4800)", () => { expect(buildProgressSnapshot(running({ iteration: 7, maxIterations: 5 })).percentComplete).toBe(100); }); + it("REGRESSION (#6773): floors percent-complete at 0 for a negative iteration, never below the documented range", () => { + // `iteration` is an unvalidated caller-supplied number: an upstream bookkeeping bug producing a negative + // one must not surface as a negative percent (the upper bound was clamped, the lower one was not). + expect(buildProgressSnapshot(running({ iteration: -1, maxIterations: 5 })).percentComplete).toBe(0); + expect(buildProgressSnapshot(running({ iteration: -100, maxIterations: 5 })).percentComplete).toBe(0); + }); + it("defaults recent activity to empty and caps the tail at MAX_PROGRESS_ACTIVITY", () => { expect(buildProgressSnapshot(running()).recentActivity).toEqual([]); // omitted const many = Array.from({ length: MAX_PROGRESS_ACTIVITY + 4 }, (_, i) => ({ step: `s${i}` }));