Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
23 KiB
JSON

{"repo": "cfbender/hygge", "files": {"cmd/hygge/cli/provider_test.go": {"731e0484ca1036d2c3ba6e166b99f249b857bf53": {"t": 1786068295.9621701, "n": 21266, "h": "c048d3fc87a48f14", "c": "package cli\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"charm.land/bubbles/v2/key\"\n\t\"charm.land/bubbles/v2/list\"\n\ttea \"charm.land/bubbletea/v2\"\n\tuv \"github.com/charmbracelet/ultraviolet\"\n\n\t\"github.com/cfbender/hygge/internal/auth\"\n\t\"github.com/cfbender/hygge/internal/provider\"\n)\n\n// ansiEscapeRE strips ANSI/VT100 escape sequences from rendered output so\n// tests can match against plain text.\nvar ansiEscapeRE = regexp.MustCompile(`\\x1b\\[[0-9;]*[mA-Za-z]`)\n\n// authOptsFor returns LoadOptions for the hermetic home dir.\nfunc authOptsFor(home string) auth.LoadOptions {\n\treturn auth.LoadOptions{\n\t\tHomeDir: home,\n\t\tXDGStateHome: filepath.Join(home, \".local\", \"state\"),\n\t}\n}\n\n// TestProviderAuth_NonTTYStdin: piping a key on stdin saves the\n// credential. Verifies the file exists with mode 0600 and the key\n// round-trips.\nfunc TestProviderAuth_NonTTYStdin(t *testing.T) {\n\thome := hermeticHome(t)\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetIn(strings.NewReader(\"sk-pasted-key-1234\\n\"))\n\troot.SetArgs([]string{\"provider\", \"auth\", \"anthropic\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"Saved credential for anthropic\") {\n\t\tt.Errorf(\"missing save confirmation:\\n%s\", got)\n\t}\n\t// Masked echo must not contain the full key.\n\tif strings.Contains(got, \"sk-pasted-key-1234\") {\n\t\tt.Errorf(\"unmasked key leaked to output:\\n%s\", got)\n\t}\n\n\t// File exists with mode 0600.\n\tp, err := auth.Path(authOptsFor(home))\n\tif err != nil {\n\t\tt.Fatalf(\"auth.Path: %v\", err)\n\t}\n\tfi, err := os.Stat(p)\n\tif err != nil {\n\t\tt.Fatalf(\"stat: %v\", err)\n\t}\n\tif mode := fi.Mode().Perm(); mode != 0o600 {\n\t\tt.Errorf(\"file mode: got %04o, want 0600\", mode)\n\t}\n\n\t// Credential round-trips.\n\tstore, err := auth.Load(authOptsFor(home))\n\tif err != nil {\n\t\tt.Fatalf(\"auth.Load: %v\", err)\n\t}\n\tcred, ok := store.Get(\"anthropic\")\n\tif !ok {\n\t\tt.Fatal(\"credential not stored\")\n\t}\n\tif cred.APIKey != \"sk-pasted-key-1234\" {\n\t\tt.Errorf(\"APIKey: got %q, want %q\", cred.APIKey, \"sk-pasted-key-1234\")\n\t}\n\tif cred.Type != auth.CredAPIKey {\n\t\tt.Errorf(\"Type: got %q, want %q\", cred.Type, auth.CredAPIKey)\n\t}\n}\n\n// TestProviderAuth_NoArgPicksFromList: with no positional arg, the\n// picker reads a provider selection from stdin. Followed by a piped\n// API key.\nfunc TestProviderAuth_NoArgPicksFromList(t *testing.T) {\n\thome := hermeticHome(t)\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetIn(strings.NewReader(\"anthropic\\nsk-second-key-9999\\n\"))\n\troot.SetArgs([]string{\"provider\", \"auth\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\n\tstore, err := auth.Load(authOptsFor(home))\n\tif err != nil {\n\t\tt.Fatalf(\"auth.Load: %v\", err)\n\t}\n\tcred, ok := store.Get(\"anthropic\")\n\tif !ok {\n\t\tt.Fatalf(\"expected anthropic credential; store had: %v\", store.List())\n\t}\n\tif cred.APIKey != \"sk-second-key-9999\" {\n\t\tt.Errorf(\"APIKey: got %q, want %q\", cred.APIKey, \"sk-second-key-9999\")\n\t}\n}\n\n// TestProviderAuth_EmptyKeyRejected: piping an empty line rejects the\n// save with a clear message.\nfunc TestProviderAuth_EmptyKeyRejected(t *testing.T) {\n\thermeticHome(t)\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetIn(strings.NewReader(\"\\n\"))\n\troot.SetArgs([]string{\"provider\", \"auth\", \"anthropic\"})\n\terr := root.Execute()\n\tif err == nil {\n\t\tt.Fatal(\"expected error for empty key\")\n\t}\n\tif !strings.Contains(out.String(), \"empty API key\") {\n\t\tt.Errorf(\"missing rejection message:\\n%s\", out.String())\n\t}\n}\n\n// TestProviderList_Empty: no credentials → friendly empty marker.\nfunc TestProviderList_Empty(t *testing.T) {\n\thome := hermeticHome(t)\n\t// hermeticHome seeds a fake credential so runTUI tests pass.\n\t// `provider list` is the one place that asserts the store is empty.\n\txdgState := filepath.Join(home, \".local\", \"state\")\n\tif err := auth.Remove(\"anthropic\", auth.LoadOptions{HomeDir: home, XDGStateHome: xdgState}); err != nil {\n\t\tt.Fatalf(\"auth.Remove: %v\", err)\n\t}\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetArgs([]string{\"provider\", \"list\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\tif !strings.Contains(out.String(), \"(no providers configured\") {\n\t\tt.Errorf(\"missing empty marker:\\n%s\", out.String())\n\t}\n}\n\n// TestProviderList_OneCredentialMasked: list shows the masked key,\n// never the raw value.\nfunc TestProviderList_OneCredentialMasked(t *testing.T) {\n\thome := hermeticHome(t)\n\tif err := auth.Set(\"anthropic\",\n\t\tauth.Credential{Type: auth.CredAPIKey, APIKey: \"sk-abcdefghijklmnop\"},\n\t\tauthOptsFor(home)); err != nil {\n\t\tt.Fatalf(\"seed: %v\", err)\n\t}\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetArgs([]string{\"provider\", \"list\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"anthropic\") {\n\t\tt.Errorf(\"missing provider name:\\n%s\", got)\n\t}\n\tif !strings.Contains(got, \"api_key\") {\n\t\tt.Errorf(\"missing type column:\\n%s\", got)\n\t}\n\tmasked := maskKey(\"sk-abcdefghijklmnop\")\n\tif !strings.Contains(got, masked) {\n\t\tt.Errorf(\"missing masked key %q in:\\n%s\", masked, got)\n\t}\n\tif strings.Contains(got, \"sk-abcdefghijklmnop\") {\n\t\tt.Errorf(\"raw key leaked to output:\\n%s\", got)\n\t}\n}\n\n// TestProviderRemove_NoConfirm: -f skips the prompt and deletes the\n// credential.\nfunc TestProviderRemove_NoConfirm(t *testing.T) {\n\thome := hermeticHome(t)\n\tif err := auth.Set(\"anthropic\",\n\t\tauth.Credential{Type: auth.CredAPIKey, APIKey: \"sk-x\"},\n\t\tauthOptsFor(home)); err != nil {\n\t\tt.Fatalf(\"seed: %v\", err)\n\t}\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetArgs([]string{\"provider\", \"remove\", \"anthropic\", \"--no-confirm\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\tif !strings.Contains(out.String(), \"Removed credential for anthropic\") {\n\t\tt.Errorf(\"missing confirmation:\\n%s\", out.String())\n\t}\n\n\tstore, err := auth.Load(authOptsFor(home))\n\tif err != nil {\n\t\tt.Fatalf(\"auth.Load: %v\", err)\n\t}\n\tif _, ok := store.Get(\"anthropic\"); ok {\n\t\tt.Error(\"credential still present after remove\")\n\t}\n}\n\n// TestProviderRemove_Missing: removing a missing provider exits 0 with\n// the friendly marker.\nfunc TestProviderRemove_Missing(t *testing.T) {\n\thermeticHome(t)\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetArgs([]string{\"provider\", \"remove\", \"nonexistent\", \"--no-confirm\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\tif !strings.Contains(out.String(), \"(no credential for nonexistent\") {\n\t\tt.Errorf(\"missing 'no credential' marker:\\n%s\", out.String())\n\t}\n}\n\n// TestProviderRemove_ConfirmYes: piped \"y\\n\" confirms the deletion.\nfunc TestProviderRemove_ConfirmYes(t *testing.T) {\n\thome := hermeticHome(t)\n\tif err := auth.Set(\"anthropic\",\n\t\tauth.Credential{Type: auth.CredAPIKey, APIKey: \"sk-x\"},\n\t\tauthOptsFor(home)); err != nil {\n\t\tt.Fatalf(\"seed: %v\", err)\n\t}\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetIn(strings.NewReader(\"y\\n\"))\n\troot.SetArgs([]string{\"provider\", \"remove\", \"anthropic\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\tif !strings.Contains(out.String(), \"Removed credential\") {\n\t\tt.Errorf(\"missing removal confirmation:\\n%s\", out.String())\n\t}\n}\n\n// TestProviderRemove_ConfirmNo: piped \"n\\n\" cancels the deletion.\nfunc TestProviderRemove_ConfirmNo(t *testing.T) {\n\thome := hermeticHome(t)\n\tif err := auth.Set(\"anthropic\",\n\t\tauth.Credential{Type: auth.CredAPIKey, APIKey: \"sk-x\"},\n\t\tauthOptsFor(home)); err != nil {\n\t\tt.Fatalf(\"seed: %v\", err)\n\t}\n\n\troot := NewRootCmd()\n\tvar out bytes.Buffer\n\troot.SetOut(&out)\n\troot.SetErr(&out)\n\troot.SetIn(strings.NewReader(\"n\\n\"))\n\troot.SetArgs([]string{\"provider\", \"remove\", \"anthropic\"})\n\tif err := root.Execute(); err != nil {\n\t\tt.Fatalf(\"execute: %v\", err)\n\t}\n\tif !strings.Contains(out.String(), \"Cancelled\") {\n\t\tt.Errorf(\"missing cancel message:\\n%s\", out.String())\n\t}\n\t// Still present.\n\tstore, _ := auth.Load(authOptsFor(home))\n\tif _, ok := store.Get(\"anthropic\"); !ok {\n\t\tt.Error(\"credential removed despite 'n' answer\")\n\t}\n}\n\nfunc TestAuthMethodOptions_AreProviderAware(t *testing.T) {\n\topenCodeMethods := authMethodOptions(\"opencode-go\")\n\tif len(openCodeMethods) != 1 || openCodeMethods[0].method != authMethodAPIKey {\n\t\tt.Fatalf(\"opencode-go methods = %#v, want only API key\", openCodeMethods)\n\t}\n\tfor _, method := range openCodeMethods {\n\t\tif strings.Contains(strings.ToLower(method.title), \"oauth\") || strings.Contains(strings.ToLower(method.description), \"chatgpt\") {\n\t\t\tt.Fatalf(\"opencode-go should not mention OAuth/ChatGPT: %#v\", method)\n\t\t}\n\t}\n\n\topenAIMethods := authMethodOptions(\"openai\")\n\tif len(openAIMethods) != 2 {\n\t\tt.Fatalf(\"openai methods count = %d, want 2\", len(openAIMethods))\n\t}\n\tif openAIMethods[1].method != authMethodOAuth {\n\t\tt.Fatalf(\"openai second method = %q, want oauth\", openAIMethods[1].method)\n\t}\n}\n\nfunc TestPickAuthMethodFromLine_RejectsUnavailableOAuth(t *testing.T) {\n\t_, err := pickAuthMethodFromLine(bufio.NewReader(strings.NewReader(\"2\\n\")), authMethodOptions(\"opencode-go\"))\n\tif err == nil {\n\t\tt.Fatalf(\"expected unavailable OAuth selection to error\")\n\t}\n\n\tgot, err := pickAuthMethodFromLine(bufio.NewReader(strings.NewReader(\"2\\n\")), authMethodOptions(\"openai\"))\n\tif err != nil {\n\t\tt.Fatalf(\"openai oauth selection: %v\", err)\n\t}\n\tif got != authMethodOAuth {\n\t\tt.Fatalf(\"method got %q, want oauth\", got)\n\t}\n}\n\nfunc TestPickProviderFromLine_NumberAndName(t *testing.T) {\n\tnames := []string{\"anthropic\", \"openai\"}\n\n\tgot, err := pickProviderFromLine(bufio.NewReader(strings.NewReader(\"2\\n\")), names)\n\tif err != nil {\n\t\tt.Fatalf(\"pick number: %v\", err)\n\t}\n\tif got != \"openai\" {\n\t\tt.Fatalf(\"pick number got %q, want openai\", got)\n\t}\n\n\tgot, err = pickProviderFromLine(bufio.NewReader(strings.NewReader(\"custom\\n\")), names)\n\tif err != nil {\n\t\tt.Fatalf(\"pick name: %v\", err)\n\t}\n\tif got != \"custom\" {\n\t\tt.Fatalf(\"pick name got %q, want custom\", got)\n\t}\n}\n\nfunc TestProviderSelectModel_EnterChoosesCurrentItem(t *testing.T) {\n\tm := newProviderSelectModelForTest([]string{\"anthropic\", \"openai\"})\n\tupdated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))\n\tgot := updated.(providerSelectModel)\n\tif got.choice != \"anthropic\" {\n\t\tt.Fatalf(\"choice got %q, want anthropic\", got.choice)\n\t}\n\tif got.cancelled {\n\t\tt.Fatalf(\"enter should not mark selection cancelled\")\n\t}\n}\n\nfunc TestProviderSelectModel_CancelKeys(t *testing.T) {\n\tfor _, msg := range []tea.KeyPressMsg{\n\t\ttea.KeyPressMsg(tea.Key{Code: tea.KeyEsc}),\n\t\ttea.KeyPressMsg(tea.Key{Code: 'c', Text: \"c\", Mod: uv.ModCtrl}),\n\t} {\n\t\tm := newProviderSelectModelForTest([]string{\"anthropic\"})\n\t\tupdated, _ := m.Update(msg)\n\t\tgot := updated.(providerSelectModel)\n\t\tif !got.cancelled {\n\t\t\tt.Fatalf(\"%s should cancel\", msg.Keystroke())\n\t\t}\n\t}\n}\n\nfunc newProviderSelectModelForTest(names []string) providerSelectModel {\n\titems := make([]list.Item, 0, len(names))\n\tfor _, name := range names {\n\t\titems = append(items, providerSelectItem{name: name})\n\t}\n\tdelegate := list.NewDefaultDelegate()\n\treturn providerSelectModel{list: list.New(items, delegate, 80, 10)}\n}\n\n// TestMaskKey covers the masking helper directly.\nfunc TestMaskKey(t *testing.T) {\n\tcases := []struct {\n\t\tin, want string\n\t}{\n\t\t{\"\", \"***\"},\n\t\t{\"short\", \"***\"},\n\t\t{\"12345678\", \"***\"},\n\t\t{\"sk-abcdef\", \"sk-***cdef\"},\n\t\t{\"sk-abcdefghijklmnop\", \"sk-***mnop\"},\n\t}\n\tfor _, c := range cases {\n\t\tif got := maskKey(c.in); got != c.want {\n\t\t\tt.Errorf(\"maskKey(%q): got %q, want %q\", c.in, got, c.want)\n\t\t}\n\t}\n}\n\n// TestProviderEnvVar covers the env-var map directly.\nfunc TestProviderEnvVar(t *testing.T) {\n\tcases := map[string]string{\n\t\t\"anthropic\": \"ANTHROPIC_API_KEY\",\n\t\t\"openai\": \"OPENAI_API_KEY\",\n\t\t\"openrouter\": \"OPENROUTER_API_KEY\",\n\t\t\"mistral\": \"MISTRAL_API_KEY\",\n\t\t\"groq\": \"GROQ_API_KEY\",\n\t\t\"deepseek\": \"DEEPSEEK_API_KEY\",\n\t\t\"gemini\": \"GOOGLE_API_KEY\",\n\t\t\"google\": \"\",\n\t\t\"xai\": \"XAI_API_KEY\",\n\t\t\"unknown\": \"\",\n\t\t\"\": \"\",\n\t}\n\tfor in, want := range cases {\n\t\tif got := providerEnvVar(in); got != want {\n\t\t\tt.Errorf(\"providerEnvVar(%q): got %q, want %q\", in, got, want)\n\t\t}\n\t}\n}\n\n// TestKnownProviders sanity-checks the picker enumeration.\nfunc TestKnownProviders(t *testing.T) {\n\tnames := knownProviders()\n\tif len(names) <= 8 {\n\t\tt.Errorf(\"knownProviders returned %d entries; want Catwalk provider list, not the legacy 8-provider cap\", len(names))\n\t}\n\tfor _, name := range []string{\"anthropic\", \"openai\", \"openrouter\"} {\n\t\tif !providerKnownContains(name) {\n\t\t\tt.Errorf(\"%s missing from knownProviders\", name)\n\t\t}\n\t}\n}\n\n// TestBuildNamedStubProvider_ErrAuthWhenNoCredential confirms that\n// buildNamedStubProvider returns provider.ErrAuth for providers with a\n// known canonical env var when neither opts[\"api_key\"] nor the env var\n// is set. This preserves the bootstrap auth-fallback path\n// (errors.Is(err, provider.ErrAuth) → stubProvider) for providers\n// whose env var the CLI knows about.\nfunc TestBuildNamedStubProvider_ErrAuthWhenNoCredential(t *testing.T) {\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"\")\n\tt.Setenv(\"OPENAI_API_KEY\", \"\")\n\tt.Setenv(\"OPENROUTER_API_KEY\", \"\")\n\n\tfor _, name := range []string{\"anthropic\", \"openai\", \"openrouter\"} {\n\t\t_, err := buildNamedStubProvider(name, map[string]any{})\n\t\tif !errors.Is(err, provider.ErrAuth) {\n\t\t\tt.Errorf(\"%s with no credential: want ErrAuth, got %v\", name, err)\n\t\t}\n\t}\n}\n\n// TestBuildNamedStubProvider_SucceedWithCredential confirms that\n// buildNamedStubProvider succeeds when opts[\"api_key\"] or the env var\n// is populated (as bootstrap does after resolveProviderOptionsFor\n// injects the auth-store credential).\nfunc TestBuildNamedStubProvider_SucceedWithCredential(t *testing.T) {\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"\")\n\tt.Setenv(\"OPENAI_API_KEY\", \"\")\n\tt.Setenv(\"OPENROUTER_API_KEY\", \"\")\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tenvVar string\n\t}{\n\t\t{\"anthropic\", \"ANTHROPIC_API_KEY\"},\n\t\t{\"openai\", \"OPENAI_API_KEY\"},\n\t\t{\"openrouter\", \"OPENROUTER_API_KEY\"},\n\t} {\n\t\t// Via opts[\"api_key\"].\n\t\tp, err := buildNamedStubProvider(tc.name, map[string]any{\"api_key\": \"sk-test-key\"})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%s with opts api_key: unexpected error: %v\", tc.name, err)\n\t\t}\n\t\tif p == nil {\n\t\t\tt.Errorf(\"%s with opts api_key: got nil provider\", tc.name)\n\t\t} else if p.Name() != tc.name {\n\t\t\tt.Errorf(\"%s: Name() = %q, want %q\", tc.name, p.Name(), tc.name)\n\t\t}\n\n\t\t// Via env var.\n\t\tt.Setenv(tc.envVar, \"sk-env-key\")\n\t\tp2, err := buildNamedStubProvider(tc.name, map[string]any{})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%s with env var: unexpected error: %v\", tc.name, err)\n\t\t}\n\t\tif p2 == nil {\n\t\t\tt.Errorf(\"%s with env var: got nil provider\", tc.name)\n\t\t}\n\t\tt.Setenv(tc.envVar, \"\")\n\t}\n}\n\n// TestBuildNamedStubProvider_UnknownReturnsStub confirms that\n// buildNamedStubProvider accepts any provider name, returning a stub\n// without error for names that have no known canonical env var.\n// Fantasy/Catwalk performs auth and capability validation at runtime.\nfunc TestBuildNamedStubProvider_UnknownReturnsStub(t *testing.T) {\n\tp, err := buildNamedStubProvider(\"some_future_provider_xyz\", map[string]any{})\n\tif err != nil {\n\t\tt.Fatalf(\"unknown provider with no env var: want nil error, got %v\", err)\n\t}\n\tif p == nil {\n\t\tt.Fatal(\"expected non-nil provider stub\")\n\t}\n\tif p.Name() != \"some_future_provider_xyz\" {\n\t\tt.Errorf(\"Name() = %q, want %q\", p.Name(), \"some_future_provider_xyz\")\n\t}\n}\n\n// TestRequireAnyKey exercises the credential-check helper used by\n// buildNamedStubProvider.\nfunc TestRequireAnyKey(t *testing.T) {\n\tt.Setenv(\"ANTHROPIC_API_KEY\", \"\")\n\tt.Setenv(\"OPENAI_API_KEY\", \"\")\n\n\t// No opts, no env var → ErrAuth.\n\tif err := requireAnyKey(nil, \"ANTHROPIC_API_KEY\"); !errors.Is(err, provider.ErrAuth) {\n\t\tt.Errorf(\"no credential: want ErrAuth, got %v\", err)\n\t}\n\n\t// Empty string in opts[\"api_key\"] → ErrAuth.\n\tif err := requireAnyKey(map[string]any{\"api_key\": \"\"}, \"ANTHROPIC_API_KEY\"); !errors.Is(err, provider.ErrAuth) {\n\t\tt.Errorf(\"empty opts api_key: want ErrAuth, got %v\", err)\n\t}\n\n\t// Non-string value in opts[\"api_key\"] → ErrAuth.\n\tif err := requireAnyKey(map[string]any{\"api_key\": 42}, \"ANTHROPIC_API_KEY\"); !errors.Is(err, provider.ErrAuth) {\n\t\tt.Errorf(\"non-string opts api_key: want ErrAuth, got %v\", err)\n\t}\n\n\t// Valid string in opts[\"api_key\"] → nil.\n\tif err := requireAnyKey(map[string]any{\"api_key\": \"sk-test\"}, \"ANTHROPIC_API_KEY\"); err != nil {\n\t\tt.Errorf(\"valid opts api_key: want nil, got %v\", err)\n\t}\n\n\t// Env var set → nil even with no opts.\n\tt.Setenv(\"OPENAI_API_KEY\", \"sk-from-env\")\n\tif err := requireAnyKey(nil, \"OPENAI_API_KEY\"); err != nil {\n\t\tt.Errorf(\"env var set: want nil, got %v\", err)\n\t}\n}\n\n// newAuthMethodSelectModelForTest builds an authMethodSelectModel for the\n// given provider using exactly the same construction as pickAuthMethodInteractive\n// so that unit tests exercise the real list configuration.\nfunc newAuthMethodSelectModelForTest(providerName string) authMethodSelectModel {\n\toptions := authMethodOptions(providerName)\n\titems := make([]list.Item, 0, len(options))\n\tfor _, option := range options {\n\t\titems = append(items, authMethodSelectItem{option: option})\n\t}\n\n\tdelegate := list.NewDefaultDelegate()\n\tdelegate.SetSpacing(0)\n\tdelegate.Styles.SelectedTitle = delegate.Styles.SelectedTitle.Foreground(providerSelectAccentColor())\n\tdelegate.Styles.SelectedDesc = delegate.Styles.SelectedDesc.Foreground(providerSelectMutedColor())\n\n\tauthMethodListHeight := len(items)*2 + 5\n\tl := list.New(items, delegate, 72, authMethodListHeight)\n\tl.Title = \"Auth method for \" + providerName\n\tl.SetShowStatusBar(false)\n\tl.SetShowPagination(false)\n\tl.SetFilteringEnabled(false)\n\tl.SetShowHelp(true)\n\tl.KeyMap.ForceQuit.SetHelp(\"ctrl+c\", \"cancel\")\n\tl.AdditionalShortHelpKeys = func() []key.Binding { return nil }\n\n\treturn authMethodSelectModel{list: l}\n}\n\n// TestAuthMethodSelectModel_OpenAIShowsAllOptionsNoPagination verifies that\n// the two-option openai picker renders both choices on a single page and does\n// not show pagination dots.\n//\n// This is a regression test for HYGGE-28: with the old height formula and\n// showPagination=true, PerPage was calculated as 1, splitting the two options\n// across pages and hiding the OAuth choice behind a paginator.\nfunc TestAuthMethodSelectModel_OpenAIShowsAllOptionsNoPagination(t *testing.T) {\n\tm := newAuthMethodSelectModelForTest(\"openai\")\n\n\t// Confirm the list is on a single page.\n\tif m.list.Paginator.TotalPages != 1 {\n\t\tt.Errorf(\"TotalPages = %d, want 1 (both options must fit on one page)\", m.list.Paginator.TotalPages)\n\t}\n\tif m.list.Paginator.PerPage < 2 {\n\t\tt.Errorf(\"PerPage = %d, want >= 2 (must accommodate both openai auth options)\", m.list.Paginator.PerPage)\n\t}\n\n\t// Confirm both options appear in the rendered view.\n\tview := ansiEscapeRE.ReplaceAllString(m.View().Content, \"\")\n\tif !strings.Contains(view, \"API key\") {\n\t\tt.Errorf(\"rendered view missing 'API key':\\n%s\", view)\n\t}\n\tif !strings.Contains(view, \"OAuth\") {\n\t\tt.Errorf(\"rendered view missing 'OAuth' — option is still hidden behind pagination:\\n%s\", view)\n\t}\n\n\t// Pagination is disabled; ShowPagination() must return false.\n\tif m.list.ShowPagination() {\n\t\tt.Errorf(\"ShowPagination() = true; expected pagination to be disabled for small auth-method list\")\n\t}\n}\n\n// TestAuthMethodSelectModel_SingleProviderFitsNoPagination verifies that a\n// provider with only one auth method (e.g. anthropic) also has a single page\n// and no pagination noise.\nfunc TestAuthMethodSelectModel_SingleProviderFitsNoPagination(t *testing.T) {\n\tm := newAuthMethodSelectModelForTest(\"anthropic\")\n\n\tif m.list.Paginator.TotalPages != 1 {\n\t\tt.Errorf(\"TotalPages = %d, want 1\", m.list.Paginator.TotalPages)\n\t}\n\tview := ansiEscapeRE.ReplaceAllString(m.View().Content, \"\")\n\tif !strings.Contains(view, \"API key\") {\n\t\tt.Errorf(\"rendered view missing 'API key':\\n%s\", view)\n\t}\n\tif m.list.ShowPagination() {\n\t\tt.Errorf(\"ShowPagination() = true; expected pagination to be disabled for single-option list\")\n\t}\n}\n\n// TestAuthMethodSelectModel_EnterChoosesCurrentItem mirrors the provider\n// picker test for the auth method model.\nfunc TestAuthMethodSelectModel_EnterChoosesCurrentItem(t *testing.T) {\n\tm := newAuthMethodSelectModelForTest(\"openai\")\n\tupdated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))\n\tgot := updated.(authMethodSelectModel)\n\tif got.choice != authMethodAPIKey {\n\t\tt.Fatalf(\"choice got %q, want %q\", got.choice, authMethodAPIKey)\n\t}\n\tif got.cancelled {\n\t\tt.Fatalf(\"enter should not mark selection cancelled\")\n\t}\n}\n\n// TestAuthMethodSelectModel_CancelKeys verifies that ESC and ctrl+c cancel\n// the auth method picker.\nfunc TestAuthMethodSelectModel_CancelKeys(t *testing.T) {\n\tfor _, msg := range []tea.KeyPressMsg{\n\t\ttea.KeyPressMsg(tea.Key{Code: tea.KeyEsc}),\n\t\ttea.KeyPressMsg(tea.Key{Code: 'c', Text: \"c\", Mod: uv.ModCtrl}),\n\t} {\n\t\tm := newAuthMethodSelectModelForTest(\"openai\")\n\t\tupdated, _ := m.Update(msg)\n\t\tgot := updated.(authMethodSelectModel)\n\t\tif !got.cancelled {\n\t\t\tt.Fatalf(\"%s should cancel the auth method picker\", msg.Keystroke())\n\t\t}\n\t}\n}\n"}}}}