Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/billyhargroveofficial_billyharness.json
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
459 KiB
JSON

{"repo": "billyhargroveofficial/billyharness", "files": {"internal/config/summary_test.go": {"6c71fa1ddc7dc9237a8419cf86a73a6237cec5af": {"t": 1785745650.877908, "n": 839, "h": "df245ae692cc167d", "c": "package config\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestFormatSummaryUsesSanitizedValuesAndProvenance(t *testing.T) {\n\ttext := FormatSummary([]ResolvedValue{\n\t\t{Key: \"provider\", Value: \"deepseek\", Source: SourceEnvironment, SourceKey: \"FAST_AGENT_PROVIDER\"},\n\t\t{Key: \"model\", Value: \"deepseek-v4-flash\", Source: SourceGateway, SourceKey: \"model\"},\n\t\t{Key: \"api_key\", Value: \"sk-secret\", Redacted: true, Source: SourceEnvironment, SourceKey: \"DEEPSEEK_API_KEY\"},\n\t}, []string{\"sample warning\"})\n\tfor _, want := range []string{\"billyharness config\", \"provider:\", \"deepseek\", \"FAST_AGENT_PROVIDER\", \"model:\", \"deepseek-v4-flash\", \"sample warning\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"summary missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\tif strings.Contains(text, \"sk-secret\") {\n\t\tt.Fatalf(\"summary leaked secret:\\n%s\", text)\n\t}\n}\n"}, "86bb1f9a304261cbc9d3b2b3ebf5f5c5bfc91469": {"t": 1785755390.5248196, "n": 839, "h": "df245ae692cc167d", "c": "package config\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestFormatSummaryUsesSanitizedValuesAndProvenance(t *testing.T) {\n\ttext := FormatSummary([]ResolvedValue{\n\t\t{Key: \"provider\", Value: \"deepseek\", Source: SourceEnvironment, SourceKey: \"FAST_AGENT_PROVIDER\"},\n\t\t{Key: \"model\", Value: \"deepseek-v4-flash\", Source: SourceGateway, SourceKey: \"model\"},\n\t\t{Key: \"api_key\", Value: \"sk-secret\", Redacted: true, Source: SourceEnvironment, SourceKey: \"DEEPSEEK_API_KEY\"},\n\t}, []string{\"sample warning\"})\n\tfor _, want := range []string{\"billyharness config\", \"provider:\", \"deepseek\", \"FAST_AGENT_PROVIDER\", \"model:\", \"deepseek-v4-flash\", \"sample warning\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"summary missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\tif strings.Contains(text, \"sk-secret\") {\n\t\tt.Fatalf(\"summary leaked secret:\\n%s\", text)\n\t}\n}\n"}, "8e186bea271783c2ac5f1538412fba4ff7128242": {"t": 1786067427.5908535, "n": 839, "h": "df245ae692cc167d", "c": "package config\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestFormatSummaryUsesSanitizedValuesAndProvenance(t *testing.T) {\n\ttext := FormatSummary([]ResolvedValue{\n\t\t{Key: \"provider\", Value: \"deepseek\", Source: SourceEnvironment, SourceKey: \"FAST_AGENT_PROVIDER\"},\n\t\t{Key: \"model\", Value: \"deepseek-v4-flash\", Source: SourceGateway, SourceKey: \"model\"},\n\t\t{Key: \"api_key\", Value: \"sk-secret\", Redacted: true, Source: SourceEnvironment, SourceKey: \"DEEPSEEK_API_KEY\"},\n\t}, []string{\"sample warning\"})\n\tfor _, want := range []string{\"billyharness config\", \"provider:\", \"deepseek\", \"FAST_AGENT_PROVIDER\", \"model:\", \"deepseek-v4-flash\", \"sample warning\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"summary missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\tif strings.Contains(text, \"sk-secret\") {\n\t\tt.Fatalf(\"summary leaked secret:\\n%s\", text)\n\t}\n}\n"}}, "internal/credentials/credentials_test.go": {"6c71fa1ddc7dc9237a8419cf86a73a6237cec5af": {"t": 1785746628.93671, "n": 18228, "h": "6700625a8060518c", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/modelinfo\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestManagerResolvesQwenTokenPlanKeyWithoutExposingIt(t *testing.T) {\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"QWEN_TOKEN_PLAN_API_KEY\", \"sk-sp-test-qwen\")\n\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"QWEN_TOKEN_PLAN_API_KEY\"})\n\tqwen, err := manager.ResolveProviderAPIKey(modelinfo.ProviderQwen)\n\tif err != nil || qwen.Value != \"sk-sp-test-qwen\" {\n\t\tt.Fatalf(\"Qwen secret = %#v err=%v\", qwen, err)\n\t}\n\tstatus := CurrentStatusForRuntime(\n\t\tconfig.AuthSettings{APIKeyEnv: \"QWEN_TOKEN_PLAN_API_KEY\"},\n\t\tmodelinfo.ProviderQwen,\n\t\t\"qwen3.8-max-preview\",\n\t)\n\tformatted := FormatStatusText(status)\n\tif !status.Qwen.Configured ||\n\t\tstatus.DeepSeek.Configured ||\n\t\tstatus.CostMode != \"subscription\" ||\n\t\tstrings.Contains(formatted, qwen.Value) ||\n\t\t!strings.Contains(formatted, \"qwen: configured\") {\n\t\tt.Fatalf(\"Qwen status = %#v\\n%s\", status, formatted)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" ||\n\t\tstatus.DeepSeek.Provenance != \"env\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\", \"provenance=env\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyWritesExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit || status.Source != explicit || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"home .env should not be written, err=%v\", err)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-explicit-save\" || secret.Path != explicit || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyCreatesMissingExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\texplicit := filepath.Join(root, \"missing\", \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", filepath.Join(root, \"home\"))\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-missing-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-missing-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyFailsForReadOnlyExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tif err := os.WriteFile(explicit, []byte(\"OTHER=value\\n\"), 0o400); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\t_, err := manager.SaveDeepSeekAPIKey(\"sk-read-only-save\")\n\tif err == nil || !strings.Contains(err.Error(), explicit) || !strings.Contains(err.Error(), \"read-only\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tbody, readErr := os.ReadFile(explicit)\n\tif readErr != nil {\n\t\tt.Fatal(readErr)\n\t}\n\tif strings.Contains(string(body), \"sk-read-only-save\") || strings.Contains(string(body), \"DEEPSEEK_API_KEY\") {\n\t\tt.Fatalf(\"read-only env was modified: %q\", body)\n\t}\n\tif _, statErr := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(statErr) {\n\t\tt.Fatalf(\"home .env should not be fallback-written, err=%v\", statErr)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile || deepseek.Provenance != \"credential_file\" {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccessToken.Provenance != \"credential_file\" || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.Provenance != \"credential_file\" || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !fileModeMatches(info.Mode().Perm(), 0o600) {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != envPath || resolved.AccessToken.Provenance != \"dotenv\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.Source != envPath || resolved.AccountID.Provenance != \"dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestManagerSecretLookupUsesSharedDotenvPrecedence(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"DEEPSEEK_API_KEY=sk-from-dotenv\\nCODEX_ACCESS_TOKEN=codex-from-dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-from-credential-file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-dotenv\" || deepseek.Source != envPath || deepseek.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"deepseek shared lookup = %#v\", deepseek)\n\t}\n\tcodex := manager.ResolveCodexAuth()\n\tif codex.AccessToken.Value != \"codex-from-dotenv\" || codex.AccessToken.Source != envPath || codex.AccessToken.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"codex shared lookup = %#v\", codex)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc fileModeMatches(got, want os.FileMode) bool {\n\treturn got == want || runtime.GOOS == \"windows\"\n}\n"}, "ec42466d752ebff87f2c5493988fa067f0d2084d": {"t": 1785755389.708361, "n": 18228, "h": "6700625a8060518c", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/modelinfo\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestManagerResolvesQwenTokenPlanKeyWithoutExposingIt(t *testing.T) {\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"QWEN_TOKEN_PLAN_API_KEY\", \"sk-sp-test-qwen\")\n\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"QWEN_TOKEN_PLAN_API_KEY\"})\n\tqwen, err := manager.ResolveProviderAPIKey(modelinfo.ProviderQwen)\n\tif err != nil || qwen.Value != \"sk-sp-test-qwen\" {\n\t\tt.Fatalf(\"Qwen secret = %#v err=%v\", qwen, err)\n\t}\n\tstatus := CurrentStatusForRuntime(\n\t\tconfig.AuthSettings{APIKeyEnv: \"QWEN_TOKEN_PLAN_API_KEY\"},\n\t\tmodelinfo.ProviderQwen,\n\t\t\"qwen3.8-max-preview\",\n\t)\n\tformatted := FormatStatusText(status)\n\tif !status.Qwen.Configured ||\n\t\tstatus.DeepSeek.Configured ||\n\t\tstatus.CostMode != \"subscription\" ||\n\t\tstrings.Contains(formatted, qwen.Value) ||\n\t\t!strings.Contains(formatted, \"qwen: configured\") {\n\t\tt.Fatalf(\"Qwen status = %#v\\n%s\", status, formatted)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" ||\n\t\tstatus.DeepSeek.Provenance != \"env\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\", \"provenance=env\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyWritesExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit || status.Source != explicit || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"home .env should not be written, err=%v\", err)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-explicit-save\" || secret.Path != explicit || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyCreatesMissingExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\texplicit := filepath.Join(root, \"missing\", \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", filepath.Join(root, \"home\"))\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-missing-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-missing-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyFailsForReadOnlyExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tif err := os.WriteFile(explicit, []byte(\"OTHER=value\\n\"), 0o400); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\t_, err := manager.SaveDeepSeekAPIKey(\"sk-read-only-save\")\n\tif err == nil || !strings.Contains(err.Error(), explicit) || !strings.Contains(err.Error(), \"read-only\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tbody, readErr := os.ReadFile(explicit)\n\tif readErr != nil {\n\t\tt.Fatal(readErr)\n\t}\n\tif strings.Contains(string(body), \"sk-read-only-save\") || strings.Contains(string(body), \"DEEPSEEK_API_KEY\") {\n\t\tt.Fatalf(\"read-only env was modified: %q\", body)\n\t}\n\tif _, statErr := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(statErr) {\n\t\tt.Fatalf(\"home .env should not be fallback-written, err=%v\", statErr)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile || deepseek.Provenance != \"credential_file\" {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccessToken.Provenance != \"credential_file\" || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.Provenance != \"credential_file\" || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !fileModeMatches(info.Mode().Perm(), 0o600) {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != envPath || resolved.AccessToken.Provenance != \"dotenv\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.Source != envPath || resolved.AccountID.Provenance != \"dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestManagerSecretLookupUsesSharedDotenvPrecedence(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"DEEPSEEK_API_KEY=sk-from-dotenv\\nCODEX_ACCESS_TOKEN=codex-from-dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-from-credential-file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-dotenv\" || deepseek.Source != envPath || deepseek.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"deepseek shared lookup = %#v\", deepseek)\n\t}\n\tcodex := manager.ResolveCodexAuth()\n\tif codex.AccessToken.Value != \"codex-from-dotenv\" || codex.AccessToken.Source != envPath || codex.AccessToken.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"codex shared lookup = %#v\", codex)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc fileModeMatches(got, want os.FileMode) bool {\n\treturn got == want || runtime.GOOS == \"windows\"\n}\n"}, "36dba64d5a544d6cc2fbde98d6ba311e9686b404": {"t": 1785755391.279853, "n": 17273, "h": "30e39345f9755322", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" ||\n\t\tstatus.DeepSeek.Provenance != \"env\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\", \"provenance=env\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyWritesExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit || status.Source != explicit || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"home .env should not be written, err=%v\", err)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-explicit-save\" || secret.Path != explicit || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyCreatesMissingExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\texplicit := filepath.Join(root, \"missing\", \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", filepath.Join(root, \"home\"))\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-missing-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-missing-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyFailsForReadOnlyExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tif err := os.WriteFile(explicit, []byte(\"OTHER=value\\n\"), 0o400); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\t_, err := manager.SaveDeepSeekAPIKey(\"sk-read-only-save\")\n\tif err == nil || !strings.Contains(err.Error(), explicit) || !strings.Contains(err.Error(), \"read-only\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tbody, readErr := os.ReadFile(explicit)\n\tif readErr != nil {\n\t\tt.Fatal(readErr)\n\t}\n\tif strings.Contains(string(body), \"sk-read-only-save\") || strings.Contains(string(body), \"DEEPSEEK_API_KEY\") {\n\t\tt.Fatalf(\"read-only env was modified: %q\", body)\n\t}\n\tif _, statErr := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(statErr) {\n\t\tt.Fatalf(\"home .env should not be fallback-written, err=%v\", statErr)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile || deepseek.Provenance != \"credential_file\" {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccessToken.Provenance != \"credential_file\" || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.Provenance != \"credential_file\" || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !fileModeMatches(info.Mode().Perm(), 0o600) {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != envPath || resolved.AccessToken.Provenance != \"dotenv\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.Source != envPath || resolved.AccountID.Provenance != \"dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestManagerSecretLookupUsesSharedDotenvPrecedence(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"DEEPSEEK_API_KEY=sk-from-dotenv\\nCODEX_ACCESS_TOKEN=codex-from-dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-from-credential-file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-dotenv\" || deepseek.Source != envPath || deepseek.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"deepseek shared lookup = %#v\", deepseek)\n\t}\n\tcodex := manager.ResolveCodexAuth()\n\tif codex.AccessToken.Value != \"codex-from-dotenv\" || codex.AccessToken.Source != envPath || codex.AccessToken.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"codex shared lookup = %#v\", codex)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc fileModeMatches(got, want os.FileMode) bool {\n\treturn got == want || runtime.GOOS == \"windows\"\n}\n"}, "04e2c212c0c98a1cbdf3ab3278bafcb7fe9c8ec4": {"t": 1785755392.939687, "n": 17143, "h": "2d95e1246e19eb70", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" ||\n\t\tstatus.DeepSeek.Provenance != \"env\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\", \"provenance=env\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyWritesExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit || status.Source != explicit || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"home .env should not be written, err=%v\", err)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-explicit-save\" || secret.Path != explicit || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyCreatesMissingExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\texplicit := filepath.Join(root, \"missing\", \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", filepath.Join(root, \"home\"))\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-missing-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-missing-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyFailsForReadOnlyExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tif err := os.WriteFile(explicit, []byte(\"OTHER=value\\n\"), 0o400); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\t_, err := manager.SaveDeepSeekAPIKey(\"sk-read-only-save\")\n\tif err == nil || !strings.Contains(err.Error(), explicit) || !strings.Contains(err.Error(), \"read-only\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tbody, readErr := os.ReadFile(explicit)\n\tif readErr != nil {\n\t\tt.Fatal(readErr)\n\t}\n\tif strings.Contains(string(body), \"sk-read-only-save\") || strings.Contains(string(body), \"DEEPSEEK_API_KEY\") {\n\t\tt.Fatalf(\"read-only env was modified: %q\", body)\n\t}\n\tif _, statErr := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(statErr) {\n\t\tt.Fatalf(\"home .env should not be fallback-written, err=%v\", statErr)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile || deepseek.Provenance != \"credential_file\" {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccessToken.Provenance != \"credential_file\" || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.Provenance != \"credential_file\" || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != envPath || resolved.AccessToken.Provenance != \"dotenv\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.Source != envPath || resolved.AccountID.Provenance != \"dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestManagerSecretLookupUsesSharedDotenvPrecedence(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"DEEPSEEK_API_KEY=sk-from-dotenv\\nCODEX_ACCESS_TOKEN=codex-from-dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-from-credential-file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-dotenv\" || deepseek.Source != envPath || deepseek.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"deepseek shared lookup = %#v\", deepseek)\n\t}\n\tcodex := manager.ResolveCodexAuth()\n\tif codex.AccessToken.Value != \"codex-from-dotenv\" || codex.AccessToken.Source != envPath || codex.AccessToken.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"codex shared lookup = %#v\", codex)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n"}, "937255863e6a8b80564d754312dd6a378481b55b": {"t": 1785755394.3244202, "n": 14123, "h": "66e4d3b0562f234f", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" ||\n\t\tstatus.DeepSeek.Provenance != \"env\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\", \"provenance=env\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile || deepseek.Provenance != \"credential_file\" {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccessToken.Provenance != \"credential_file\" || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.Provenance != \"credential_file\" || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != envPath || resolved.AccessToken.Provenance != \"dotenv\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.Source != envPath || resolved.AccountID.Provenance != \"dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestManagerSecretLookupUsesSharedDotenvPrecedence(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"DEEPSEEK_API_KEY=sk-from-dotenv\\nCODEX_ACCESS_TOKEN=codex-from-dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-from-credential-file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-dotenv\" || deepseek.Source != envPath || deepseek.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"deepseek shared lookup = %#v\", deepseek)\n\t}\n\tcodex := manager.ResolveCodexAuth()\n\tif codex.AccessToken.Value != \"codex-from-dotenv\" || codex.AccessToken.Source != envPath || codex.AccessToken.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"codex shared lookup = %#v\", codex)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n"}, "160c8b910be3dc3d89e5a7f3d635607bf6d789b5": {"t": 1785755396.201026, "n": 12335, "h": "eea97f653bd55e2e", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != \".env\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n"}, "b9d56de539bd66b7f338f0b7a3cb0ef865e8160a": {"t": 1785755397.5409324, "n": 10518, "h": "d8088d4fd6e2047e", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != \".env\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n"}, "a826567afa288a16c578c9e7b8988e41faf1ff2e": {"t": 1785755399.9557889, "n": 8810, "h": "8f085bace625be48", "c": "package credentials\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManager(config.Config{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManager(config.Config{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuth(config.Default(), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSON(config.Default(), json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManager(config.Config{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != \".env\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc testJWT(t *testing.T, claims map[string]any) string {\n\tt.Helper()\n\tencode := func(v any) string {\n\t\traw, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn base64.RawURLEncoding.EncodeToString(raw)\n\t}\n\treturn encode(map[string]string{\"alg\": \"none\", \"typ\": \"JWT\"}) + \".\" + encode(claims) + \".sig\"\n}\n"}, "ae8353ff3214fba25e260e49d53e5f0cb09e7702": {"t": 1785755402.346644, "n": 7193, "h": "5fab99e434c406a2", "c": "package credentials\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManager(config.Config{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuth(config.Default(), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSON(config.Default(), json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManager(config.Config{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != \".env\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc testJWT(t *testing.T, claims map[string]any) string {\n\tt.Helper()\n\tencode := func(v any) string {\n\t\traw, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn base64.RawURLEncoding.EncodeToString(raw)\n\t}\n\treturn encode(map[string]string{\"alg\": \"none\", \"typ\": \"JWT\"}) + \".\" + encode(claims) + \".sig\"\n}\n"}, "d3c9a634ddc6b9654cc4bc25ee588507cb37bc29": {"t": 1785755404.9772682, "n": 6172, "h": "69af325759085db5", "c": "package credentials\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManager(config.Config{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuth(config.Default(), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSON(config.Default(), json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc testJWT(t *testing.T, claims map[string]any) string {\n\tt.Helper()\n\tencode := func(v any) string {\n\t\traw, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn base64.RawURLEncoding.EncodeToString(raw)\n\t}\n\treturn encode(map[string]string{\"alg\": \"none\", \"typ\": \"JWT\"}) + \".\" + encode(claims) + \".sig\"\n}\n"}, "f6b68cb600b24396b90269fe5935be230277eccc": {"t": 1785755407.308001, "n": 4894, "h": "10f0f2b456cdc7e9", "c": "package credentials\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManager(config.Config{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuth(config.Default(), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSON(config.Default(), json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc testJWT(t *testing.T, claims map[string]any) string {\n\tt.Helper()\n\tencode := func(v any) string {\n\t\traw, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn base64.RawURLEncoding.EncodeToString(raw)\n\t}\n\treturn encode(map[string]string{\"alg\": \"none\", \"typ\": \"JWT\"}) + \".\" + encode(claims) + \".sig\"\n}\n"}, "e2d8776010532865df5b7bce2ee1a27a35096bf8": {"t": 1785755408.441808, "n": 4017, "h": "37a4851ea115a9c4", "c": "package credentials\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuth(config.Default(), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSON(config.Default(), json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatus(config.Default())\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatus(config.Default())\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc testJWT(t *testing.T, claims map[string]any) string {\n\tt.Helper()\n\tencode := func(v any) string {\n\t\traw, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn base64.RawURLEncoding.EncodeToString(raw)\n\t}\n\treturn encode(map[string]string{\"alg\": \"none\", \"typ\": \"JWT\"}) + \".\" + encode(claims) + \".sig\"\n}\n"}, "264f6d7160ea3283e3a1820057fd234d6cc2fbeb": {"t": 1785755409.776535, "n": 2751, "h": "116a025adc6d9b90", "c": "package credentials\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testJWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuth(config.Default(), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif info.Mode().Perm() != 0o600 {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSON(config.Default(), json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc testJWT(t *testing.T, claims map[string]any) string {\n\tt.Helper()\n\tencode := func(v any) string {\n\t\traw, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn base64.RawURLEncoding.EncodeToString(raw)\n\t}\n\treturn encode(map[string]string{\"alg\": \"none\", \"typ\": \"JWT\"}) + \".\" + encode(claims) + \".sig\"\n}\n"}, "8e186bea271783c2ac5f1538412fba4ff7128242": {"t": 1786067846.4416986, "n": 18228, "h": "6700625a8060518c", "c": "package credentials\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/modelinfo\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/testkit\"\n)\n\nfunc TestSaveDeepSeekAPIKeyWritesBillyDotenv(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\n\tstatus, err := SaveDeepSeekAPIKey(\"sk-test-value\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-test-value\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tif got := DeepSeekStatus(); !got.Configured || got.Source != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"DeepSeekStatus = %#v\", got)\n\t}\n}\n\nfunc TestManagerResolveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-custom-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-value\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" || secret.Path != filepath.Join(root, \".env\") || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tstatus := manager.DeepSeekStatus()\n\tif !status.Configured || status.Source != filepath.Join(root, \".env\") || status.Path != filepath.Join(root, \".env\") || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestManagerResolvesQwenTokenPlanKeyWithoutExposingIt(t *testing.T) {\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"QWEN_TOKEN_PLAN_API_KEY\", \"sk-sp-test-qwen\")\n\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"QWEN_TOKEN_PLAN_API_KEY\"})\n\tqwen, err := manager.ResolveProviderAPIKey(modelinfo.ProviderQwen)\n\tif err != nil || qwen.Value != \"sk-sp-test-qwen\" {\n\t\tt.Fatalf(\"Qwen secret = %#v err=%v\", qwen, err)\n\t}\n\tstatus := CurrentStatusForRuntime(\n\t\tconfig.AuthSettings{APIKeyEnv: \"QWEN_TOKEN_PLAN_API_KEY\"},\n\t\tmodelinfo.ProviderQwen,\n\t\t\"qwen3.8-max-preview\",\n\t)\n\tformatted := FormatStatusText(status)\n\tif !status.Qwen.Configured ||\n\t\tstatus.DeepSeek.Configured ||\n\t\tstatus.CostMode != \"subscription\" ||\n\t\tstrings.Contains(formatted, qwen.Value) ||\n\t\t!strings.Contains(formatted, \"qwen: configured\") {\n\t\tt.Fatalf(\"Qwen status = %#v\\n%s\", status, formatted)\n\t}\n}\n\nfunc TestAuthStatusClassifiesAndRedactsCredentials(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"sk-classified-secret\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-codex-secret\")\n\n\tstatus := CurrentStatusForRuntime(config.AuthSettings{}, \"deepseek\", \"gpt-5.5\")\n\tif status.ActiveProvider != \"openai-codex\" || status.ActiveModel != \"gpt-5.5\" || status.CostMode != \"subscription\" {\n\t\tt.Fatalf(\"runtime status = %#v\", status)\n\t}\n\tif status.DeepSeek.Provider != \"deepseek\" || status.DeepSeek.AuthType != \"api-key\" ||\n\t\tstatus.DeepSeek.Status != \"configured\" || status.DeepSeek.Credential != \"redacted\" ||\n\t\tstatus.DeepSeek.Provenance != \"env\" {\n\t\tt.Fatalf(\"deepseek classified status = %#v\", status.DeepSeek)\n\t}\n\tif status.Codex.Provider != \"codex\" || status.Codex.AuthType != \"codex-oauth\" ||\n\t\tstatus.Codex.Status != \"configured\" || status.Codex.Credential != \"redacted\" ||\n\t\tstatus.Codex.Mode != \"personalAccessToken\" {\n\t\tt.Fatalf(\"codex classified status = %#v\", status.Codex)\n\t}\n\ttext := FormatStatusText(status)\n\tfor _, leak := range []string{\"sk-classified-secret\", \"at-codex-secret\"} {\n\t\tif strings.Contains(text, leak) {\n\t\t\tt.Fatalf(\"formatted status leaked %q:\\n%s\", leak, text)\n\t\t}\n\t}\n\tfor _, want := range []string{\"runtime: provider=openai-codex model=gpt-5.5 cost_mode=subscription\", \"auth=api-key\", \"auth=codex-oauth\", \"credential=redacted\", \"provenance=env\"} {\n\t\tif !strings.Contains(text, want) {\n\t\t\tt.Fatalf(\"formatted status missing %q:\\n%s\", want, text)\n\t\t}\n\t}\n\n\tdeepseekRuntime := CurrentStatusForRuntime(config.AuthSettings{}, \"openai-codex\", \"deepseek-v4-flash\")\n\tif deepseekRuntime.ActiveProvider != \"deepseek\" || deepseekRuntime.CostMode != \"metered\" {\n\t\tt.Fatalf(\"deepseek runtime status = %#v\", deepseekRuntime)\n\t}\n}\n\nfunc TestNewManagerFromAuthSettingsUsesProjection(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"PROJECTED_DEEPSEEK_KEY=sk-projected\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthFile := filepath.Join(root, \"auth\", \"projected-codex.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{\n\t\tAPIKeyEnv: \"PROJECTED_DEEPSEEK_KEY\",\n\t\tCodexAuthFile: authFile,\n\t})\n\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-projected\" || secret.EnvVar != \"PROJECTED_DEEPSEEK_KEY\" || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n\tif got := manager.CodexAuthFilePath(); got != authFile {\n\t\tt.Fatalf(\"CodexAuthFilePath = %q\", got)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyUsesConfiguredEnvName(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{APIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\"})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-custom-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != filepath.Join(root, \".env\") {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(filepath.Join(root, \".env\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"CUSTOM_DEEPSEEK_KEY=sk-custom-save\" {\n\t\tt.Fatalf(\".env = %q\", body)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-custom-save\" || secret.EnvVar != \"CUSTOM_DEEPSEEK_KEY\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyWritesExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit || status.Source != explicit || status.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n\tif _, err := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(err) {\n\t\tt.Fatalf(\"home .env should not be written, err=%v\", err)\n\t}\n\tsecret, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif secret.Value != \"sk-explicit-save\" || secret.Path != explicit || secret.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"secret = %#v\", secret)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyCreatesMissingExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\texplicit := filepath.Join(root, \"missing\", \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", filepath.Join(root, \"home\"))\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\tstatus, err := manager.SaveDeepSeekAPIKey(\"sk-missing-explicit-save\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !status.Configured || status.Path != explicit {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tbody, err := os.ReadFile(explicit)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif strings.TrimSpace(string(body)) != \"DEEPSEEK_API_KEY=sk-missing-explicit-save\" {\n\t\tt.Fatalf(\"explicit env = %q\", body)\n\t}\n}\n\nfunc TestManagerSaveDeepSeekAPIKeyFailsForReadOnlyExplicitEnvFile(t *testing.T) {\n\troot := t.TempDir()\n\thome := filepath.Join(root, \"home\")\n\texplicit := filepath.Join(root, \"runtime.env\")\n\tt.Setenv(\"BILLYHARNESS_HOME\", home)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", explicit)\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"\")\n\tif err := os.WriteFile(explicit, []byte(\"OTHER=value\\n\"), 0o400); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{})\n\n\t_, err := manager.SaveDeepSeekAPIKey(\"sk-read-only-save\")\n\tif err == nil || !strings.Contains(err.Error(), explicit) || !strings.Contains(err.Error(), \"read-only\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tbody, readErr := os.ReadFile(explicit)\n\tif readErr != nil {\n\t\tt.Fatal(readErr)\n\t}\n\tif strings.Contains(string(body), \"sk-read-only-save\") || strings.Contains(string(body), \"DEEPSEEK_API_KEY\") {\n\t\tt.Fatalf(\"read-only env was modified: %q\", body)\n\t}\n\tif _, statErr := os.Stat(filepath.Join(home, \".env\")); !os.IsNotExist(statErr) {\n\t\tt.Fatalf(\"home .env should not be fallback-written, err=%v\", statErr)\n\t}\n}\n\nfunc TestManagerResolvesCredentialFileSecrets(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-token-from-file\",\n\t\t\"codex_account_id\": \"acct_file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-credential-file\" || deepseek.Source != credentialFile || deepseek.Path != credentialFile || deepseek.Provenance != \"credential_file\" {\n\t\tt.Fatalf(\"deepseek secret = %#v\", deepseek)\n\t}\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"codex-token-from-file\" || resolved.AccessToken.Source != credentialFile || resolved.AccessToken.Provenance != \"credential_file\" || resolved.AccountID.Value != \"acct_file\" {\n\t\tt.Fatalf(\"codex resolution = %#v\", resolved)\n\t}\n\tstatus := manager.CodexStatus()\n\tif !status.Configured || status.Source != credentialFile || status.Provenance != \"credential_file\" || status.AccountID != \"acct_file\" {\n\t\tt.Fatalf(\"codex status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"codex-token-from-file\") || strings.Contains(status.Path, \"codex-token-from-file\") {\n\t\tt.Fatalf(\"status leaked credential file token: %#v\", status)\n\t}\n}\n\nfunc TestImportCodexAuthCopiesOAuthJSONToBillyHome(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tcodexHome := filepath.Join(root, \"codex\")\n\tif err := os.MkdirAll(codexHome, 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\texp := time.Now().Add(time.Hour).Unix()\n\tsource := filepath.Join(codexHome, \"auth.json\")\n\tif err := writeJSON(source, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_test\"}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Setenv(\"CODEX_HOME\", codexHome)\n\n\tstatus, err := ImportCodexAuthFromAuthSettings(config.AuthSettings{}, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantPath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif !status.Configured || status.Path != wantPath || status.Source != source || status.AccountID != \"acct_test\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tinfo, err := os.Stat(wantPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !fileModeMatches(info.Mode().Perm(), 0o600) {\n\t\tt.Fatalf(\"mode = %v\", info.Mode().Perm())\n\t}\n}\n\nfunc TestSaveCodexAuthJSONRejectsMissingTokens(t *testing.T) {\n\tt.Setenv(\"BILLYHARNESS_HOME\", t.TempDir())\n\t_, err := SaveCodexAuthJSONFromAuthSettings(config.AuthSettings{}, json.RawMessage(`{\"auth_mode\":\"chatgpt\"}`))\n\tif err == nil || !strings.Contains(err.Error(), \"does not contain OAuth tokens\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestCodexStatusSeesEnvAccessToken(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\texp := time.Now().Add(time.Hour).Unix()\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", testkit.JWT(t, map[string]any{\"exp\": exp, \"chatgpt_account_id\": \"acct_env\"}))\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Source != \"env:CODEX_ACCESS_TOKEN\" || status.AccountID != \"acct_env\" || status.Mode != \"accessToken\" || status.Refresh != \"fresh\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif status.ExpiresAt == \"\" {\n\t\tt.Fatalf(\"expires_at missing: %#v\", status)\n\t}\n}\n\nfunc TestManagerResolveCodexAuthUsesSharedSources(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"CODEX_ACCESS_TOKEN=token-from-dotenv\\nCODEX_CHATGPT_ACCOUNT_ID=acct_dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tauthPath := filepath.Join(root, \"custom-auth.json\")\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CodexAuthFile: authPath})\n\n\tresolved := manager.ResolveCodexAuth()\n\tif resolved.AccessToken.Value != \"token-from-dotenv\" || resolved.AccessToken.Source != envPath || resolved.AccessToken.Provenance != \"dotenv\" || resolved.AccessToken.EnvVar != CodexAccessTokenEnv {\n\t\tt.Fatalf(\"access token source = %#v\", resolved.AccessToken)\n\t}\n\tif resolved.AccountID.Value != \"acct_dotenv\" || resolved.AccountID.Source != envPath || resolved.AccountID.Provenance != \"dotenv\" || resolved.AccountID.EnvVar != CodexAccountIDEnv {\n\t\tt.Fatalf(\"account source = %#v\", resolved.AccountID)\n\t}\n\tif resolved.AuthFile != authPath {\n\t\tt.Fatalf(\"auth file = %q\", resolved.AuthFile)\n\t}\n}\n\nfunc TestManagerSecretLookupUsesSharedDotenvPrecedence(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"DEEPSEEK_API_KEY\", \"\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\tt.Setenv(\"CODEX_CHATGPT_ACCOUNT_ID\", \"\")\n\tenvPath := filepath.Join(root, \".env\")\n\tif err := os.WriteFile(envPath, []byte(\"DEEPSEEK_API_KEY=sk-from-dotenv\\nCODEX_ACCESS_TOKEN=codex-from-dotenv\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcredentialFile := filepath.Join(root, \"auth\", \"credentials.json\")\n\tif err := os.MkdirAll(filepath.Dir(credentialFile), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(credentialFile, map[string]any{\n\t\t\"deepseek_api_key\": \"sk-from-credential-file\",\n\t\t\"codex_access_token\": \"codex-from-credential-file\",\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tmanager := NewManagerFromAuthSettings(config.AuthSettings{CredentialFile: credentialFile})\n\n\tdeepseek, err := manager.ResolveDeepSeekAPIKey()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif deepseek.Value != \"sk-from-dotenv\" || deepseek.Source != envPath || deepseek.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"deepseek shared lookup = %#v\", deepseek)\n\t}\n\tcodex := manager.ResolveCodexAuth()\n\tif codex.AccessToken.Value != \"codex-from-dotenv\" || codex.AccessToken.Source != envPath || codex.AccessToken.Provenance != \"dotenv\" {\n\t\tt.Fatalf(\"codex shared lookup = %#v\", codex)\n\t}\n}\n\nfunc TestCodexStatusShowsRefreshStateForAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\n\t\t\"auth_mode\": \"chatgpt\",\n\t\t\"tokens\": map[string]any{\n\t\t\t\"access_token\": testkit.JWT(t, map[string]any{\"exp\": time.Now().Add(-time.Hour).Unix()}),\n\t\t\t\"refresh_token\": \"refresh-secret\",\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Refresh != \"refresh_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n\tif strings.Contains(status.Source, \"refresh-secret\") || strings.Contains(status.Path, \"refresh-secret\") {\n\t\tt.Fatalf(\"status leaked refresh token: %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusPATDoesNotNeedRefresh(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"at-secret\")\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif !status.Configured || status.Mode != \"personalAccessToken\" || status.Refresh != \"not_required\" {\n\t\tt.Fatalf(\"status = %#v\", status)\n\t}\n}\n\nfunc TestCodexStatusDoesNotConfigureEmptyAuthFile(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", filepath.Join(root, \"missing.env\"))\n\tpath := filepath.Join(root, \"auth\", \"codex.json\")\n\tif err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeJSON(path, map[string]any{\"auth_mode\": \"chatgpt\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstatus := CodexStatusFromAuthSettings(config.AuthSettings{})\n\tif status.Configured || status.Source != \"\" {\n\t\tt.Fatalf(\"empty auth file should not be configured: %#v\", status)\n\t}\n\tif status.Path != path || status.Mode != \"chatgpt\" {\n\t\tt.Fatalf(\"status should still report path/mode: %#v\", status)\n\t}\n}\n\nfunc writeJSON(path string, value any) error {\n\tbody, err := json.MarshalIndent(value, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(path, append(body, '\\n'), 0o600)\n}\n\nfunc fileModeMatches(got, want os.FileMode) bool {\n\treturn got == want || runtime.GOOS == \"windows\"\n}\n"}}, "internal/provider/provider_test.go": {"6c71fa1ddc7dc9237a8419cf86a73a6237cec5af": {"t": 1785746628.978479, "n": 28682, "h": "ad8f6c5879927a30", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/modelinfo\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestQwenBodyUsesAlwaysOnThinkingAndCompletionBudget(t *testing.T) {\n\ttemp := 0.7\n\tqwen := &DeepSeek{\n\t\tProviderID: modelinfo.ProviderQwen,\n\t\tThinking: \"disabled\",\n\t\tReasoningEffort: \"high\",\n\t\tMaxTokens: 4096,\n\t\tMaxParallelTools: 4,\n\t}\n\tbody, err := qwen.body(Request{\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"first\"},\n\t\t\t{Role: protocol.RoleAssistant, Content: \"answer\", ReasoningContent: \"historical reasoning\"},\n\t\t\t{Role: protocol.RoleUser, Content: \"continue\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{{\n\t\t\tName: \"time_now\",\n\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t}},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"enable_thinking\"] != true ||\n\t\tpayload[\"preserve_thinking\"] != true ||\n\t\tpayload[\"reasoning_effort\"] != \"xhigh\" ||\n\t\tpayload[\"max_completion_tokens\"] != float64(4096) ||\n\t\tpayload[\"parallel_tool_calls\"] != true {\n\t\tt.Fatalf(\"Qwen payload = %s\", body)\n\t}\n\tfor _, forbidden := range []string{\"thinking\", \"temperature\", \"max_tokens\"} {\n\t\tif _, ok := payload[forbidden]; ok {\n\t\t\tt.Fatalf(\"Qwen payload contains %q: %s\", forbidden, body)\n\t\t}\n\t}\n\tmessages, ok := payload[\"messages\"].([]any)\n\tif !ok || len(messages) != 3 {\n\t\tt.Fatalf(\"Qwen messages = %#v\", payload[\"messages\"])\n\t}\n\tassistant, ok := messages[1].(map[string]any)\n\tif !ok || assistant[\"reasoning_content\"] != \"historical reasoning\" {\n\t\tt.Fatalf(\"Qwen historical assistant message = %#v\", messages[1])\n\t}\n}\n\nfunc TestNewFromBindingBuildsOfficialQwenTokenPlanProvider(t *testing.T) {\n\tt.Setenv(\"QWEN_TOKEN_PLAN_API_KEY\", \"sk-sp-qwen-test\")\n\tcfg := config.BuiltIn()\n\tcfg.Provider = modelinfo.ProviderQwen\n\tcfg.Model = \"qwen3.8-max-preview\"\n\tcfg.ProviderMaxRetries = 0\n\tt.Setenv(\"OVERRIDE_QWEN_KEY\", \"sk-sp-wrong-key\")\n\tbinding := cfg.ProviderBinding()\n\tbinding.Provider.BaseURL = \"https://override.invalid/v1\"\n\tbinding.Auth.APIKeyEnv = \"OVERRIDE_QWEN_KEY\"\n\n\tprov, err := NewFromBinding(binding)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tqwen, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek OpenAI-compatible transport\", prov)\n\t}\n\tif qwen.ProviderID != modelinfo.ProviderQwen ||\n\t\tqwen.BaseURL != \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\" ||\n\t\tqwen.APIKey != \"sk-sp-qwen-test\" ||\n\t\tqwen.Model != \"qwen3.8-max-preview\" ||\n\t\tqwen.MaxParallelTools != cfg.MaxParallelTools ||\n\t\tqwen.MaxRetries != 0 {\n\t\tt.Fatalf(\"Qwen provider = %#v\", qwen)\n\t}\n}\n\nfunc TestQwenStreamUsesChatCompletionsBearerAndEmitsAccounting(t *testing.T) {\n\ttype capturedRequest struct {\n\t\tMethod string\n\t\tPath string\n\t\tAuthorization string\n\t\tBody []byte\n\t}\n\tcaptured := make(chan capturedRequest, 1)\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, _ := io.ReadAll(r.Body)\n\t\tcaptured <- capturedRequest{\n\t\t\tMethod: r.Method,\n\t\t\tPath: r.URL.Path,\n\t\t\tAuthorization: r.Header.Get(\"Authorization\"),\n\t\t\tBody: body,\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"qwen-request-1\")\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\tqwen := &DeepSeek{\n\t\tProviderID: modelinfo.ProviderQwen,\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"sk-sp-qwen-secret\",\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tThinking: \"disabled\",\n\t\tReasoningEffort: \"medium\",\n\t\tMaxTokens: 1024,\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 0,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := qwen.Stream(context.Background(), Request{\n\t\tRequestID: \"local-qwen-request\",\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar metadata RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmetadata = event.Request\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequest := <-captured\n\tif request.Method != http.MethodPost ||\n\t\trequest.Path != \"/chat/completions\" ||\n\t\trequest.Authorization != \"Bearer sk-sp-qwen-secret\" {\n\t\tt.Fatalf(\"Qwen request = %#v\", request)\n\t}\n\tvar payload map[string]any\n\tif err := json.Unmarshal(request.Body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"enable_thinking\"] != true ||\n\t\tpayload[\"preserve_thinking\"] != true ||\n\t\tpayload[\"reasoning_effort\"] != \"medium\" ||\n\t\tpayload[\"max_completion_tokens\"] != float64(1024) {\n\t\tt.Fatalf(\"Qwen HTTP payload = %s\", request.Body)\n\t}\n\tif metadata.RequestID != \"local-qwen-request\" ||\n\t\tmetadata.ProviderID != modelinfo.ProviderQwen ||\n\t\tmetadata.ModelID != \"qwen3.8-max-preview\" ||\n\t\tmetadata.ProviderRequestID != \"qwen-request-1\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"Qwen request metadata = %#v\", metadata)\n\t}\n}\n\nfunc TestDeepSeekStreamRejectsImageInputBeforeHTTP(t *testing.T) {\n\tvar called atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {\n\t\tcalled.Add(1)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{\n\t\t\tRole: protocol.RoleUser,\n\t\t\tContent: \"look\",\n\t\t\tParts: []protocol.MessagePart{\n\t\t\t\tprotocol.TextPart(\"look\"),\n\t\t\t\tprotocol.AttachmentPart(protocol.AttachmentRef{ID: \"att_test\", Kind: protocol.AttachmentKindImage}),\n\t\t\t},\n\t\t}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tif err == nil || !strings.Contains(err.Error(), \"image input is required\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tif called.Load() != 0 {\n\t\tt.Fatalf(\"DeepSeek HTTP server was called %d time(s)\", called.Load())\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewFromBindingRejectsUnsupportedCapabilityPolicyBeforeCredentials(t *testing.T) {\n\t_, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"enabled\",\n\t\t\tReasoningEffort: \"warp\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported reasoning_effort\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\n\t_, err = NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tMaxTokens: 9000,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"max_output_tokens=9000\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestDrainStreamNormalCompletion(t *testing.T) {\n\tevents := make(chan Event, 2)\n\terrs := make(chan error)\n\tevents <- Event{Kind: EventContent, Text: \"hello\"}\n\tevents <- Event{Kind: EventDone}\n\tclose(events)\n\tclose(errs)\n\n\tvar got []EventKind\n\tif err := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot = append(got, event.Kind)\n\t\t\treturn nil\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(got) != 2 || got[0] != EventContent || got[1] != EventDone {\n\t\tt.Fatalf(\"events = %#v\", got)\n\t}\n}\n\nfunc TestDrainStreamReturnsErrorAfterPartialEvents(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error, 1)\n\twantErr := errors.New(\"stream exploded\")\n\tseen := make(chan struct{})\n\tgo func() {\n\t\tevents <- Event{Kind: EventContent, Text: \"partial\"}\n\t\t<-seen\n\t\terrs <- wantErr\n\t}()\n\n\tvar got strings.Builder\n\terr := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot.WriteString(event.Text)\n\t\t\tclose(seen)\n\t\t\treturn nil\n\t\t},\n\t})\n\tif !errors.Is(err, wantErr) {\n\t\tt.Fatalf(\"err = %v, want %v\", err, wantErr)\n\t}\n\tif got.String() != \"partial\" {\n\t\tt.Fatalf(\"partial events = %q\", got.String())\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenEventsNeverClose(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\terr := DrainStream(ctx, make(chan Event), make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.Canceled) {\n\t\tt.Fatalf(\"err = %v, want context canceled\", err)\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenErrChannelBlocks(t *testing.T) {\n\tevents := make(chan Event)\n\tclose(events)\n\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)\n\tdefer cancel()\n\terr := DrainStream(ctx, events, make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\tt.Fatalf(\"err = %v, want deadline exceeded\", err)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "ec42466d752ebff87f2c5493988fa067f0d2084d": {"t": 1785755389.8909655, "n": 28682, "h": "ad8f6c5879927a30", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/modelinfo\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestQwenBodyUsesAlwaysOnThinkingAndCompletionBudget(t *testing.T) {\n\ttemp := 0.7\n\tqwen := &DeepSeek{\n\t\tProviderID: modelinfo.ProviderQwen,\n\t\tThinking: \"disabled\",\n\t\tReasoningEffort: \"high\",\n\t\tMaxTokens: 4096,\n\t\tMaxParallelTools: 4,\n\t}\n\tbody, err := qwen.body(Request{\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"first\"},\n\t\t\t{Role: protocol.RoleAssistant, Content: \"answer\", ReasoningContent: \"historical reasoning\"},\n\t\t\t{Role: protocol.RoleUser, Content: \"continue\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{{\n\t\t\tName: \"time_now\",\n\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t}},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"enable_thinking\"] != true ||\n\t\tpayload[\"preserve_thinking\"] != true ||\n\t\tpayload[\"reasoning_effort\"] != \"xhigh\" ||\n\t\tpayload[\"max_completion_tokens\"] != float64(4096) ||\n\t\tpayload[\"parallel_tool_calls\"] != true {\n\t\tt.Fatalf(\"Qwen payload = %s\", body)\n\t}\n\tfor _, forbidden := range []string{\"thinking\", \"temperature\", \"max_tokens\"} {\n\t\tif _, ok := payload[forbidden]; ok {\n\t\t\tt.Fatalf(\"Qwen payload contains %q: %s\", forbidden, body)\n\t\t}\n\t}\n\tmessages, ok := payload[\"messages\"].([]any)\n\tif !ok || len(messages) != 3 {\n\t\tt.Fatalf(\"Qwen messages = %#v\", payload[\"messages\"])\n\t}\n\tassistant, ok := messages[1].(map[string]any)\n\tif !ok || assistant[\"reasoning_content\"] != \"historical reasoning\" {\n\t\tt.Fatalf(\"Qwen historical assistant message = %#v\", messages[1])\n\t}\n}\n\nfunc TestNewFromBindingBuildsOfficialQwenTokenPlanProvider(t *testing.T) {\n\tt.Setenv(\"QWEN_TOKEN_PLAN_API_KEY\", \"sk-sp-qwen-test\")\n\tcfg := config.BuiltIn()\n\tcfg.Provider = modelinfo.ProviderQwen\n\tcfg.Model = \"qwen3.8-max-preview\"\n\tcfg.ProviderMaxRetries = 0\n\tt.Setenv(\"OVERRIDE_QWEN_KEY\", \"sk-sp-wrong-key\")\n\tbinding := cfg.ProviderBinding()\n\tbinding.Provider.BaseURL = \"https://override.invalid/v1\"\n\tbinding.Auth.APIKeyEnv = \"OVERRIDE_QWEN_KEY\"\n\n\tprov, err := NewFromBinding(binding)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tqwen, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek OpenAI-compatible transport\", prov)\n\t}\n\tif qwen.ProviderID != modelinfo.ProviderQwen ||\n\t\tqwen.BaseURL != \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\" ||\n\t\tqwen.APIKey != \"sk-sp-qwen-test\" ||\n\t\tqwen.Model != \"qwen3.8-max-preview\" ||\n\t\tqwen.MaxParallelTools != cfg.MaxParallelTools ||\n\t\tqwen.MaxRetries != 0 {\n\t\tt.Fatalf(\"Qwen provider = %#v\", qwen)\n\t}\n}\n\nfunc TestQwenStreamUsesChatCompletionsBearerAndEmitsAccounting(t *testing.T) {\n\ttype capturedRequest struct {\n\t\tMethod string\n\t\tPath string\n\t\tAuthorization string\n\t\tBody []byte\n\t}\n\tcaptured := make(chan capturedRequest, 1)\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, _ := io.ReadAll(r.Body)\n\t\tcaptured <- capturedRequest{\n\t\t\tMethod: r.Method,\n\t\t\tPath: r.URL.Path,\n\t\t\tAuthorization: r.Header.Get(\"Authorization\"),\n\t\t\tBody: body,\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"qwen-request-1\")\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\tqwen := &DeepSeek{\n\t\tProviderID: modelinfo.ProviderQwen,\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"sk-sp-qwen-secret\",\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tThinking: \"disabled\",\n\t\tReasoningEffort: \"medium\",\n\t\tMaxTokens: 1024,\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 0,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := qwen.Stream(context.Background(), Request{\n\t\tRequestID: \"local-qwen-request\",\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar metadata RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmetadata = event.Request\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequest := <-captured\n\tif request.Method != http.MethodPost ||\n\t\trequest.Path != \"/chat/completions\" ||\n\t\trequest.Authorization != \"Bearer sk-sp-qwen-secret\" {\n\t\tt.Fatalf(\"Qwen request = %#v\", request)\n\t}\n\tvar payload map[string]any\n\tif err := json.Unmarshal(request.Body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"enable_thinking\"] != true ||\n\t\tpayload[\"preserve_thinking\"] != true ||\n\t\tpayload[\"reasoning_effort\"] != \"medium\" ||\n\t\tpayload[\"max_completion_tokens\"] != float64(1024) {\n\t\tt.Fatalf(\"Qwen HTTP payload = %s\", request.Body)\n\t}\n\tif metadata.RequestID != \"local-qwen-request\" ||\n\t\tmetadata.ProviderID != modelinfo.ProviderQwen ||\n\t\tmetadata.ModelID != \"qwen3.8-max-preview\" ||\n\t\tmetadata.ProviderRequestID != \"qwen-request-1\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"Qwen request metadata = %#v\", metadata)\n\t}\n}\n\nfunc TestDeepSeekStreamRejectsImageInputBeforeHTTP(t *testing.T) {\n\tvar called atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {\n\t\tcalled.Add(1)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{\n\t\t\tRole: protocol.RoleUser,\n\t\t\tContent: \"look\",\n\t\t\tParts: []protocol.MessagePart{\n\t\t\t\tprotocol.TextPart(\"look\"),\n\t\t\t\tprotocol.AttachmentPart(protocol.AttachmentRef{ID: \"att_test\", Kind: protocol.AttachmentKindImage}),\n\t\t\t},\n\t\t}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tif err == nil || !strings.Contains(err.Error(), \"image input is required\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tif called.Load() != 0 {\n\t\tt.Fatalf(\"DeepSeek HTTP server was called %d time(s)\", called.Load())\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewFromBindingRejectsUnsupportedCapabilityPolicyBeforeCredentials(t *testing.T) {\n\t_, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"enabled\",\n\t\t\tReasoningEffort: \"warp\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported reasoning_effort\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\n\t_, err = NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tMaxTokens: 9000,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"max_output_tokens=9000\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestDrainStreamNormalCompletion(t *testing.T) {\n\tevents := make(chan Event, 2)\n\terrs := make(chan error)\n\tevents <- Event{Kind: EventContent, Text: \"hello\"}\n\tevents <- Event{Kind: EventDone}\n\tclose(events)\n\tclose(errs)\n\n\tvar got []EventKind\n\tif err := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot = append(got, event.Kind)\n\t\t\treturn nil\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(got) != 2 || got[0] != EventContent || got[1] != EventDone {\n\t\tt.Fatalf(\"events = %#v\", got)\n\t}\n}\n\nfunc TestDrainStreamReturnsErrorAfterPartialEvents(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error, 1)\n\twantErr := errors.New(\"stream exploded\")\n\tseen := make(chan struct{})\n\tgo func() {\n\t\tevents <- Event{Kind: EventContent, Text: \"partial\"}\n\t\t<-seen\n\t\terrs <- wantErr\n\t}()\n\n\tvar got strings.Builder\n\terr := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot.WriteString(event.Text)\n\t\t\tclose(seen)\n\t\t\treturn nil\n\t\t},\n\t})\n\tif !errors.Is(err, wantErr) {\n\t\tt.Fatalf(\"err = %v, want %v\", err, wantErr)\n\t}\n\tif got.String() != \"partial\" {\n\t\tt.Fatalf(\"partial events = %q\", got.String())\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenEventsNeverClose(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\terr := DrainStream(ctx, make(chan Event), make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.Canceled) {\n\t\tt.Fatalf(\"err = %v, want context canceled\", err)\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenErrChannelBlocks(t *testing.T) {\n\tevents := make(chan Event)\n\tclose(events)\n\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)\n\tdefer cancel()\n\terr := DrainStream(ctx, events, make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\tt.Fatalf(\"err = %v, want deadline exceeded\", err)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "233f6708682bbda3da74606a73feb878f6d76360": {"t": 1785755391.367129, "n": 23437, "h": "e1eb03975c7300a8", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestDeepSeekStreamRejectsImageInputBeforeHTTP(t *testing.T) {\n\tvar called atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {\n\t\tcalled.Add(1)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{\n\t\t\tRole: protocol.RoleUser,\n\t\t\tContent: \"look\",\n\t\t\tParts: []protocol.MessagePart{\n\t\t\t\tprotocol.TextPart(\"look\"),\n\t\t\t\tprotocol.AttachmentPart(protocol.AttachmentRef{ID: \"att_test\", Kind: protocol.AttachmentKindImage}),\n\t\t\t},\n\t\t}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tif err == nil || !strings.Contains(err.Error(), \"image input is required\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tif called.Load() != 0 {\n\t\tt.Fatalf(\"DeepSeek HTTP server was called %d time(s)\", called.Load())\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewFromBindingRejectsUnsupportedCapabilityPolicyBeforeCredentials(t *testing.T) {\n\t_, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"enabled\",\n\t\t\tReasoningEffort: \"warp\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported reasoning_effort\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\n\t_, err = NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tMaxTokens: 9000,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"max_output_tokens=9000\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestDrainStreamNormalCompletion(t *testing.T) {\n\tevents := make(chan Event, 2)\n\terrs := make(chan error)\n\tevents <- Event{Kind: EventContent, Text: \"hello\"}\n\tevents <- Event{Kind: EventDone}\n\tclose(events)\n\tclose(errs)\n\n\tvar got []EventKind\n\tif err := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot = append(got, event.Kind)\n\t\t\treturn nil\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(got) != 2 || got[0] != EventContent || got[1] != EventDone {\n\t\tt.Fatalf(\"events = %#v\", got)\n\t}\n}\n\nfunc TestDrainStreamReturnsErrorAfterPartialEvents(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error, 1)\n\twantErr := errors.New(\"stream exploded\")\n\tseen := make(chan struct{})\n\tgo func() {\n\t\tevents <- Event{Kind: EventContent, Text: \"partial\"}\n\t\t<-seen\n\t\terrs <- wantErr\n\t}()\n\n\tvar got strings.Builder\n\terr := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot.WriteString(event.Text)\n\t\t\tclose(seen)\n\t\t\treturn nil\n\t\t},\n\t})\n\tif !errors.Is(err, wantErr) {\n\t\tt.Fatalf(\"err = %v, want %v\", err, wantErr)\n\t}\n\tif got.String() != \"partial\" {\n\t\tt.Fatalf(\"partial events = %q\", got.String())\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenEventsNeverClose(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\terr := DrainStream(ctx, make(chan Event), make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.Canceled) {\n\t\tt.Fatalf(\"err = %v, want context canceled\", err)\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenErrChannelBlocks(t *testing.T) {\n\tevents := make(chan Event)\n\tclose(events)\n\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)\n\tdefer cancel()\n\terr := DrainStream(ctx, events, make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\tt.Fatalf(\"err = %v, want deadline exceeded\", err)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "58854886f776e78e72f19f8ad987c6c634770531": {"t": 1785755394.0732336, "n": 21505, "h": "6d42dd1807445f61", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestDeepSeekStreamRejectsImageInputBeforeHTTP(t *testing.T) {\n\tvar called atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {\n\t\tcalled.Add(1)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{\n\t\t\tRole: protocol.RoleUser,\n\t\t\tContent: \"look\",\n\t\t\tParts: []protocol.MessagePart{\n\t\t\t\tprotocol.TextPart(\"look\"),\n\t\t\t\tprotocol.AttachmentPart(protocol.AttachmentRef{ID: \"att_test\", Kind: protocol.AttachmentKindImage}),\n\t\t\t},\n\t\t}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tif err == nil || !strings.Contains(err.Error(), \"image input is required\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tif called.Load() != 0 {\n\t\tt.Fatalf(\"DeepSeek HTTP server was called %d time(s)\", called.Load())\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewFromBindingRejectsUnsupportedCapabilityPolicyBeforeCredentials(t *testing.T) {\n\t_, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"enabled\",\n\t\t\tReasoningEffort: \"warp\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported reasoning_effort\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\n\t_, err = NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tMaxTokens: 9000,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"max_output_tokens=9000\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "b0885693bf0c4518e8284fd255b3044ce7b6a0da": {"t": 1785755398.097665, "n": 20480, "h": "0d0a0c30c19d1545", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewFromBindingRejectsUnsupportedCapabilityPolicyBeforeCredentials(t *testing.T) {\n\t_, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"enabled\",\n\t\t\tReasoningEffort: \"warp\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported reasoning_effort\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\n\t_, err = NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tMaxTokens: 9000,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"max_output_tokens=9000\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "f40a82f9c0ab20edfd9a536194068de2506a1435": {"t": 1785755399.795357, "n": 19472, "h": "4bc26553f58a72f4", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "b9d56de539bd66b7f338f0b7a3cb0ef865e8160a": {"t": 1785755401.4886093, "n": 17493, "h": "33b0c54bfaae79f6", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "f6b68cb600b24396b90269fe5935be230277eccc": {"t": 1785755402.9388921, "n": 13746, "h": "d92b99f3caf8fb59", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexProvider(cfg) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tprov, err := New(config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "2ac1238e7f0b97d3db343f057f956c78bd5361d1": {"t": 1785755404.4915373, "n": 12870, "h": "0aa874ec429dd420", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexProvider(cfg) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "b429568632931632f5da50205506e293edbc9fce": {"t": 1785755405.8277252, "n": 12414, "h": "0772298f2a1214ee", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexProvider(cfg) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "e2d8776010532865df5b7bce2ee1a27a35096bf8": {"t": 1785755407.3632617, "n": 9434, "h": "7ff328734eb16d05", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexProvider(cfg) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "fc5335c195e5809a0d46dbc9dbd63363b3a9bb87": {"t": 1785755408.6656435, "n": 8313, "h": "a8703f226977e0b1", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "05a25507b16b5cf8f415329af0f7e82c67a150aa": {"t": 1785755410.1068864, "n": 5943, "h": "160f42689bb21d0e", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "264f6d7160ea3283e3a1820057fd234d6cc2fbeb": {"t": 1785755411.3243492, "n": 5280, "h": "f8f4990ee306f47d", "c": "package provider\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}, "fa2643131a2fad4eac1363a5dc3a9a7c6e172e7a": {"t": 1785755412.7591953, "n": 3040, "h": "ec8629a5f4d0ab9d", "c": "package provider\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":5}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage || events[0].Usage.InputTokens != 3 || events[0].Usage.OutputTokens != 5 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n"}, "8e186bea271783c2ac5f1538412fba4ff7128242": {"t": 1786067846.3579364, "n": 28682, "h": "ad8f6c5879927a30", "c": "package provider\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/billyhargroveofficial/billyharness/internal/config\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/modelinfo\"\n\t\"github.com/billyhargroveofficial/billyharness/internal/protocol\"\n)\n\nfunc TestDeepSeekBodyThinkingHigh(t *testing.T) {\n\ttemp := 0.7\n\td := &DeepSeek{Thinking: \"enabled\", ReasoningEffort: \"high\"}\n\tbody, err := d.body(Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"hello\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{\n\t\t\t{\n\t\t\t\tName: \"time_now\",\n\t\t\t\tDescription: \"Return time.\",\n\t\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t\t\tRisk: protocol.RiskReadOnly,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"model\"] != \"deepseek-v4-flash\" {\n\t\tt.Fatalf(\"model = %v\", payload[\"model\"])\n\t}\n\tif _, ok := payload[\"temperature\"]; ok {\n\t\tt.Fatalf(\"temperature must be omitted when thinking is enabled: %s\", body)\n\t}\n\tif payload[\"reasoning_effort\"] != \"high\" {\n\t\tt.Fatalf(\"reasoning_effort = %v\", payload[\"reasoning_effort\"])\n\t}\n\tthinking, ok := payload[\"thinking\"].(map[string]any)\n\tif !ok || thinking[\"type\"] != \"enabled\" {\n\t\tt.Fatalf(\"thinking = %#v\", payload[\"thinking\"])\n\t}\n\ttools, ok := payload[\"tools\"].([]any)\n\tif !ok || len(tools) != 1 {\n\t\tt.Fatalf(\"tools = %#v\", payload[\"tools\"])\n\t}\n}\n\nfunc TestQwenBodyUsesAlwaysOnThinkingAndCompletionBudget(t *testing.T) {\n\ttemp := 0.7\n\tqwen := &DeepSeek{\n\t\tProviderID: modelinfo.ProviderQwen,\n\t\tThinking: \"disabled\",\n\t\tReasoningEffort: \"high\",\n\t\tMaxTokens: 4096,\n\t\tMaxParallelTools: 4,\n\t}\n\tbody, err := qwen.body(Request{\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tTemperature: &temp,\n\t\tMessages: []protocol.Message{\n\t\t\t{Role: protocol.RoleUser, Content: \"first\"},\n\t\t\t{Role: protocol.RoleAssistant, Content: \"answer\", ReasoningContent: \"historical reasoning\"},\n\t\t\t{Role: protocol.RoleUser, Content: \"continue\"},\n\t\t},\n\t\tTools: []protocol.ToolSpec{{\n\t\t\tName: \"time_now\",\n\t\t\tParameters: json.RawMessage(`{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}`),\n\t\t}},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar payload map[string]any\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"enable_thinking\"] != true ||\n\t\tpayload[\"preserve_thinking\"] != true ||\n\t\tpayload[\"reasoning_effort\"] != \"xhigh\" ||\n\t\tpayload[\"max_completion_tokens\"] != float64(4096) ||\n\t\tpayload[\"parallel_tool_calls\"] != true {\n\t\tt.Fatalf(\"Qwen payload = %s\", body)\n\t}\n\tfor _, forbidden := range []string{\"thinking\", \"temperature\", \"max_tokens\"} {\n\t\tif _, ok := payload[forbidden]; ok {\n\t\t\tt.Fatalf(\"Qwen payload contains %q: %s\", forbidden, body)\n\t\t}\n\t}\n\tmessages, ok := payload[\"messages\"].([]any)\n\tif !ok || len(messages) != 3 {\n\t\tt.Fatalf(\"Qwen messages = %#v\", payload[\"messages\"])\n\t}\n\tassistant, ok := messages[1].(map[string]any)\n\tif !ok || assistant[\"reasoning_content\"] != \"historical reasoning\" {\n\t\tt.Fatalf(\"Qwen historical assistant message = %#v\", messages[1])\n\t}\n}\n\nfunc TestNewFromBindingBuildsOfficialQwenTokenPlanProvider(t *testing.T) {\n\tt.Setenv(\"QWEN_TOKEN_PLAN_API_KEY\", \"sk-sp-qwen-test\")\n\tcfg := config.BuiltIn()\n\tcfg.Provider = modelinfo.ProviderQwen\n\tcfg.Model = \"qwen3.8-max-preview\"\n\tcfg.ProviderMaxRetries = 0\n\tt.Setenv(\"OVERRIDE_QWEN_KEY\", \"sk-sp-wrong-key\")\n\tbinding := cfg.ProviderBinding()\n\tbinding.Provider.BaseURL = \"https://override.invalid/v1\"\n\tbinding.Auth.APIKeyEnv = \"OVERRIDE_QWEN_KEY\"\n\n\tprov, err := NewFromBinding(binding)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tqwen, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek OpenAI-compatible transport\", prov)\n\t}\n\tif qwen.ProviderID != modelinfo.ProviderQwen ||\n\t\tqwen.BaseURL != \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\" ||\n\t\tqwen.APIKey != \"sk-sp-qwen-test\" ||\n\t\tqwen.Model != \"qwen3.8-max-preview\" ||\n\t\tqwen.MaxParallelTools != cfg.MaxParallelTools ||\n\t\tqwen.MaxRetries != 0 {\n\t\tt.Fatalf(\"Qwen provider = %#v\", qwen)\n\t}\n}\n\nfunc TestQwenStreamUsesChatCompletionsBearerAndEmitsAccounting(t *testing.T) {\n\ttype capturedRequest struct {\n\t\tMethod string\n\t\tPath string\n\t\tAuthorization string\n\t\tBody []byte\n\t}\n\tcaptured := make(chan capturedRequest, 1)\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, _ := io.ReadAll(r.Body)\n\t\tcaptured <- capturedRequest{\n\t\t\tMethod: r.Method,\n\t\t\tPath: r.URL.Path,\n\t\t\tAuthorization: r.Header.Get(\"Authorization\"),\n\t\t\tBody: body,\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"qwen-request-1\")\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\tqwen := &DeepSeek{\n\t\tProviderID: modelinfo.ProviderQwen,\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"sk-sp-qwen-secret\",\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tThinking: \"disabled\",\n\t\tReasoningEffort: \"medium\",\n\t\tMaxTokens: 1024,\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 0,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := qwen.Stream(context.Background(), Request{\n\t\tRequestID: \"local-qwen-request\",\n\t\tModel: \"qwen3.8-max-preview\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar metadata RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmetadata = event.Request\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequest := <-captured\n\tif request.Method != http.MethodPost ||\n\t\trequest.Path != \"/chat/completions\" ||\n\t\trequest.Authorization != \"Bearer sk-sp-qwen-secret\" {\n\t\tt.Fatalf(\"Qwen request = %#v\", request)\n\t}\n\tvar payload map[string]any\n\tif err := json.Unmarshal(request.Body, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload[\"enable_thinking\"] != true ||\n\t\tpayload[\"preserve_thinking\"] != true ||\n\t\tpayload[\"reasoning_effort\"] != \"medium\" ||\n\t\tpayload[\"max_completion_tokens\"] != float64(1024) {\n\t\tt.Fatalf(\"Qwen HTTP payload = %s\", request.Body)\n\t}\n\tif metadata.RequestID != \"local-qwen-request\" ||\n\t\tmetadata.ProviderID != modelinfo.ProviderQwen ||\n\t\tmetadata.ModelID != \"qwen3.8-max-preview\" ||\n\t\tmetadata.ProviderRequestID != \"qwen-request-1\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"Qwen request metadata = %#v\", metadata)\n\t}\n}\n\nfunc TestDeepSeekStreamRejectsImageInputBeforeHTTP(t *testing.T) {\n\tvar called atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {\n\t\tcalled.Add(1)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{\n\t\t\tRole: protocol.RoleUser,\n\t\t\tContent: \"look\",\n\t\t\tParts: []protocol.MessagePart{\n\t\t\t\tprotocol.TextPart(\"look\"),\n\t\t\t\tprotocol.AttachmentPart(protocol.AttachmentRef{ID: \"att_test\", Kind: protocol.AttachmentKindImage}),\n\t\t\t},\n\t\t}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tif err == nil || !strings.Contains(err.Error(), \"image input is required\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\tif called.Load() != 0 {\n\t\tt.Fatalf(\"DeepSeek HTTP server was called %d time(s)\", called.Load())\n\t}\n}\n\nfunc TestIsCodexProviderReroutesOSeriesModels(t *testing.T) {\n\tfor _, model := range []string{\"gpt-5.5\", \"o1-preview\", \"o3-mini\", \"o4-mini\"} {\n\t\tcfg := config.Config{Provider: \"deepseek\", Model: model}\n\t\tif !isCodexBinding(cfg.ProviderBinding()) {\n\t\t\tt.Fatalf(\"model %q should route to Codex provider\", model)\n\t\t}\n\t}\n}\n\nfunc TestNewFromBindingRejectsUnsupportedCapabilityPolicyBeforeCredentials(t *testing.T) {\n\t_, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"enabled\",\n\t\t\tReasoningEffort: \"warp\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"unsupported reasoning_effort\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n\n\t_, err = NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{Provider: \"deepseek\", BaseURL: \"https://api.deepseek.example\"},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tMaxTokens: 9000,\n\t\t},\n\t\tAuth: config.AuthSettings{APIKeyEnv: \"MISSING_DEEPSEEK_KEY\"},\n\t})\n\tif err == nil || !strings.Contains(err.Error(), \"max_output_tokens=9000\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n\nfunc TestNewDeepSeekProviderUsesCredentialsManager(t *testing.T) {\n\troot := t.TempDir()\n\tt.Setenv(\"BILLYHARNESS_HOME\", root)\n\tt.Setenv(\"FAST_AGENT_ENV_FILE\", \"\")\n\tt.Setenv(\"BILLYHARNESS_DOTENV_HOME_ONLY\", \"1\")\n\tif err := os.WriteFile(filepath.Join(root, \".env\"), []byte(\"CUSTOM_DEEPSEEK_KEY=sk-provider-value\\n\"), 0o600); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcfg := config.Config{\n\t\tProvider: \"deepseek\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tBaseURL: \"https://api.deepseek.com\",\n\t\tAPIKeyEnv: \"CUSTOM_DEEPSEEK_KEY\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t}\n\tprov, err := NewFromBinding(cfg.ProviderBinding())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.APIKey != \"sk-provider-value\" {\n\t\tt.Fatalf(\"APIKey = %q\", deepseek.APIKey)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekProviderUsesProjection(t *testing.T) {\n\tt.Setenv(\"BINDING_DEEPSEEK_KEY\", \"sk-binding-value\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t\tThinking: \"disabled\",\n\t\t\tReasoningEffort: \"low\",\n\t\t\tMaxTokens: 512,\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"BINDING_DEEPSEEK_KEY\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: 2 * time.Second,\n\t\t\tProviderMaxRetries: 3,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdeepseek, ok := prov.(*DeepSeek)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n\tif deepseek.BaseURL != \"https://api.deepseek.example\" ||\n\t\tdeepseek.APIKey != \"sk-binding-value\" ||\n\t\tdeepseek.Model != \"deepseek-v4-flash\" ||\n\t\tdeepseek.Thinking != \"disabled\" ||\n\t\tdeepseek.ReasoningEffort != \"low\" ||\n\t\tdeepseek.MaxTokens != 512 ||\n\t\tdeepseek.RequestTimeout != time.Second ||\n\t\tdeepseek.StreamIdleTimeout != 2*time.Second ||\n\t\tdeepseek.MaxRetries != 3 {\n\t\tt.Fatalf(\"deepseek provider = %#v\", deepseek)\n\t}\n}\n\nfunc TestNewFromBindingDeepSeekCredentialsIgnoreCodexAuthPath(t *testing.T) {\n\tt.Setenv(\"ISOLATED_DEEPSEEK_KEY\", \"sk-isolated-value\")\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.example\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"deepseek-v4-flash\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"ISOLATED_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.invalid/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, ok := prov.(*DeepSeek); !ok {\n\t\tt.Fatalf(\"provider = %T, want *DeepSeek\", prov)\n\t}\n}\n\nfunc TestNewFromBindingCodexIgnoresDeepSeekCredentials(t *testing.T) {\n\tt.Setenv(\"CODEX_ACCESS_TOKEN\", \"codex-env-token\")\n\tt.Setenv(\"MISSING_DEEPSEEK_KEY\", \"\")\n\n\tprov, err := NewFromBinding(config.ProviderBinding{\n\t\tProvider: config.ProviderSelection{\n\t\t\tProvider: \"deepseek\",\n\t\t\tBaseURL: \"https://api.deepseek.invalid\",\n\t\t\tCodexBaseURL: \"https://codex.example/backend\",\n\t\t},\n\t\tModel: config.ModelSelection{\n\t\t\tModel: \"gpt-5.5\",\n\t\t},\n\t\tAuth: config.AuthSettings{\n\t\t\tAPIKeyEnv: \"MISSING_DEEPSEEK_KEY\",\n\t\t\tCodexAuthFile: filepath.Join(t.TempDir(), \"missing-codex.json\"),\n\t\t\tCodexRefreshURL: \"https://auth.example/token\",\n\t\t\tCodexClientID: \"codex-client\",\n\t\t\tCodexOriginator: \"test-originator\",\n\t\t},\n\t\tLimits: config.RuntimeLimits{\n\t\t\tRequestTimeout: time.Second,\n\t\t\tStreamIdleTimeout: time.Second,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcodex, ok := prov.(*Codex)\n\tif !ok {\n\t\tt.Fatalf(\"provider = %T, want *Codex\", prov)\n\t}\n\tif codex.BaseURL != \"https://codex.example/backend\" || codex.Auth.AccessToken != \"codex-env-token\" {\n\t\tt.Fatalf(\"codex provider = %#v\", codex)\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesEventsBeforeErrs(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error)\n\twantErr := errors.New(\"provider boom\")\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\trunProviderStream(events, errs, func() error {\n\t\t\treturn wantErr\n\t\t})\n\t}()\n\n\tselect {\n\tcase _, ok := <-events:\n\t\tif ok {\n\t\t\tt.Fatal(\"event channel yielded an event\")\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for events to close\")\n\t}\n\tselect {\n\tcase err, ok := <-errs:\n\t\tif !ok || !errors.Is(err, wantErr) {\n\t\t\tt.Fatalf(\"err = %v ok=%v, want %v\", err, ok, wantErr)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for terminal error\")\n\t}\n\tif _, ok := <-errs; ok {\n\t\tt.Fatal(\"error channel yielded more than one value\")\n\t}\n\tselect {\n\tcase <-done:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stream runner did not return\")\n\t}\n}\n\nfunc TestProviderStreamRunnerClosesErrsWithoutValueOnSuccess(t *testing.T) {\n\tevents := make(chan Event, 1)\n\terrs := make(chan error)\n\tgo runProviderStream(events, errs, func() error {\n\t\tevents <- Event{Kind: EventDone}\n\t\treturn nil\n\t})\n\n\tif event, ok := <-events; !ok || event.Kind != EventDone {\n\t\tt.Fatalf(\"event = %#v ok=%v\", event, ok)\n\t}\n\tif _, ok := <-events; ok {\n\t\tt.Fatal(\"events channel remained open\")\n\t}\n\tif err, ok := <-errs; ok || err != nil {\n\t\tt.Fatalf(\"err = %v ok=%v, want closed channel\", err, ok)\n\t}\n}\n\nfunc TestDrainStreamNormalCompletion(t *testing.T) {\n\tevents := make(chan Event, 2)\n\terrs := make(chan error)\n\tevents <- Event{Kind: EventContent, Text: \"hello\"}\n\tevents <- Event{Kind: EventDone}\n\tclose(events)\n\tclose(errs)\n\n\tvar got []EventKind\n\tif err := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot = append(got, event.Kind)\n\t\t\treturn nil\n\t\t},\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(got) != 2 || got[0] != EventContent || got[1] != EventDone {\n\t\tt.Fatalf(\"events = %#v\", got)\n\t}\n}\n\nfunc TestDrainStreamReturnsErrorAfterPartialEvents(t *testing.T) {\n\tevents := make(chan Event)\n\terrs := make(chan error, 1)\n\twantErr := errors.New(\"stream exploded\")\n\tseen := make(chan struct{})\n\tgo func() {\n\t\tevents <- Event{Kind: EventContent, Text: \"partial\"}\n\t\t<-seen\n\t\terrs <- wantErr\n\t}()\n\n\tvar got strings.Builder\n\terr := DrainStream(context.Background(), events, errs, StreamDrainOptions{\n\t\tOnEvent: func(event Event) error {\n\t\t\tgot.WriteString(event.Text)\n\t\t\tclose(seen)\n\t\t\treturn nil\n\t\t},\n\t})\n\tif !errors.Is(err, wantErr) {\n\t\tt.Fatalf(\"err = %v, want %v\", err, wantErr)\n\t}\n\tif got.String() != \"partial\" {\n\t\tt.Fatalf(\"partial events = %q\", got.String())\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenEventsNeverClose(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\terr := DrainStream(ctx, make(chan Event), make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.Canceled) {\n\t\tt.Fatalf(\"err = %v, want context canceled\", err)\n\t}\n}\n\nfunc TestDrainStreamReturnsContextWhenErrChannelBlocks(t *testing.T) {\n\tevents := make(chan Event)\n\tclose(events)\n\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)\n\tdefer cancel()\n\terr := DrainStream(ctx, events, make(chan error), StreamDrainOptions{})\n\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\tt.Fatalf(\"err = %v, want deadline exceeded\", err)\n\t}\n}\n\nfunc TestParseChunkEmitsReasoningContentToolAndUsage(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[{\n\t\t\t\"delta\":{\n\t\t\t\t\"reasoning_content\":\"think\",\n\t\t\t\t\"content\":\"answer\",\n\t\t\t\t\"tool_calls\":[{\n\t\t\t\t\t\"index\":0,\n\t\t\t\t\t\"id\":\"call_1\",\n\t\t\t\t\t\"function\":{\"name\":\"time_now\",\"arguments\":\"{}\"}\n\t\t\t\t}]\n\t\t\t}\n\t\t}],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":30,\n\t\t\t\"completion_tokens\":5,\n\t\t\t\"prompt_cache_hit_tokens\":20,\n\t\t\t\"prompt_cache_miss_tokens\":10,\n\t\t\t\"completion_tokens_details\":{\"reasoning_tokens\":2}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 4 {\n\t\tt.Fatalf(\"events len = %d: %#v\", len(events), events)\n\t}\n\tif events[0].Kind != EventUsage ||\n\t\tevents[0].Usage.InputTokens != 30 ||\n\t\tevents[0].Usage.OutputTokens != 5 ||\n\t\tevents[0].Usage.CacheHitTokens != 20 ||\n\t\tevents[0].Usage.CacheMissTokens != 10 ||\n\t\tevents[0].Usage.ReasoningTokens != 2 {\n\t\tt.Fatalf(\"usage event = %#v\", events[0])\n\t}\n\tif events[1].Kind != EventContent || events[1].Text != \"answer\" {\n\t\tt.Fatalf(\"content event = %#v\", events[1])\n\t}\n\tif events[2].Kind != EventReasoning || events[2].Text != \"think\" {\n\t\tt.Fatalf(\"reasoning event = %#v\", events[2])\n\t}\n\tif events[3].Kind != EventToolCallDelta || events[3].ToolID != \"call_1\" || events[3].ToolName != \"time_now\" || events[3].ArgsDelta != \"{}\" {\n\t\tt.Fatalf(\"tool event = %#v\", events[3])\n\t}\n}\n\nfunc TestParseChunkNormalizesOpenAICachedTokens(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7,\n\t\t\t\"prompt_tokens_details\":{\"cached_tokens\":64}\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 64 || events[0].Usage.CacheMissTokens != 36 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseChunkCountsPromptTokensAsMissWhenCacheFieldsMissing(t *testing.T) {\n\tevents, err := parseChunk([]byte(`{\n\t\t\"choices\":[],\n\t\t\"usage\":{\n\t\t\t\"prompt_tokens\":100,\n\t\t\t\"completion_tokens\":7\n\t\t}\n\t}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(events) != 1 || events[0].Kind != EventUsage {\n\t\tt.Fatalf(\"events = %#v\", events)\n\t}\n\tif events[0].Usage.CacheHitTokens != 0 || events[0].Usage.CacheMissTokens != 100 {\n\t\tt.Fatalf(\"usage = %#v\", events[0].Usage)\n\t}\n}\n\nfunc TestParseSSEReturnsWhenEventConsumerBlockedAndContextCancelled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tevents := make(chan Event)\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- parseSSE(ctx, strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"blocked\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\t}()\n\ttime.Sleep(20 * time.Millisecond)\n\tcancel()\n\tselect {\n\tcase err := <-done:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"err = %v, want context.Canceled\", err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"parseSSE did not return after context cancellation\")\n\t}\n}\n\nfunc TestParseSSEReturnsErrorWhenStreamClosesBeforeDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(`data: {\"choices\":[{\"delta\":{\"content\":\"partial\"}}]}`+\"\\n\\n\"), time.Minute, events)\n\tif err == nil || !strings.Contains(err.Error(), \"before [DONE]\") {\n\t\tt.Fatalf(\"err = %v, want truncated stream error\", err)\n\t}\n}\n\nfunc TestParseSSEReturnsImmediatelyAfterDone(t *testing.T) {\n\tevents := make(chan Event, 4)\n\terr := parseSSE(context.Background(), strings.NewReader(\"data: [DONE]\\n\\n\"), time.Minute, events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tselect {\n\tcase event := <-events:\n\t\tif event.Kind != EventDone {\n\t\t\tt.Fatalf(\"event = %#v\", event)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"missing done event\")\n\t}\n}\n\nfunc TestDeepSeekStreamRetriesRateLimitBeforeStreaming(t *testing.T) {\n\tvar calls int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path != \"/chat/completions\" {\n\t\t\tt.Fatalf(\"path = %q\", r.URL.Path)\n\t\t}\n\t\tn := atomic.AddInt32(&calls, 1)\n\t\tif n == 1 {\n\t\t\tw.Header().Set(\"Retry-After\", \"0\")\n\t\t\thttp.Error(w, \"slow down\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-req-2\")\n\t\t_, _ = w.Write([]byte(\"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"ok\\\"}}]}\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tvar meta RequestMetadata\n\tfor event := range events {\n\t\tif event.Kind == EventRequestMetadata {\n\t\t\tmeta = event.Request\n\t\t}\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif calls != 2 || content != \"ok\" {\n\t\tt.Fatalf(\"calls=%d content=%q\", calls, content)\n\t}\n\tif meta.RequestID != \"local-request\" || meta.ProviderID != \"deepseek\" || meta.ModelID != \"deepseek-v4-flash\" ||\n\t\tmeta.ProviderRequestID != \"deepseek-req-2\" || meta.Attempts != 2 || meta.Retries != 1 || meta.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"metadata = %#v\", meta)\n\t}\n}\n\nfunc TestDeepSeekStreamDoesNotApplyRequestTimeoutAfterHeaders(t *testing.T) {\n\trequestTimeout := 50 * time.Millisecond\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"first\"}}]}` + \"\\n\\n\"))\n\t\tif flusher, ok := w.(http.Flusher); ok {\n\t\t\tflusher.Flush()\n\t\t}\n\t\ttime.Sleep(2 * requestTimeout)\n\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"second\"}}]}` + \"\\n\\n\"))\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: requestTimeout,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tvar content string\n\tfor event := range events {\n\t\tif event.Kind == EventContent {\n\t\t\tcontent += event.Text\n\t\t}\n\t}\n\tif err := <-errs; err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif content != \"firstsecond\" {\n\t\tt.Fatalf(\"content = %q\", content)\n\t}\n}\n\nfunc TestDeepSeekStreamBuffersEventsForSlowConsumer(t *testing.T) {\n\tconst eventCount = 128\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tfor i := 0; i < eventCount; i++ {\n\t\t\t_, _ = w.Write([]byte(`data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}` + \"\\n\\n\"))\n\t\t}\n\t\t_, _ = w.Write([]byte(\"data: [DONE]\\n\\n\"))\n\t}))\n\tt.Cleanup(server.Close)\n\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret\",\n\t\tRequestTimeout: time.Second,\n\t\tStreamIdleTimeout: time.Second,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tselect {\n\tcase err := <-errs:\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"provider stream blocked behind slow event consumer\")\n\t}\n\n\tvar contentEvents int\n\tvar sawDone bool\n\tfor event := range events {\n\t\tswitch event.Kind {\n\t\tcase EventContent:\n\t\t\tcontentEvents++\n\t\tcase EventDone:\n\t\t\tsawDone = true\n\t\t}\n\t}\n\tif contentEvents != eventCount || !sawDone {\n\t\tt.Fatalf(\"contentEvents=%d sawDone=%v\", contentEvents, sawDone)\n\t}\n}\n\nfunc TestRetryAfterParsingAndDelay(t *testing.T) {\n\tif got := parseRetryAfter(\"0.25\", time.Now()); got != 250*time.Millisecond {\n\t\tt.Fatalf(\"fractional Retry-After = %s\", got)\n\t}\n\tretryAfter := 30 * time.Second\n\terr := &ProviderError{Kind: ErrorRateLimit, RetryAfter: retryAfter}\n\tif got := providerRetryDelay(err, 0); got != retryAfter {\n\t\tt.Fatalf(\"retry delay = %s, want %s\", got, retryAfter)\n\t}\n}\n\nfunc TestDeepSeekHTTPErrorIsTypedAndRedacted(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"x-request-id\", \"deepseek-error-request\")\n\t\thttp.Error(w, \"bad secret-token\", http.StatusBadRequest)\n\t}))\n\tt.Cleanup(server.Close)\n\td := &DeepSeek{\n\t\tBaseURL: server.URL,\n\t\tAPIKey: \"secret-token\",\n\t\tRequestTimeout: time.Second,\n\t\tMaxRetries: 1,\n\t\tClient: server.Client(),\n\t}\n\tevents, errs := d.Stream(context.Background(), Request{\n\t\tRequestID: \"local-deepseek-request\",\n\t\tModel: \"deepseek-v4-flash\",\n\t\tMessages: []protocol.Message{{Role: protocol.RoleUser, Content: \"ping\"}},\n\t})\n\tfor range events {\n\t}\n\terr := <-errs\n\tvar providerErr *ProviderError\n\tif !errors.As(err, &providerErr) {\n\t\tt.Fatalf(\"err = %T %v, want ProviderError\", err, err)\n\t}\n\tif providerErr.Kind != ErrorBadRequest || providerErr.Status != http.StatusBadRequest {\n\t\tt.Fatalf(\"provider error = %#v\", providerErr)\n\t}\n\tif strings.Contains(err.Error(), \"secret-token\") || !strings.Contains(err.Error(), \"[redacted]\") {\n\t\tt.Fatalf(\"error redaction failed: %v\", err)\n\t}\n\tmetadata, ok := RequestMetadataFromError(err)\n\tif !ok ||\n\t\tmetadata.RequestID != \"local-deepseek-request\" ||\n\t\tmetadata.ProviderID != \"deepseek\" ||\n\t\tmetadata.ModelID != \"deepseek-v4-flash\" ||\n\t\tmetadata.ProviderRequestID != \"deepseek-error-request\" ||\n\t\tmetadata.Attempts != 1 ||\n\t\tmetadata.Retries != 0 ||\n\t\tmetadata.StatusCode != http.StatusBadRequest {\n\t\tt.Fatalf(\"metadata = %#v ok=%v\", metadata, ok)\n\t}\n}\n\nfunc TestToolAccumulatorAssemblesArgumentDeltas(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"fs_list\", ArgsDelta: `{\"path\"`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ArgsDelta: `:\".\"}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls len = %d\", len(calls))\n\t}\n\tif calls[0].ID != \"call_a\" || calls[0].Name != \"fs_list\" || string(calls[0].Arguments) != `{\"path\":\".\"}` {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorParallelCallsAndGaps(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 1, ToolID: \"call_b\", ToolName: \"fs_read_file\", ArgsDelta: `{\"path\":\"README.md\"}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ToolName: \"time_now\", ArgsDelta: `{}`})\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: -1, ToolID: \"bad\", ToolName: \"bad\", ArgsDelta: `{}`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 2 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif calls[0].Name != \"time_now\" || calls[1].Name != \"fs_read_file\" {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n}\n\nfunc TestToolAccumulatorSanitizesInvalidJSONArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_bad\", ToolName: \"shell_exec\", ArgsDelta: `{bad`})\n\n\tcalls, err := acc.Finish()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(calls) != 1 {\n\t\tt.Fatalf(\"calls = %#v\", calls)\n\t}\n\tif string(calls[0].Arguments) != \"{}\" ||\n\t\tcalls[0].InvalidArguments != \"{bad\" ||\n\t\t!strings.Contains(calls[0].InvalidArgumentError, \"invalid JSON args\") {\n\t\tt.Fatalf(\"call = %#v\", calls[0])\n\t}\n}\n\nfunc TestToolAccumulatorErrorsOnMissingNameWithArguments(t *testing.T) {\n\tvar acc ToolAccumulator\n\tacc.Push(Event{Kind: EventToolCallDelta, ToolIndex: 0, ToolID: \"call_a\", ArgsDelta: `{\"path\":\".\"}`})\n\tif _, err := acc.Finish(); err == nil || !strings.Contains(err.Error(), \"missing name\") {\n\t\tt.Fatalf(\"err = %v\", err)\n\t}\n}\n"}}}}