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) ───────────────────────────────────────────────── /**