fix(guided-flow): route dispatchWorkflow through dynamic routing pipeline (#3153)
Closes #2958 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46dff43e21
commit
45bd2572ac
2 changed files with 149 additions and 18 deletions
|
|
@ -38,7 +38,7 @@ import { showConfirm } from "../shared/tui.js";
|
|||
import { debugLog } from "./debug-logger.js";
|
||||
import { findMilestoneIds, nextMilestoneId, reserveMilestoneId, getReservedMilestoneIds, clearReservedMilestoneIds } from "./milestone-ids.js";
|
||||
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
||||
import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
|
||||
import { selectAndApplyModel } from "./auto-model-selection.js";
|
||||
|
||||
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
||||
export {
|
||||
|
|
@ -224,24 +224,20 @@ async function dispatchWorkflow(
|
|||
ctx?: ExtensionContext,
|
||||
unitType?: string,
|
||||
): Promise<void> {
|
||||
// Apply model preference for this unit type (if configured)
|
||||
// Route through the dynamic routing pipeline (complexity classification,
|
||||
// tier downgrade, fallback chains) — same path as auto-mode dispatches (#2958).
|
||||
if (ctx && unitType) {
|
||||
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
||||
if (modelConfig) {
|
||||
const availableModels = ctx.modelRegistry.getAvailable();
|
||||
const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
|
||||
|
||||
for (const modelId of modelsToTry) {
|
||||
// Resolve model from available models (same logic as auto-model-selection)
|
||||
const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
|
||||
if (!model) continue;
|
||||
|
||||
const ok = await pi.setModel(model, { persist: false });
|
||||
if (ok) {
|
||||
debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
|
||||
break;
|
||||
}
|
||||
}
|
||||
const prefs = loadEffectiveGSDPreferences()?.preferences;
|
||||
const result = await selectAndApplyModel(
|
||||
ctx, pi, unitType, /* unitId */ "", /* basePath */ process.cwd(),
|
||||
prefs, /* verbose */ false, /* autoModeStartModel */ null,
|
||||
);
|
||||
if (result.appliedModel) {
|
||||
debugLog("guided-flow-model-applied", {
|
||||
unitType,
|
||||
model: `${result.appliedModel.provider}/${result.appliedModel.id}`,
|
||||
routing: result.routing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* Guided-flow dynamic routing — regression test for #2958.
|
||||
*
|
||||
* Verifies that dispatchWorkflow() routes through the dynamic routing pipeline
|
||||
* (selectAndApplyModel from auto-model-selection.ts) instead of bypassing it
|
||||
* with a direct call to resolveModelWithFallbacksForUnit.
|
||||
*
|
||||
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const gsdDir = join(__dirname, "..");
|
||||
|
||||
function readSrc(file: string): string {
|
||||
return readFileSync(join(gsdDir, file), "utf-8");
|
||||
}
|
||||
|
||||
const guidedFlowSrc = readSrc("guided-flow.ts");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// #2958: dispatchWorkflow must route through dynamic routing pipeline
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("#2958: guided-flow imports selectAndApplyModel from auto-model-selection", () => {
|
||||
assert.ok(
|
||||
guidedFlowSrc.includes("selectAndApplyModel"),
|
||||
"guided-flow.ts must import and use selectAndApplyModel from auto-model-selection.ts",
|
||||
);
|
||||
});
|
||||
|
||||
test("#2958: dispatchWorkflow does not call resolveModelWithFallbacksForUnit directly", () => {
|
||||
// Extract the dispatchWorkflow function body
|
||||
const fnStart = guidedFlowSrc.indexOf("async function dispatchWorkflow(");
|
||||
assert.ok(fnStart !== -1, "dispatchWorkflow function not found");
|
||||
|
||||
// Find the function body by tracking brace depth
|
||||
const openBrace = guidedFlowSrc.indexOf("{", fnStart);
|
||||
let depth = 1;
|
||||
let pos = openBrace + 1;
|
||||
while (depth > 0 && pos < guidedFlowSrc.length) {
|
||||
if (guidedFlowSrc[pos] === "{") depth++;
|
||||
else if (guidedFlowSrc[pos] === "}") depth--;
|
||||
pos++;
|
||||
}
|
||||
const fnBody = guidedFlowSrc.slice(openBrace, pos);
|
||||
|
||||
assert.ok(
|
||||
!fnBody.includes("resolveModelWithFallbacksForUnit"),
|
||||
"dispatchWorkflow must NOT call resolveModelWithFallbacksForUnit directly — " +
|
||||
"it must route through selectAndApplyModel for dynamic routing support (#2958)",
|
||||
);
|
||||
});
|
||||
|
||||
test("#2958: dispatchWorkflow calls selectAndApplyModel for model selection", () => {
|
||||
// Extract the dispatchWorkflow function body
|
||||
const fnStart = guidedFlowSrc.indexOf("async function dispatchWorkflow(");
|
||||
assert.ok(fnStart !== -1, "dispatchWorkflow function not found");
|
||||
|
||||
const openBrace = guidedFlowSrc.indexOf("{", fnStart);
|
||||
let depth = 1;
|
||||
let pos = openBrace + 1;
|
||||
while (depth > 0 && pos < guidedFlowSrc.length) {
|
||||
if (guidedFlowSrc[pos] === "{") depth++;
|
||||
else if (guidedFlowSrc[pos] === "}") depth--;
|
||||
pos++;
|
||||
}
|
||||
const fnBody = guidedFlowSrc.slice(openBrace, pos);
|
||||
|
||||
assert.ok(
|
||||
fnBody.includes("selectAndApplyModel"),
|
||||
"dispatchWorkflow must call selectAndApplyModel to route through the dynamic routing pipeline (#2958)",
|
||||
);
|
||||
});
|
||||
|
||||
test("#2958: dispatchWorkflow does not use resolveAvailableModel inline", () => {
|
||||
const fnStart = guidedFlowSrc.indexOf("async function dispatchWorkflow(");
|
||||
assert.ok(fnStart !== -1, "dispatchWorkflow function not found");
|
||||
|
||||
const openBrace = guidedFlowSrc.indexOf("{", fnStart);
|
||||
let depth = 1;
|
||||
let pos = openBrace + 1;
|
||||
while (depth > 0 && pos < guidedFlowSrc.length) {
|
||||
if (guidedFlowSrc[pos] === "{") depth++;
|
||||
else if (guidedFlowSrc[pos] === "}") depth--;
|
||||
pos++;
|
||||
}
|
||||
const fnBody = guidedFlowSrc.slice(openBrace, pos);
|
||||
|
||||
assert.ok(
|
||||
!fnBody.includes("resolveAvailableModel"),
|
||||
"dispatchWorkflow must NOT use resolveAvailableModel inline — " +
|
||||
"model resolution is handled by selectAndApplyModel (#2958)",
|
||||
);
|
||||
});
|
||||
|
||||
test("#2958: guided-flow does not import resolveModelWithFallbacksForUnit", () => {
|
||||
// The import should be removed since dispatchWorkflow was the only consumer
|
||||
// Check if resolveModelWithFallbacksForUnit is still used elsewhere in the file
|
||||
const fnStart = guidedFlowSrc.indexOf("async function dispatchWorkflow(");
|
||||
const beforeDispatch = guidedFlowSrc.slice(0, fnStart);
|
||||
const afterFnEnd = (() => {
|
||||
const openBrace = guidedFlowSrc.indexOf("{", fnStart);
|
||||
let depth = 1;
|
||||
let p = openBrace + 1;
|
||||
while (depth > 0 && p < guidedFlowSrc.length) {
|
||||
if (guidedFlowSrc[p] === "{") depth++;
|
||||
else if (guidedFlowSrc[p] === "}") depth--;
|
||||
p++;
|
||||
}
|
||||
return guidedFlowSrc.slice(p);
|
||||
})();
|
||||
|
||||
// If resolveModelWithFallbacksForUnit is not used outside dispatchWorkflow,
|
||||
// the import should be removed
|
||||
const usedOutside = beforeDispatch.includes("resolveModelWithFallbacksForUnit(")
|
||||
|| afterFnEnd.includes("resolveModelWithFallbacksForUnit(");
|
||||
|
||||
if (!usedOutside) {
|
||||
// Verify the import line was cleaned up
|
||||
const importLines = guidedFlowSrc.split("\n").filter(l =>
|
||||
l.includes("import") && l.includes("resolveModelWithFallbacksForUnit"),
|
||||
);
|
||||
assert.equal(
|
||||
importLines.length,
|
||||
0,
|
||||
"resolveModelWithFallbacksForUnit import should be removed when no longer used outside dispatchWorkflow",
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue