singularity-forge/src/tests/startup-model-validation.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

151 lines
5.6 KiB
TypeScript

/**
* GSD-2 — Regression tests for startup model validation (#3534)
*
* Verifies that validateConfiguredModel() correctly handles extension-provided
* models and that stale model IDs (e.g. claude-opus-4-6[1m]) trigger fallback.
*/
import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { validateConfiguredModel } from "../startup-model-validation.js";
// ─── Helpers ────────────────────────────────────────────────────────────────
interface MockModel {
provider: string;
id: string;
}
function createMockRegistry(allModels: MockModel[], availableModels?: MockModel[]) {
return {
getAll: () => allModels,
getAvailable: () => availableModels ?? allModels,
};
}
function createMockSettings(defaults: { provider?: string; model?: string; thinking?: "off" | "high" }) {
let currentProvider = defaults.provider;
let currentModel = defaults.model;
let currentThinking: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" = defaults.thinking ?? "off";
return {
getDefaultProvider: () => currentProvider,
getDefaultModel: () => currentModel,
getDefaultThinkingLevel: () => currentThinking,
setDefaultModelAndProvider: (provider: string, modelId: string) => {
currentProvider = provider;
currentModel = modelId;
},
setDefaultThinkingLevel: (level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh") => {
currentThinking = level;
},
// Expose for assertions
get _provider() { return currentProvider; },
get _model() { return currentModel; },
get _thinking() { return currentThinking; },
};
}
// ─── Tests ──────────────────────────────────────────────────────────────────
describe("validateConfiguredModel — regression #3534", () => {
it("preserves valid extension-provided model without overwriting", () => {
// Simulate: user configured claude-code/claude-opus-4-6, extension has registered it
const registry = createMockRegistry([
{ provider: "claude-code", id: "claude-opus-4-6" },
{ provider: "google", id: "gemini-2.5-pro" },
]);
const settings = createMockSettings({ provider: "claude-code", model: "claude-opus-4-6" });
validateConfiguredModel(registry, settings);
// Should NOT have changed the settings — the model is valid
assert.equal(settings._provider, "claude-code");
assert.equal(settings._model, "claude-opus-4-6");
});
it("falls back when configured model ID does not exist in registry", () => {
// Simulate: user configured claude-opus-4-6[1m] but registry only has claude-opus-4-6
const registry = createMockRegistry([
{ provider: "anthropic", id: "claude-opus-4-6" },
{ provider: "google", id: "gemini-2.5-pro" },
]);
const settings = createMockSettings({ provider: "anthropic", model: "claude-opus-4-6[1m]" });
validateConfiguredModel(registry, settings);
// Should have replaced with a fallback — the [1m] variant doesn't exist
assert.notEqual(settings._model, "claude-opus-4-6[1m]");
});
it("does not fall back to google when anthropic models are available", () => {
// Simulate: stale setting triggers fallback, anthropic should be preferred over google
const registry = createMockRegistry([
{ provider: "anthropic", id: "claude-opus-4-6" },
{ provider: "google", id: "gemini-2.5-pro" },
]);
const settings = createMockSettings({ provider: "anthropic", model: "nonexistent-model" });
validateConfiguredModel(registry, settings);
// Should pick anthropic fallback, not google
assert.equal(settings._provider, "anthropic");
assert.equal(settings._model, "claude-opus-4-6");
});
it("resets thinking level when model is replaced", () => {
const registry = createMockRegistry([
{ provider: "anthropic", id: "claude-opus-4-6" },
]);
const settings = createMockSettings({
provider: "anthropic",
model: "nonexistent-model",
thinking: "high",
});
validateConfiguredModel(registry, settings);
assert.equal(settings._thinking, "off");
});
it("is a no-op when no model is configured at all", () => {
const registry = createMockRegistry([
{ provider: "anthropic", id: "claude-opus-4-6" },
{ provider: "google", id: "gemini-2.5-pro" },
]);
const settings = createMockSettings({ provider: undefined, model: undefined });
validateConfiguredModel(registry, settings);
// Should pick a fallback since nothing was configured
assert.ok(settings._provider);
assert.ok(settings._model);
});
it("falls back when configured model exists in registry but provider has no auth", () => {
// Simulate: user configured xai/grok-4 but XAI_API_KEY is unset, so
// xai is in getAll() but not getAvailable(). Previously this slipped
// through configuredExists and left an unusable default in place.
const allModels = [
{ provider: "xai", id: "grok-4-fast-non-reasoning" },
{ provider: "anthropic", id: "claude-opus-4-6" },
];
const availableModels = [
{ provider: "anthropic", id: "claude-opus-4-6" },
];
const registry = createMockRegistry(allModels, availableModels);
const settings = createMockSettings({
provider: "xai",
model: "grok-4-fast-non-reasoning",
thinking: "high",
});
validateConfiguredModel(registry, settings);
// Should have replaced with an authenticated fallback
assert.equal(settings._provider, "anthropic");
assert.equal(settings._model, "claude-opus-4-6");
// Thinking level resets because the original model was replaced
assert.equal(settings._thinking, "off");
});
});