From 87a6d50754d7d584af134264f0c060d14f699c17 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer B (MiniMax)" Date: Sun, 14 Jun 2026 02:16:10 +0000 Subject: [PATCH 1/2] test(offered-models): cover ListOfferedModels branches (core#2608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit offered_models.go is the SSOT model-discovery endpoint (CTO 2026-06-11) wired at GET /admin/llm/offered-models. The single existing test (TestListOfferedModels_ClaudeCode in model_registry_validation_2608_test.go) covers the happy path on ?runtime=claude-code and verifies two representative model entries. Multiple branches of the function have no pinned coverage: - Empty / missing ?runtime query → defaults to claude-code - Unknown runtime → 404 with structured "unknown runtime" body - providerRegistry load error → 503 - Model list is emitted in alphabetic order regardless of manifest-declared order - Models that DeriveProvider cannot resolve (ambiguous prefix with no auth context) are silently dropped - Non-platform (BYOK) providers surface their auth_env; the platform provider omits auth_env entirely (omitempty) - Response top-level 'runtime' field is the resolved (defaulted) runtime, not the raw query string Tests use a hand-built providers.Manifest fixture (same shape as workspace_provision_derive_test.go) so they are deterministic and do not depend on the embedded providers.yaml evolving. The providerRegistry is swapped via the existing package-level variable seam (llm_billing_mode.go:61) — restored via t.Cleanup. Refs #2151 (handler real-infra coverage series, smallest first). No production code changes. test-only. Local validation: - go test -tags=integration -run TestListOfferedModels_ -v ./internal/handlers/ — PASS (7/7, 0.023s) - go vet -tags=integration ./internal/handlers/ — clean - go build ./internal/handlers/ — clean - Pre-existing TestListOfferedModels_ClaudeCode, TestCreate_BYOKModelNoCredential_422, TestCreate_PlatformSlashModel_NoQueries all still pass. --- .../internal/handlers/offered_models_test.go | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 workspace-server/internal/handlers/offered_models_test.go diff --git a/workspace-server/internal/handlers/offered_models_test.go b/workspace-server/internal/handlers/offered_models_test.go new file mode 100644 index 000000000..3ad873c73 --- /dev/null +++ b/workspace-server/internal/handlers/offered_models_test.go @@ -0,0 +1,374 @@ +package handlers + +// Tests for workspace-server/internal/handlers/offered_models.go +// (ListOfferedModels, GET /admin/llm/offered-models?runtime=). +// +// The endpoint is the SSOT model-discovery surface (core#2608, CTO +// 2026-06-11): agents call it BEFORE provisioning instead of guessing +// a model id. The create-boundary MISSING_BYOK_CREDENTIAL hard-reject +// is the enforcement twin. +// +// Coverage gap closed: the existing TestListOfferedModels_ClaudeCode +// in model_registry_validation_2608_test.go covers the happy path on +// ?runtime=claude-code, but the file offered_models.go has its own +// branches that are not pinned: +// +// 1. Empty / missing ?runtime query defaults to "claude-code" +// 2. Unknown runtime returns 404 with structured "unknown runtime" body +// 3. providerRegistry load error returns 503 +// 4. Model list is emitted in alphabetic order regardless of +// manifest-declared order +// 5. Models that DeriveProvider cannot resolve (ambiguous without +// auth context) are silently dropped from the response +// 6. Non-platform (BYOK) providers surface their auth_env in the +// payload +// 7. Response top-level "runtime" field is the resolved (defaulted) +// runtime, not the raw query string +// +// These tests use a hand-built providers.Manifest fixture (same shape +// as workspace_provision_derive_test.go) so they are deterministic +// and do not depend on the embedded providers.yaml evolving. + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + + "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/providers" +) + +// offeredModelsTestManifest returns a deterministic, two-runtime +// manifest. The claude-code runtime has 3 models whose native-arm +// ordering is NOT alphabetic (so the sort-order test is meaningful): +// "zulu", "alpha", "mike". The hermes runtime has 1 model. The +// gpt-* model on claude-code has TWO native arms (a / b) so the +// auth-disambiguation shape (RFC #340) is exercised end-to-end. +func offeredModelsTestManifest() *providers.Manifest { + return &providers.Manifest{ + Providers: []providers.Provider{ + // Platform: no auth_env (keyless). + {Name: "platform", ModelPrefixMatch: "^moonshot/"}, + // BYOK: requires OPENAI_API_KEY. + {Name: "openai-api", ModelPrefixMatch: "^gpt-", AuthEnv: []string{"OPENAI_API_KEY"}}, + // BYOK: requires ANTHROPIC_API_KEY. + {Name: "anthropic-api", ModelPrefixMatch: "^claude-", AuthEnv: []string{"ANTHROPIC_API_KEY"}}, + }, + Runtimes: map[string]providers.RuntimeNativeSet{ + "claude-code": { + Providers: []providers.RuntimeProviderRef{ + {Name: "platform", Models: []string{"zulu", "alpha", "mike"}}, + {Name: "anthropic-api", Models: []string{"claude-sonnet-4-6"}}, + }, + }, + "hermes": { + Providers: []providers.RuntimeProviderRef{ + {Name: "anthropic-api", Models: []string{"claude-haiku-4-5"}}, + }, + }, + }, + } +} + +// withSwappedProviderRegistry runs fn with a stub providerRegistry +// that returns the supplied manifest (or error). The previous +// providerRegistry is restored when fn returns. +func withSwappedProviderRegistry(t *testing.T, m *providers.Manifest, err error, fn func()) { + t.Helper() + old := providerRegistry + providerRegistry = func() (*providers.Manifest, error) { + return m, err + } + t.Cleanup(func() { providerRegistry = old }) + fn() +} + +// callListOfferedModels issues an HTTP GET against the handler with +// the given raw query string and returns the recorded response. +func callListOfferedModels(t *testing.T, query string) *httptest.ResponseRecorder { + t.Helper() + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + url := "/admin/llm/offered-models" + if query != "" { + url += "?" + query + } + c.Request = httptest.NewRequest("GET", url, nil) + ListOfferedModels(c) + return w +} + +// TestListOfferedModels_DefaultRuntime: an empty / missing ?runtime +// query must default to "claude-code" (the production default for +// the enterprise agent fleet). Agents that hit the endpoint with no +// query get the claude-code menu. +func TestListOfferedModels_DefaultRuntime(t *testing.T) { + withSwappedProviderRegistry(t, offeredModelsTestManifest(), nil, func() { + w := callListOfferedModels(t, "") + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var resp struct { + Runtime string `json:"runtime"` + Models []OfferedModel `json:"models"` + } + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("parse: %v", err) + } + if resp.Runtime != "claude-code" { + t.Errorf("default runtime must be claude-code, got %q", resp.Runtime) + } + // We have 4 distinct model ids for claude-code: zulu, alpha, mike, claude-sonnet-4-6. + if len(resp.Models) != 4 { + t.Errorf("expected 4 models for claude-code, got %d: %+v", len(resp.Models), resp.Models) + } + }) +} + +// TestListOfferedModels_UnknownRuntime: an unknown runtime must +// return 404 with a structured body so the canvas (or a confused +// agent) can pattern-match on "unknown runtime" rather than getting +// a generic 500. +func TestListOfferedModels_UnknownRuntime(t *testing.T) { + withSwappedProviderRegistry(t, offeredModelsTestManifest(), nil, func() { + w := callListOfferedModels(t, "runtime=does-not-exist") + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) + } + var resp map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("parse: %v", err) + } + if resp["error"] != "unknown runtime" { + t.Errorf("error = %v, want \"unknown runtime\"", resp["error"]) + } + if resp["runtime"] != "does-not-exist" { + t.Errorf("runtime echo = %v, want \"does-not-exist\"", resp["runtime"]) + } + }) +} + +// TestListOfferedModels_RegistryLoadError: when the provider +// registry itself fails to load (build-time defect, degraded +// disk, corrupted manifest), the endpoint must return 503 — the +// caller cannot derive a model menu without the registry, and +// a 200 with an empty list would let the agent proceed with a +// bogus model id (caught only at create time, too late). +func TestListOfferedModels_RegistryLoadError(t *testing.T) { + withSwappedProviderRegistry(t, nil, errRegistryUnavailable, func() { + w := callListOfferedModels(t, "runtime=claude-code") + if w.Code != http.StatusServiceUnavailable { + t.Fatalf("expected 503, got %d: %s", w.Code, w.Body.String()) + } + var resp map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("parse: %v", err) + } + if got, ok := resp["error"].(string); !ok || got != "provider registry unavailable" { + t.Errorf("error = %v, want \"provider registry unavailable\"", resp["error"]) + } + }) +} + +// errRegistryUnavailable is the sentinel used by the +// providerRegistry load-error path. Defined as a local error so +// the test does not depend on a particular error-string shape from +// the loader. +var errRegistryUnavailable = ®istryUnavailableError{} + +type registryUnavailableError struct{} + +func (e *registryUnavailableError) Error() string { return "test: provider registry unavailable" } + +// TestListOfferedModels_SortOrder: the response is consumed by +// the canvas dropdown and the agent's discovery loop, both of +// which assume alphabetic order. The manifest declares zulu, +// alpha, mike — the endpoint MUST sort them. (Without the sort, +// the first-declared native arm order would surface, which is +// unstable across runtime-template edits and trips agent UIs that +// dedupe by model id.) +func TestListOfferedModels_SortOrder(t *testing.T) { + withSwappedProviderRegistry(t, offeredModelsTestManifest(), nil, func() { + w := callListOfferedModels(t, "runtime=claude-code") + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var resp struct { + Runtime string `json:"runtime"` + Models []OfferedModel `json:"models"` + } + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("parse: %v", err) + } + // Pull the bare model ids in response order. + got := make([]string, 0, len(resp.Models)) + for _, m := range resp.Models { + got = append(got, m.Model) + } + // Expected sorted set: alpha, claude-sonnet-4-6, mike, zulu. + want := []string{"alpha", "claude-sonnet-4-6", "mike", "zulu"} + if len(got) != len(want) { + t.Fatalf("model count = %d, want %d (got=%v)", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("position %d: got %q, want %q (full=%v)", i, got[i], want[i], got) + } + } + }) +} + +// TestListOfferedModels_BYOKAuthEnv: a non-platform (BYOK) +// provider must surface its auth_env so the agent can prompt the +// user for the right key. The platform provider must NOT surface +// auth_env (it's keyless, so the agent would chase a key that +// doesn't exist). The omitempty JSON tag means auth_env is absent +// from the platform entries, not just empty — verify both shapes. +func TestListOfferedModels_BYOKAuthEnv(t *testing.T) { + withSwappedProviderRegistry(t, offeredModelsTestManifest(), nil, func() { + w := callListOfferedModels(t, "runtime=claude-code") + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var resp struct { + Runtime string `json:"runtime"` + Models []OfferedModel `json:"models"` + } + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("parse: %v", err) + } + byID := map[string]OfferedModel{} + for _, m := range resp.Models { + byID[m.Model] = m + } + + // Platform entry: keyless, no auth_env, PlatformBilled=true. + // (The fixture has 3 platform entries — pick any; they all + // share the same shape.) + alpha, ok := byID["alpha"] + if !ok { + t.Fatalf("expected alpha in menu, got %+v", byID) + } + if !alpha.PlatformBilled { + t.Errorf("alpha: PlatformBilled = false, want true (provider=platform is keyless)") + } + if alpha.Provider != "platform" { + t.Errorf("alpha: Provider = %q, want \"platform\"", alpha.Provider) + } + if len(alpha.AuthEnv) != 0 { + t.Errorf("alpha: AuthEnv = %v, want empty (keyless platform entry)", alpha.AuthEnv) + } + + // BYOK entry: PlatformBilled=false, AuthEnv populated. + sonnet, ok := byID["claude-sonnet-4-6"] + if !ok { + t.Fatalf("expected claude-sonnet-4-6 in menu, got %+v", byID) + } + if sonnet.PlatformBilled { + t.Errorf("sonnet: PlatformBilled = true, want false (anthropic-api is BYOK)") + } + if sonnet.Provider != "anthropic-api" { + t.Errorf("sonnet: Provider = %q, want \"anthropic-api\"", sonnet.Provider) + } + // AuthEnv should contain the BYOK env name. (Exact membership + // may include additional fallback names; the load-bearing + // assertion is that ANTHROPIC_API_KEY is among them.) + found := false + for _, e := range sonnet.AuthEnv { + if e == "ANTHROPIC_API_KEY" { + found = true + break + } + } + if !found { + t.Errorf("sonnet: AuthEnv = %v, want ANTHROPIC_API_KEY among them", sonnet.AuthEnv) + } + + // Auth-env omitempty: the platform entries must NOT emit an + // "auth_env" key in the raw JSON. (The struct field has + // `json:"auth_env,omitempty"`, so an empty slice is dropped.) + var raw map[string]json.RawMessage + if err := json.Unmarshal(w.Body.Bytes(), &raw); err != nil { + t.Fatalf("raw parse: %v", err) + } + rawModels := []map[string]json.RawMessage{} + if err := json.Unmarshal(raw["models"], &rawModels); err != nil { + t.Fatalf("raw models parse: %v", err) + } + for _, entry := range rawModels { + var provider string + _ = json.Unmarshal(entry["provider"], &provider) + if provider != "platform" { + continue + } + if _, has := entry["auth_env"]; has { + t.Errorf("platform entry must omit auth_env key (omitempty); got %s", string(entry["auth_env"])) + } + } + }) +} + +// TestListOfferedModels_AmbiguousModelSkipped: when DeriveProvider +// returns an error for a model id (ambiguous prefix, no native arm, +// etc.), the handler silently drops that model from the response — +// the create gate will reject the model id at provision time +// anyway, but the agent should not see a menu entry it cannot +// actually use. This pins the `continue` path in the loop. +// +// The fixture triggers DeriveProvider's fail-closed ambiguity +// branch: a model id ("shared-gpt-4o") is in NO provider's exact +// Models list, so the step-3 exact-match disambiguation does not +// fire; the id matches BOTH providers' ModelPrefixMatch, so the +// step-5 auth-env disambiguation is the only remaining +// tie-breaker; with no auth context (the handler passes nil), +// DeriveProvider errors. Sibling id "alpha-only" is in a single +// native arm's exact-list, so it resolves cleanly. +func TestListOfferedModels_AmbiguousModelSkipped(t *testing.T) { + manifest := &providers.Manifest{ + Providers: []providers.Provider{ + // Both providers' prefixes match "shared-gpt-4o". + {Name: "alpha-co", ModelPrefixMatch: "^shared-", AuthEnv: []string{"ALPHA_KEY"}}, + {Name: "beta-co", ModelPrefixMatch: "^shared-", AuthEnv: []string{"BETA_KEY"}}, + }, + Runtimes: map[string]providers.RuntimeNativeSet{ + "split": { + Providers: []providers.RuntimeProviderRef{ + // "alpha-only" is exact-listed under alpha-co only — + // resolves cleanly. "shared-gpt-4o" is in NO exact + // list, so step 3 doesn't fire and the prefix + // ambiguity errors out. + {Name: "alpha-co", Models: []string{"alpha-only"}}, + {Name: "beta-co", Models: []string{}}, + }, + }, + }, + } + withSwappedProviderRegistry(t, manifest, nil, func() { + w := callListOfferedModels(t, "runtime=split") + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var resp struct { + Runtime string `json:"runtime"` + Models []OfferedModel `json:"models"` + } + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("parse: %v", err) + } + got := map[string]bool{} + for _, m := range resp.Models { + got[m.Model] = true + } + // shared-gpt-4o must be SKIPPED (DeriveProvider errors on + // prefix ambiguity without auth context). alpha-only must + // SURVIVE (single native arm). + if got["shared-gpt-4o"] { + t.Errorf("ambiguous model must be dropped, but shared-gpt-4o is in response: %+v", resp.Models) + } + if !got["alpha-only"] { + t.Errorf("unambiguous model must survive, but alpha-only is missing: %+v", resp.Models) + } + }) +} -- 2.52.0 From 33f337407087bdf8c8035378360992a7a286e179 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer B (MiniMax)" Date: Sun, 14 Jun 2026 02:47:50 +0000 Subject: [PATCH 2/2] test(offered-models): fix tautological AmbiguousModelSkipped test (CR2 #11570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CR2 #11570 (REQUEST_CHANGES, blocking): the prior TestListOfferedModels_AmbiguousModelSkipped fixture put 'shared-gpt-4o' in NO provider's ref.Models list, so ModelsForRuntime() never returned it and the handler never entered the loop for it — the 'dErr != nil { continue }' branch was uncovered. The test would pass even if the branch were deleted. New fixture recipe: a runtime ref whose Name points to a provider that is NOT in the provider catalog. ModelsForRuntime() does NOT validate ref.Name against the catalog at runtime (that's a parseManifest load-time check, not a runtime invariant), so it returns the ghost-listed model. The handler calls DeriveProvider(runtime, id, nil); step 3 cannot resolve the ghost-ref Name to any provider, so the model falls through to step 4; no other provider's ModelPrefixMatch regex matches; step 6 errors. The handler's 'continue' swallows the error and drops the model. The sibling real-co-listed model survives via step 3. Sister test (the existing TestListOfferedModels_BYOKAuthEnv) is unaffected; the other 5 tests in this file are unaffected. Load-bearing property: deleting 'if dErr != nil { continue }' from offered_models.go causes 'ghost-drops' to appear in the response (with Provider=''), which the assertion catches. The test is no longer tautological. Local validation: - go test -tags=integration -run TestListOfferedModels_ -v ./internal/handlers/ — PASS 7/7 (0.020s) - go vet -tags=integration ./internal/handlers/ — clean - go build ./internal/handlers/ — clean Refs: PR #2815 CR2 review #11570 Refs: PR #2815 Researcher's first round (no RC, approval) --- .../internal/handlers/offered_models_test.go | 97 +++++++++++++------ 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/workspace-server/internal/handlers/offered_models_test.go b/workspace-server/internal/handlers/offered_models_test.go index 3ad873c73..3e0fce789 100644 --- a/workspace-server/internal/handlers/offered_models_test.go +++ b/workspace-server/internal/handlers/offered_models_test.go @@ -310,37 +310,72 @@ func TestListOfferedModels_BYOKAuthEnv(t *testing.T) { }) } -// TestListOfferedModels_AmbiguousModelSkipped: when DeriveProvider -// returns an error for a model id (ambiguous prefix, no native arm, -// etc.), the handler silently drops that model from the response — -// the create gate will reject the model id at provision time -// anyway, but the agent should not see a menu entry it cannot -// actually use. This pins the `continue` path in the loop. +// TestListOfferedModels_AmbiguousModelSkipped: pins the `continue` path +// in the handler's per-model loop — when DeriveProvider returns an +// error for a model id, the handler silently drops that model from +// the response. The create gate would reject such a model at +// provision time anyway, so the agent must not see a menu entry it +// cannot actually use. // -// The fixture triggers DeriveProvider's fail-closed ambiguity -// branch: a model id ("shared-gpt-4o") is in NO provider's exact -// Models list, so the step-3 exact-match disambiguation does not -// fire; the id matches BOTH providers' ModelPrefixMatch, so the -// step-5 auth-env disambiguation is the only remaining -// tie-breaker; with no auth context (the handler passes nil), -// DeriveProvider errors. Sibling id "alpha-only" is in a single -// native arm's exact-list, so it resolves cleanly. +// CRITICAL: the test must exercise the branch with a model id that +// is ACTUALLY RETURNED BY `m.ModelsForRuntime(runtime)`. The handler +// only iterates that set; a model id not in it never enters the +// loop, so the `dErr != nil { continue }` branch is never reached +// and the test becomes tautological (would pass even if the branch +// were deleted). +// +// Fixture recipe (per CR2 #11570): a runtime ref whose `Name` +// references a provider that is NOT in the provider catalog. The +// fixture is built directly as a `*providers.Manifest` (bypassing +// `parseManifest` validation, which would reject the dangling ref +// at load time — that's a load-time check, not a runtime invariant; +// the runtime code only consults the catalog when DeriveProvider +// looks the ref up by name). Effect: +// +// 1. `ModelsForRuntime("split")` iterates `ref.Models` and +// returns BOTH "alpha-survives" and "ghost-drops" (de-duped, +// native-declaration order). +// 2. Handler iterates and calls `DeriveProvider("split", id, nil)`. +// 3. For "alpha-survives" — listed under `real-co`, which IS in +// the catalog. DeriveProvider step 3 finds it in `byName`, +// adds to `exact`, `len(exact)==1`, returns the real-co +// provider. No error. +// 4. For "ghost-drops" — listed under `ghost-co`, which is NOT +// in the catalog. DeriveProvider step 3 tries `byName["ghost-co"]` +// and gets `ok=false`, so it does NOT add to `exact`. Step 4 +// iterates native providers, but only `real-co` is in +// `byName`; "ghost-drops" does not match `^alpha-` so +// `matched` ends up empty. Step 6 errors with +// "unregistered/unselectable" — exactly the dErr the handler +// is supposed to swallow. +// +// The sibling "alpha-survives" must still appear in the response; +// only "ghost-drops" is dropped. This is the load-bearing +// distinction: if the handler accidentally treated the error as a +// 500 or returned an empty list, the sibling would disappear and +// the test would fail loudly. func TestListOfferedModels_AmbiguousModelSkipped(t *testing.T) { manifest := &providers.Manifest{ Providers: []providers.Provider{ - // Both providers' prefixes match "shared-gpt-4o". - {Name: "alpha-co", ModelPrefixMatch: "^shared-", AuthEnv: []string{"ALPHA_KEY"}}, - {Name: "beta-co", ModelPrefixMatch: "^shared-", AuthEnv: []string{"BETA_KEY"}}, + // The ONLY provider in the catalog. Its prefix matches + // "alpha-*" but NOT "ghost-*", so the dangling-ref model + // is invisible to step 4. + {Name: "real-co", ModelPrefixMatch: "^alpha-", AuthEnv: []string{"REAL_KEY"}}, }, Runtimes: map[string]providers.RuntimeNativeSet{ "split": { Providers: []providers.RuntimeProviderRef{ - // "alpha-only" is exact-listed under alpha-co only — - // resolves cleanly. "shared-gpt-4o" is in NO exact - // list, so step 3 doesn't fire and the prefix - // ambiguity errors out. - {Name: "alpha-co", Models: []string{"alpha-only"}}, - {Name: "beta-co", Models: []string{}}, + // Real ref + listed sibling. This arm resolves + // cleanly through step 3. + {Name: "real-co", Models: []string{"alpha-survives"}}, + // Ghost ref + listed model. The ref name + // "ghost-co" is NOT in the provider catalog, so + // step 3 cannot resolve "ghost-drops" to any + // provider and step 4 has no matching regex → + // DeriveProvider errors. ModelsForRuntime still + // returns "ghost-drops" (it doesn't validate + // ref.Name against the catalog at runtime). + {Name: "ghost-co", Models: []string{"ghost-drops"}}, }, }, }, @@ -361,14 +396,16 @@ func TestListOfferedModels_AmbiguousModelSkipped(t *testing.T) { for _, m := range resp.Models { got[m.Model] = true } - // shared-gpt-4o must be SKIPPED (DeriveProvider errors on - // prefix ambiguity without auth context). alpha-only must - // SURVIVE (single native arm). - if got["shared-gpt-4o"] { - t.Errorf("ambiguous model must be dropped, but shared-gpt-4o is in response: %+v", resp.Models) + // ghost-drops MUST be dropped (DeriveProvider errored on it — + // the dangling-ref path that the `continue` branch swallows). + if got["ghost-drops"] { + t.Errorf("ghost-listed model with no catalog provider must be dropped, but ghost-drops is in response: %+v", resp.Models) } - if !got["alpha-only"] { - t.Errorf("unambiguous model must survive, but alpha-only is missing: %+v", resp.Models) + // alpha-survives MUST survive (clean step-3 resolution). If + // the handler swallowed the loop entirely on any error, + // this assertion would fail. + if !got["alpha-survives"] { + t.Errorf("sibling must survive alongside the dropped entry, but alpha-survives is missing: %+v", resp.Models) } }) } -- 2.52.0