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 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-30 16:40:03 -04:00 committed by GitHub
parent cbb9c2edd9
commit e7351fbd75
2 changed files with 46 additions and 17 deletions

View file

@ -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) {

View file

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