refactor: major codebase cleanup - BDD comments, file splitting, bug fixes (#1350)
* style(tests): normalize BDD comments from '// #given' to '// given'
- Replace 4,668 Python-style BDD comments across 107 test files
- Patterns changed: // #given -> // given, // #when -> // when, // #then -> // then
- Also handles no-space variants: //#given -> // given
* fix(rules-injector): prefer output.metadata.filePath over output.title
- Extract file path resolution to dedicated output-path.ts module
- Prefer metadata.filePath which contains actual file path
- Fall back to output.title only when metadata unavailable
- Fixes issue where rules weren't injected when tool output title was a label
* feat(slashcommand): add optional user_message parameter
- Add user_message optional parameter for command arguments
- Model can now call: command='publish' user_message='patch'
- Improves error messages with clearer format guidance
- Helps LLMs understand correct parameter usage
* feat(hooks): restore compaction-context-injector hook
- Restore hook deleted in cbbc7bd0 for session compaction context
- Injects 7 mandatory sections: User Requests, Final Goal, Work Completed,
Remaining Tasks, Active Working Context, MUST NOT Do, Agent Verification State
- Re-register in hooks/index.ts and main plugin entry
* refactor(background-agent): split manager.ts into focused modules
- Extract constants.ts for TTL values and internal types (52 lines)
- Extract state.ts for TaskStateManager class (204 lines)
- Extract spawner.ts for task creation logic (244 lines)
- Extract result-handler.ts for completion handling (265 lines)
- Reduce manager.ts from 1377 to 755 lines (45% reduction)
- Maintain backward compatible exports
* refactor(agents): split prometheus-prompt.ts into subdirectory
- Move 1196-line prometheus-prompt.ts to prometheus/ subdirectory
- Organize prompt sections into separate files for maintainability
- Update agents/index.ts exports
* refactor(delegate-task): split tools.ts into focused modules
- Extract categories.ts for category definitions and routing
- Extract executor.ts for task execution logic
- Extract helpers.ts for utility functions
- Extract prompt-builder.ts for prompt construction
- Reduce tools.ts complexity with cleaner separation of concerns
* refactor(builtin-skills): split skills.ts into individual skill files
- Move each skill to dedicated file in skills/ subdirectory
- Create barrel export for backward compatibility
- Improve maintainability with focused skill modules
* chore: update import paths and lockfile
- Update prometheus import path after refactor
- Update bun.lock
* fix(tests): complete BDD comment normalization
- Fix remaining #when/#then patterns missed by initial sed
- Affected: state.test.ts, events.test.ts
---------
Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
+138
-138
@@ -6,97 +6,97 @@ import * as connectedProvidersCache from "./connected-providers-cache"
|
||||
describe("resolveModel", () => {
|
||||
describe("priority chain", () => {
|
||||
test("returns userModel when all three are set", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
systemDefault: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModel(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("returns inheritedModel when userModel is undefined", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: undefined,
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
systemDefault: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModel(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
})
|
||||
|
||||
test("returns systemDefault when both userModel and inheritedModel are undefined", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: undefined,
|
||||
inheritedModel: undefined,
|
||||
systemDefault: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModel(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBe("google/gemini-3-pro")
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty string handling", () => {
|
||||
test("treats empty string as unset, uses fallback", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
systemDefault: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModel(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBe("openai/gpt-5.2")
|
||||
})
|
||||
|
||||
test("treats whitespace-only string as unset, uses fallback", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: " ",
|
||||
inheritedModel: "",
|
||||
systemDefault: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModel(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBe("google/gemini-3-pro")
|
||||
})
|
||||
})
|
||||
|
||||
describe("purity", () => {
|
||||
test("same input returns same output (referential transparency)", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
inheritedModel: "openai/gpt-5.2",
|
||||
systemDefault: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result1 = resolveModel(input)
|
||||
const result2 = resolveModel(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
})
|
||||
@@ -115,7 +115,7 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
describe("Step 1: UI Selection (highest priority)", () => {
|
||||
test("returns uiSelectedModel with override source when provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
uiSelectedModel: "opencode/glm-4.7-free",
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
@@ -126,17 +126,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("opencode/glm-4.7-free")
|
||||
expect(result!.source).toBe("override")
|
||||
expect(logSpy).toHaveBeenCalledWith("Model resolved via UI selection", { model: "opencode/glm-4.7-free" })
|
||||
})
|
||||
|
||||
test("UI selection takes priority over config override", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
uiSelectedModel: "opencode/glm-4.7-free",
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
@@ -144,16 +144,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("opencode/glm-4.7-free")
|
||||
expect(result!.source).toBe("override")
|
||||
})
|
||||
|
||||
test("whitespace-only uiSelectedModel is treated as not provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
uiSelectedModel: " ",
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
@@ -161,16 +161,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-5" })
|
||||
})
|
||||
|
||||
test("empty string uiSelectedModel falls through to config override", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
uiSelectedModel: "",
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
@@ -178,17 +178,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Step 2: Config Override", () => {
|
||||
test("returns userModel with override source when userModel is provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
fallbackChain: [
|
||||
@@ -198,17 +198,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("override")
|
||||
expect(logSpy).toHaveBeenCalledWith("Model resolved via config override", { model: "anthropic/claude-opus-4-5" })
|
||||
})
|
||||
|
||||
test("override takes priority even if model not in availableModels", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: "custom/my-model",
|
||||
fallbackChain: [
|
||||
@@ -218,16 +218,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("custom/my-model")
|
||||
expect(result!.source).toBe("override")
|
||||
})
|
||||
|
||||
test("whitespace-only userModel is treated as not provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: " ",
|
||||
fallbackChain: [
|
||||
@@ -237,15 +237,15 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.source).not.toBe("override")
|
||||
})
|
||||
|
||||
test("empty string userModel is treated as not provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: "",
|
||||
fallbackChain: [
|
||||
@@ -255,17 +255,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.source).not.toBe("override")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Step 3: Provider fallback chain", () => {
|
||||
test("tries providers in order within entry and returns first match", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" },
|
||||
@@ -274,10 +274,10 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("github-copilot/claude-opus-4-5-preview")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
expect(logSpy).toHaveBeenCalledWith("Model resolved via fallback chain (availability confirmed)", {
|
||||
@@ -289,7 +289,7 @@ describe("resolveModelWithFallback", () => {
|
||||
})
|
||||
|
||||
test("respects provider priority order within entry", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.2" },
|
||||
@@ -298,16 +298,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("tries next provider when first provider has no match", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "opencode"], model: "gpt-5-nano" },
|
||||
@@ -316,16 +316,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("opencode/gpt-5-nano")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("uses fuzzy matching within provider", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot"], model: "claude-opus" },
|
||||
@@ -334,45 +334,45 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("skips fallback chain when not provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["anthropic/claude-opus-4-5"]),
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.source).toBe("system-default")
|
||||
})
|
||||
|
||||
test("skips fallback chain when empty", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [],
|
||||
availableModels: new Set(["anthropic/claude-opus-4-5"]),
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.source).toBe("system-default")
|
||||
})
|
||||
|
||||
test("case-insensitive fuzzy matching", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "CLAUDE-OPUS" },
|
||||
@@ -381,16 +381,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("cross-provider fuzzy match when preferred provider unavailable (librarian scenario)", () => {
|
||||
// #given - glm-4.7 is defined for zai-coding-plan, but only opencode has it
|
||||
// given - glm-4.7 is defined for zai-coding-plan, but only opencode has it
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
@@ -400,10 +400,10 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should find glm-4.7 from opencode via cross-provider fuzzy match
|
||||
// then - should find glm-4.7 from opencode via cross-provider fuzzy match
|
||||
expect(result!.model).toBe("opencode/glm-4.7")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
expect(logSpy).toHaveBeenCalledWith("Model resolved via fallback chain (cross-provider fuzzy match)", {
|
||||
@@ -414,7 +414,7 @@ describe("resolveModelWithFallback", () => {
|
||||
})
|
||||
|
||||
test("prefers specified provider over cross-provider match", () => {
|
||||
// #given - both zai-coding-plan and opencode have glm-4.7
|
||||
// given - both zai-coding-plan and opencode have glm-4.7
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
@@ -423,16 +423,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should prefer zai-coding-plan (specified provider) over opencode
|
||||
// then - should prefer zai-coding-plan (specified provider) over opencode
|
||||
expect(result!.model).toBe("zai-coding-plan/glm-4.7")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("cross-provider match preserves variant from entry", () => {
|
||||
// #given - entry has variant, model found via cross-provider
|
||||
// given - entry has variant, model found via cross-provider
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7", variant: "high" },
|
||||
@@ -441,16 +441,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - variant should be preserved
|
||||
// then - variant should be preserved
|
||||
expect(result!.model).toBe("opencode/glm-4.7")
|
||||
expect(result!.variant).toBe("high")
|
||||
})
|
||||
|
||||
test("cross-provider match tries next entry if no match found anywhere", () => {
|
||||
// #given - first entry model not available anywhere, second entry available
|
||||
// given - first entry model not available anywhere, second entry available
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["zai-coding-plan"], model: "nonexistent-model" },
|
||||
@@ -460,10 +460,10 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should fall through to second entry
|
||||
// then - should fall through to second entry
|
||||
expect(result!.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
@@ -471,7 +471,7 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
describe("Step 4: System default fallback (no availability match)", () => {
|
||||
test("returns system default when no availability match found in fallback chain", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "nonexistent-model" },
|
||||
@@ -480,17 +480,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("system-default")
|
||||
expect(logSpy).toHaveBeenCalledWith("No available model found in fallback chain, falling through to system default")
|
||||
})
|
||||
|
||||
test("returns undefined when availableModels empty and no connected providers cache exists", () => {
|
||||
// #given - both model cache and connected-providers cache are missing (first run)
|
||||
// given - both model cache and connected-providers cache are missing (first run)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
@@ -500,16 +500,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: undefined, // no system default configured
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should return undefined to let OpenCode use Provider.defaultModel()
|
||||
// then - should return undefined to let OpenCode use Provider.defaultModel()
|
||||
expect(result).toBeUndefined()
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("uses connected provider from fallback when availableModels empty but cache exists", () => {
|
||||
// #given - model cache missing but connected-providers cache exists
|
||||
// given - model cache missing but connected-providers cache exists
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai", "google"])
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
@@ -519,17 +519,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should use connected provider (openai) from fallback chain
|
||||
// then - should use connected provider (openai) from fallback chain
|
||||
expect(result!.model).toBe("openai/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("uses github-copilot when google not connected (visual-engineering scenario)", () => {
|
||||
// #given - user has github-copilot but not google connected
|
||||
// given - user has github-copilot but not google connected
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["github-copilot"])
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
@@ -539,17 +539,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should use github-copilot (second provider) since google not connected
|
||||
// then - should use github-copilot (second provider) since google not connected
|
||||
expect(result!.model).toBe("github-copilot/gemini-3-pro")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("falls through to system default when no provider in fallback is connected", () => {
|
||||
// #given - user only has quotio connected, but fallback chain has anthropic/opencode
|
||||
// given - user only has quotio connected, but fallback chain has anthropic/opencode
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["quotio"])
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
@@ -559,17 +559,17 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "quotio/claude-opus-4-5-20251101",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - no provider in fallback is connected, fall through to system default
|
||||
// then - no provider in fallback is connected, fall through to system default
|
||||
expect(result!.model).toBe("quotio/claude-opus-4-5-20251101")
|
||||
expect(result!.source).toBe("system-default")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("falls through to system default when no cache and systemDefaultModel is provided", () => {
|
||||
// #given - no cache but system default is configured
|
||||
// given - no cache but system default is configured
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
@@ -579,26 +579,26 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should fall through to system default
|
||||
// then - should fall through to system default
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("system-default")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("returns system default when fallbackChain is not provided", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("system-default")
|
||||
})
|
||||
@@ -606,10 +606,10 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
describe("Multi-entry fallbackChain", () => {
|
||||
test("resolves to claude-opus when OpenAI unavailable but Anthropic available (oracle scenario)", () => {
|
||||
// #given
|
||||
// given
|
||||
const availableModels = new Set(["anthropic/claude-opus-4-5"])
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
@@ -619,16 +619,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "system/default",
|
||||
})
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("tries all providers in first entry before moving to second entry", () => {
|
||||
// #given
|
||||
// given
|
||||
const availableModels = new Set(["google/gemini-3-pro"])
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic"], model: "gpt-5.2" },
|
||||
@@ -638,19 +638,19 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "system/default",
|
||||
})
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("returns first matching entry even if later entries have better matches", () => {
|
||||
// #given
|
||||
// given
|
||||
const availableModels = new Set([
|
||||
"openai/gpt-5.2",
|
||||
"anthropic/claude-opus-4-5",
|
||||
])
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
@@ -660,16 +660,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "system/default",
|
||||
})
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("openai/gpt-5.2")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("falls through to system default when none match availability", () => {
|
||||
// #given
|
||||
// given
|
||||
const availableModels = new Set(["other/model"])
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai"], model: "gpt-5.2" },
|
||||
@@ -680,7 +680,7 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "system/default",
|
||||
})
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result!.model).toBe("system/default")
|
||||
expect(result!.source).toBe("system-default")
|
||||
})
|
||||
@@ -688,17 +688,17 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
describe("Type safety", () => {
|
||||
test("result has correct ModelResolutionResult shape", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
availableModels: new Set(),
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result!.model).toBe("string")
|
||||
expect(["override", "provider-fallback", "system-default"]).toContain(result!.source)
|
||||
@@ -707,7 +707,7 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
describe("categoryDefaultModel (fuzzy matching for category defaults)", () => {
|
||||
test("applies fuzzy matching to categoryDefaultModel when userModel not provided", () => {
|
||||
// #given - gemini-3-pro is the category default, but only gemini-3-pro-preview is available
|
||||
// given - gemini-3-pro is the category default, but only gemini-3-pro-preview is available
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
categoryDefaultModel: "google/gemini-3-pro",
|
||||
fallbackChain: [
|
||||
@@ -717,16 +717,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should fuzzy match gemini-3-pro → gemini-3-pro-preview
|
||||
// then - should fuzzy match gemini-3-pro → gemini-3-pro-preview
|
||||
expect(result!.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(result!.source).toBe("category-default")
|
||||
})
|
||||
|
||||
test("categoryDefaultModel uses exact match when available", () => {
|
||||
// #given - exact match exists
|
||||
// given - exact match exists
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
categoryDefaultModel: "google/gemini-3-pro",
|
||||
fallbackChain: [
|
||||
@@ -736,16 +736,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should use exact match
|
||||
// then - should use exact match
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("category-default")
|
||||
})
|
||||
|
||||
test("categoryDefaultModel falls through to fallbackChain when no match in availableModels", () => {
|
||||
// #given - categoryDefaultModel has no match, but fallbackChain does
|
||||
// given - categoryDefaultModel has no match, but fallbackChain does
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
categoryDefaultModel: "google/gemini-3-pro",
|
||||
fallbackChain: [
|
||||
@@ -755,16 +755,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "system/default",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should fall through to fallbackChain
|
||||
// then - should fall through to fallbackChain
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
test("userModel takes priority over categoryDefaultModel", () => {
|
||||
// #given - both userModel and categoryDefaultModel provided
|
||||
// given - both userModel and categoryDefaultModel provided
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
categoryDefaultModel: "google/gemini-3-pro",
|
||||
@@ -775,16 +775,16 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "system/default",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - userModel wins
|
||||
// then - userModel wins
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("override")
|
||||
})
|
||||
|
||||
test("categoryDefaultModel works when availableModels is empty but connected provider exists", () => {
|
||||
// #given - no availableModels but connected provider cache exists
|
||||
// given - no availableModels but connected provider cache exists
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"])
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
categoryDefaultModel: "google/gemini-3-pro",
|
||||
@@ -792,10 +792,10 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: "anthropic/claude-sonnet-4-5",
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should use categoryDefaultModel since google is connected
|
||||
// then - should use categoryDefaultModel since google is connected
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("category-default")
|
||||
cacheSpy.mockRestore()
|
||||
@@ -804,7 +804,7 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
describe("Optional systemDefaultModel", () => {
|
||||
test("returns undefined when systemDefaultModel is undefined and no fallback found", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "nonexistent-model" },
|
||||
@@ -813,46 +813,46 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns undefined when no fallbackChain and systemDefaultModel is undefined", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
availableModels: new Set(["openai/gpt-5.2"]),
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test("still returns override when userModel provided even if systemDefaultModel undefined", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
userModel: "anthropic/claude-opus-4-5",
|
||||
availableModels: new Set(),
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBeDefined()
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("override")
|
||||
})
|
||||
|
||||
test("still returns fallback match when systemDefaultModel undefined", () => {
|
||||
// #given
|
||||
// given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic"], model: "claude-opus-4-5" },
|
||||
@@ -861,10 +861,10 @@ describe("resolveModelWithFallback", () => {
|
||||
systemDefaultModel: undefined,
|
||||
}
|
||||
|
||||
// #when
|
||||
// when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
// then
|
||||
expect(result).toBeDefined()
|
||||
expect(result!.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
|
||||
Reference in New Issue
Block a user