singularity-forge/packages/pi-coding-agent/src/core/model-resolver.test.ts
Claude 701ab18d81 fix(models): block unconfigured models from selection surfaces
Filter models whose provider has no working API key or OAuth out of
every user-facing selection path. Previously, stale defaults and scoped
sets could leak unconfigured models into /model, /gsd model, and auto
run — the user could "pick" a model that immediately threw on use.

- model-selector: filter scopedModels via isProviderRequestReady;
  default to "all" scope when no scoped model is ready.
- model-controller: same filter for getModelCandidates, so exact-match
  resolution from /model <term> can't return an unauth'd scoped model.
- model-resolver: gate findInitialModel step 3 on provider readiness so
  a stale saved default falls through to the available-models path.
- startup-model-validation: check configuredExists against getAvailable
  instead of getAll, so a configured-but-unauth default triggers the
  fallback picker and thinking-level reset.
- auto-start: validate resolveDefaultSessionModel against the live
  registry + auth before snapshotting, and warn when PREFERENCES.md
  names an unconfigured model.

https://claude.ai/code/session_015q6b23ap9Pyqdogzz2FXGh
2026-04-12 17:25:06 -05:00

85 lines
2.5 KiB
TypeScript

/**
* Regression test for the #unconfigured-models fix: findInitialModel() must
* skip the saved default when its provider has no working auth, rather than
* returning an unusable model that every selector surface would display as
* "current".
*/
import test from "node:test";
import assert from "node:assert/strict";
import { findInitialModel } from "./model-resolver.js";
function fakeRegistry(options: {
models: Array<{ provider: string; id: string }>;
readyProviders: Set<string>;
}) {
const fullModels = options.models.map((m) => ({
...m,
name: m.id,
api: "anthropic-messages",
baseUrl: "",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 4096,
}));
const available = fullModels.filter((m) => options.readyProviders.has(m.provider));
return {
find(provider: string, id: string) {
return fullModels.find((m) => m.provider === provider && m.id === id);
},
getAvailable() {
return available;
},
isProviderRequestReady(provider: string) {
return options.readyProviders.has(provider);
},
};
}
test("findInitialModel skips saved default when provider has no auth", async () => {
// User saved xai/grok-4 as default, but XAI_API_KEY is unset so xai is
// in the registry but not ready. Previously findInitialModel() step 3
// returned xai anyway — now it must fall through to step 4 and pick
// an available model.
const registry = fakeRegistry({
models: [
{ provider: "xai", id: "grok-4-fast-non-reasoning" },
{ provider: "anthropic", id: "claude-opus-4-6" },
],
readyProviders: new Set(["anthropic"]),
});
const result = await findInitialModel({
scopedModels: [],
isContinuing: false,
defaultProvider: "xai",
defaultModelId: "grok-4-fast-non-reasoning",
modelRegistry: registry as any,
});
assert.ok(result.model, "a model must be returned");
assert.equal(result.model!.provider, "anthropic", "unauth'd saved default must be skipped");
});
test("findInitialModel keeps saved default when provider has auth", async () => {
const registry = fakeRegistry({
models: [
{ provider: "anthropic", id: "claude-opus-4-6" },
{ provider: "openai", id: "gpt-5.4" },
],
readyProviders: new Set(["anthropic", "openai"]),
});
const result = await findInitialModel({
scopedModels: [],
isContinuing: false,
defaultProvider: "openai",
defaultModelId: "gpt-5.4",
modelRegistry: registry as any,
});
assert.equal(result.model?.provider, "openai");
assert.equal(result.model?.id, "gpt-5.4");
});