From e7351fbd754f52a9cab5c18e6415593db4cf316e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 30 Mar 2026 16:40:03 -0400 Subject: [PATCH] fix(auto): move selectAndApplyModel before updateProgressWidget (#3079) Closes #2907 selectAndApplyModel was called after updateProgressWidget and the prompt injection block, so the dashboard showed the previous unit's model label and a second call (if added by a future #2899 fix) would overwrite the first result. Move the single selectAndApplyModel call to before updateProgressWidget so the model is resolved before the widget renders and there is exactly one call per unit dispatch. Adds a structural regression test that asserts selectAndApplyModel appears exactly once in runUnitPhase and before updateProgressWidget. Co-authored-by: Claude Opus 4.6 --- src/resources/extensions/gsd/auto/phases.ts | 34 +++++++++---------- .../extensions/gsd/tests/auto-loop.test.ts | 29 ++++++++++++++++ 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/resources/extensions/gsd/auto/phases.ts b/src/resources/extensions/gsd/auto/phases.ts index 3633c9940..26685796d 100644 --- a/src/resources/extensions/gsd/auto/phases.ts +++ b/src/resources/extensions/gsd/auto/phases.ts @@ -929,6 +929,23 @@ export async function runUnitPhase( }, ); + // Select and apply model (with tier escalation on retry — normal units only) + const modelResult = await deps.selectAndApplyModel( + ctx, + pi, + unitType, + unitId, + s.basePath, + prefs, + s.verbose, + s.autoModeStartModel, + sidecarItem ? undefined : { isRetry, previousTier }, + ); + s.currentUnitRouting = + modelResult.routing as AutoSession["currentUnitRouting"]; + s.currentUnitModel = + modelResult.appliedModel as AutoSession["currentUnitModel"]; + // Status bar + progress widget ctx.ui.setStatus("gsd-auto", "auto"); if (mid) @@ -1001,23 +1018,6 @@ export async function runUnitPhase( logWarning("engine", "Prompt reorder failed", { error: msg }); } - // Select and apply model (with tier escalation on retry — normal units only) - const modelResult = await deps.selectAndApplyModel( - ctx, - pi, - unitType, - unitId, - s.basePath, - prefs, - s.verbose, - s.autoModeStartModel, - sidecarItem ? undefined : { isRetry, previousTier }, - ); - s.currentUnitRouting = - modelResult.routing as AutoSession["currentUnitRouting"]; - s.currentUnitModel = - modelResult.appliedModel as AutoSession["currentUnitModel"]; - // Apply sidecar/pre-dispatch hook model override (takes priority over standard model selection) const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride; if (hookModelOverride) { diff --git a/src/resources/extensions/gsd/tests/auto-loop.test.ts b/src/resources/extensions/gsd/tests/auto-loop.test.ts index c472780cc..3a548f326 100644 --- a/src/resources/extensions/gsd/tests/auto-loop.test.ts +++ b/src/resources/extensions/gsd/tests/auto-loop.test.ts @@ -317,6 +317,35 @@ test("auto/resolve.ts one-shot pattern: _currentResolve is nulled before calling ); }); +test("auto/phases.ts: selectAndApplyModel called exactly once and before updateProgressWidget (#2907)", () => { + const src = readFileSync( + resolve(import.meta.dirname, "..", "auto", "phases.ts"), + "utf-8", + ); + // Extract the runUnitPhase function body + const fnStart = src.indexOf("export async function runUnitPhase"); + assert.ok(fnStart > 0, "runUnitPhase should exist in phases.ts"); + const fnBody = src.slice(fnStart, fnStart + 8000); + + // selectAndApplyModel must appear exactly once + const allOccurrences = [...fnBody.matchAll(/selectAndApplyModel\(/g)]; + assert.equal( + allOccurrences.length, + 1, + `selectAndApplyModel should be called exactly once in runUnitPhase, found ${allOccurrences.length} calls`, + ); + + // selectAndApplyModel must appear BEFORE updateProgressWidget + const modelIdx = fnBody.indexOf("selectAndApplyModel("); + const widgetIdx = fnBody.indexOf("updateProgressWidget("); + assert.ok(modelIdx > 0, "selectAndApplyModel should exist in runUnitPhase"); + assert.ok(widgetIdx > 0, "updateProgressWidget should exist in runUnitPhase"); + assert.ok( + modelIdx < widgetIdx, + "selectAndApplyModel must be called BEFORE updateProgressWidget (#2899/#2907)", + ); +}); + // ─── autoLoop tests (T02) ───────────────────────────────────────────────── /**