Files

1 line
68 KiB
JSON

{"repo": "arbazkhan971/bharatcode", "files": {"internal/cmd/root_test.go": {"4520f16f286aaa74f7174c9b469caa636f1b3159": {"t": 1785745652.3024912, "n": 9817, "h": "f0c2cd4cf8bfb7f6", "c": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/arbazkhan971/bharatcode/internal/app\"\n\t\"github.com/arbazkhan971/bharatcode/internal/session\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRootNoArgsStartsTUI(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar called bool\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\t_ = ctx\n\t\trequire.True(t, opts.YOLO)\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\t_ = ctx\n\t\t_ = application\n\t\t_ = initialSessionID\n\t\tcalled = true\n\t\treturn nil\n\t}\n\n\tstdout, stderr, err := executeRoot(t, \"--yolo\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Empty(t, stderr)\n\trequire.True(t, called)\n}\n\nfunc TestHelpTextListsSubcommands(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"--help\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"Usage:\")\n\tfor _, name := range []string{\"run\", \"login\", \"logout\", \"models\", \"sessions\", \"stats\", \"budget\", \"update-providers\", \"config\", \"version\"} {\n\t\trequire.Contains(t, stdout, name)\n\t}\n}\n\nfunc TestVersionFormat(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"version\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Regexp(t, regexp.MustCompile(`^bharatcode v\\d+\\.\\d+\\.\\d+(-[a-z0-9.-]+)? \\([0-9a-f]{7,}\\)\\n$`), stdout)\n}\n\nfunc TestModelsTable(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"models\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"PROVIDER\")\n\trequire.Contains(t, stdout, \"deepseek-chat\")\n\trequire.Contains(t, stdout, \"INPUT$/MTOK\")\n\trequire.Contains(t, stdout, \" \")\n}\n\nfunc TestStatsEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tdataDir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dataDir)\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"stats\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"no usage recorded\\n\", stdout)\n}\n\nfunc TestBudgetSetPersists(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"budget\", \"set\", \"--month\", \"500\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Monthly budget set to ₹500\\n\", stdout)\n\n\traw, err := os.ReadFile(configPath)\n\trequire.NoError(t, err)\n\tvar cfg map[string]any\n\trequire.NoError(t, json.Unmarshal(raw, &cfg))\n\tledger, ok := cfg[\"ledger\"].(map[string]any)\n\trequire.True(t, ok)\n\trequire.Equal(t, float64(500), ledger[\"max_inr_per_month\"])\n}\n\nfunc TestProfileFlagForwardedToApp(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar gotProfile string\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\tgotProfile = opts.Profile\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t, \"--profile\", \"ollama\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"ollama\", gotProfile)\n}\n\nfunc TestProfileFlagAbsentDefaultsToEmpty(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar gotProfile string\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\tgotProfile = opts.Profile\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t)\n\trequire.NoError(t, err)\n\trequire.Empty(t, gotProfile)\n}\n\nfunc TestLoginLogoutFakeKeyring(t *testing.T) {\n\toldKeyring := keyring\n\tfake := newFakeKeyring()\n\tkeyring = fake\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged in to openai\\n\", stdout)\n\trequire.Equal(t, \"secret\", fake.values[\"bharatcode/openai\"])\n\n\tstdout, stderr, err = executeRoot(t, \"logout\", \"openai\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged out of openai\\n\", stdout)\n\trequire.NotContains(t, fake.values, \"bharatcode/openai\")\n}\n\nfunc TestLoginKeyringUnavailable(t *testing.T) {\n\toldKeyring := keyring\n\tkeyring = failingKeyring{err: errors.New(\"boom\")}\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: keyring unavailable: boom\")\n}\n\nfunc TestSessionShowNotFound(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"show\", \"bogus-id\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: session bogus-id not found\")\n}\n\nfunc TestSessionsListEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"list\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Equal(t, \"no sessions\\n\", stderr)\n}\n\nfunc TestRunStdinPromptValidation(t *testing.T) {\n\tcmd := newRunCmd()\n\tcmd.SetIn(strings.NewReader(\"hello\\n\"))\n\tprompt, err := readPrompt(cmd, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello\", prompt)\n}\n\nfunc executeRoot(t *testing.T, args ...string) (string, string, error) {\n\tt.Helper()\n\troot := newRootCmd()\n\tvar stdout bytes.Buffer\n\tvar stderr bytes.Buffer\n\troot.SetOut(&stdout)\n\troot.SetErr(&stderr)\n\troot.SetArgs(args)\n\terr := executeCommand(context.Background(), root)\n\treturn stdout.String(), stderr.String(), err\n}\n\nfunc writeConfig(t *testing.T, body string) string {\n\tt.Helper()\n\tpath := filepath.Join(t.TempDir(), \"config.json\")\n\trequire.NoError(t, os.WriteFile(path, []byte(body), 0o600))\n\treturn path\n}\n\nfunc defaultTestConfig() string {\n\treturn `{\n \"providers\": [\n {\n \"name\": \"deepseek\",\n \"type\": \"openai_compatible\",\n \"base_url\": \"https://api.deepseek.com/v1\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"models\": [\"deepseek-chat\"]\n }\n ],\n \"models\": [\n {\n \"id\": \"deepseek-chat\",\n \"provider\": \"deepseek\",\n \"context_window\": 64000,\n \"input_price_per_mtok_usd\": 0.27,\n \"output_price_per_mtok_usd\": 1.10,\n \"supports_images\": false,\n \"supports_tools\": true\n }\n ],\n \"agents\": [\n {\n \"name\": \"coder\",\n \"model\": \"deepseek-chat\",\n \"system_prompt\": \"You are concise.\"\n }\n ],\n \"ledger\": {\n \"currency\": \"INR\",\n \"usd_inr_rate\": 83.5\n }\n}`\n}\n\ntype fakeKeyring struct {\n\tvalues map[string]string\n}\n\nfunc newFakeKeyring() *fakeKeyring {\n\treturn &fakeKeyring{values: make(map[string]string)}\n}\n\nfunc (k *fakeKeyring) Get(service, account string) (string, error) {\n\tvalue, ok := k.values[service+\"/\"+account]\n\tif !ok {\n\t\treturn \"\", os.ErrNotExist\n\t}\n\treturn value, nil\n}\n\nfunc (k *fakeKeyring) Set(service, account, secret string) error {\n\tk.values[service+\"/\"+account] = secret\n\treturn nil\n}\n\nfunc (k *fakeKeyring) Delete(service, account string) error {\n\tdelete(k.values, service+\"/\"+account)\n\treturn nil\n}\n\ntype failingKeyring struct {\n\terr error\n}\n\nfunc (k failingKeyring) Get(service, account string) (string, error) {\n\t_ = service\n\t_ = account\n\treturn \"\", k.err\n}\n\nfunc (k failingKeyring) Set(service, account, secret string) error {\n\t_ = service\n\t_ = account\n\t_ = secret\n\treturn k.err\n}\n\nfunc (k failingKeyring) Delete(service, account string) error {\n\t_ = service\n\t_ = account\n\treturn k.err\n}\n\nfunc TestContinueFlagPassesSessionID(t *testing.T) {\n\toldRunTUI := runTUI\n\tdefer func() { runTUI = oldRunTUI }()\n\n\t// Use a real on-disk DB so sessions survive across app.New calls.\n\tdir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dir)\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tprojDir := t.TempDir()\n\n\t// Seed one session via a short-lived app.New / Close pair.\n\tseedApp, err := app.New(context.Background(), app.Options{ConfigPath: configPath, ProjectDir: projDir})\n\trequire.NoError(t, err)\n\tsess := &session.Session{ID: \"abc123\", Title: \"Test\", ProjectPath: projDir}\n\trequire.NoError(t, seedApp.Sessions.Create(context.Background(), sess))\n\trequire.NoError(t, seedApp.Close(context.Background()))\n\n\t// Replace only runTUI — let buildApp call newApp for real so resolveInitialSession\n\t// reads from the same DB where we seeded the session above.\n\tvar gotID string\n\trunTUI = func(ctx context.Context, a *app.App, initialSessionID string) error {\n\t\tgotID = initialSessionID\n\t\treturn nil\n\t}\n\n\t_, _, err = executeRoot(t, \"--config\", configPath, \"--project-dir\", projDir, \"--continue\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"abc123\", gotID, \"--continue should pass the latest session ID to the TUI\")\n}\n\nfunc TestContinueFlagNoSessions(t *testing.T) {\n\toldRunTUI := runTUI\n\tdefer func() { runTUI = oldRunTUI }()\n\n\tdir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dir)\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\n\tvar gotID string\n\trunTUI = func(ctx context.Context, a *app.App, initialSessionID string) error {\n\t\tgotID = initialSessionID\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"--continue\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, gotID, \"--continue with no sessions should pass empty string (start fresh)\")\n}\n\nvar _ = cobra.Command{}\n"}, "f8f012d9f712f70063fdc6cff6b21af25ae3b640": {"t": 1785755416.3096251, "n": 9817, "h": "f0c2cd4cf8bfb7f6", "c": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/arbazkhan971/bharatcode/internal/app\"\n\t\"github.com/arbazkhan971/bharatcode/internal/session\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRootNoArgsStartsTUI(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar called bool\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\t_ = ctx\n\t\trequire.True(t, opts.YOLO)\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\t_ = ctx\n\t\t_ = application\n\t\t_ = initialSessionID\n\t\tcalled = true\n\t\treturn nil\n\t}\n\n\tstdout, stderr, err := executeRoot(t, \"--yolo\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Empty(t, stderr)\n\trequire.True(t, called)\n}\n\nfunc TestHelpTextListsSubcommands(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"--help\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"Usage:\")\n\tfor _, name := range []string{\"run\", \"login\", \"logout\", \"models\", \"sessions\", \"stats\", \"budget\", \"update-providers\", \"config\", \"version\"} {\n\t\trequire.Contains(t, stdout, name)\n\t}\n}\n\nfunc TestVersionFormat(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"version\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Regexp(t, regexp.MustCompile(`^bharatcode v\\d+\\.\\d+\\.\\d+(-[a-z0-9.-]+)? \\([0-9a-f]{7,}\\)\\n$`), stdout)\n}\n\nfunc TestModelsTable(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"models\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"PROVIDER\")\n\trequire.Contains(t, stdout, \"deepseek-chat\")\n\trequire.Contains(t, stdout, \"INPUT$/MTOK\")\n\trequire.Contains(t, stdout, \" \")\n}\n\nfunc TestStatsEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tdataDir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dataDir)\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"stats\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"no usage recorded\\n\", stdout)\n}\n\nfunc TestBudgetSetPersists(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"budget\", \"set\", \"--month\", \"500\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Monthly budget set to ₹500\\n\", stdout)\n\n\traw, err := os.ReadFile(configPath)\n\trequire.NoError(t, err)\n\tvar cfg map[string]any\n\trequire.NoError(t, json.Unmarshal(raw, &cfg))\n\tledger, ok := cfg[\"ledger\"].(map[string]any)\n\trequire.True(t, ok)\n\trequire.Equal(t, float64(500), ledger[\"max_inr_per_month\"])\n}\n\nfunc TestProfileFlagForwardedToApp(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar gotProfile string\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\tgotProfile = opts.Profile\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t, \"--profile\", \"ollama\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"ollama\", gotProfile)\n}\n\nfunc TestProfileFlagAbsentDefaultsToEmpty(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar gotProfile string\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\tgotProfile = opts.Profile\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t)\n\trequire.NoError(t, err)\n\trequire.Empty(t, gotProfile)\n}\n\nfunc TestLoginLogoutFakeKeyring(t *testing.T) {\n\toldKeyring := keyring\n\tfake := newFakeKeyring()\n\tkeyring = fake\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged in to openai\\n\", stdout)\n\trequire.Equal(t, \"secret\", fake.values[\"bharatcode/openai\"])\n\n\tstdout, stderr, err = executeRoot(t, \"logout\", \"openai\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged out of openai\\n\", stdout)\n\trequire.NotContains(t, fake.values, \"bharatcode/openai\")\n}\n\nfunc TestLoginKeyringUnavailable(t *testing.T) {\n\toldKeyring := keyring\n\tkeyring = failingKeyring{err: errors.New(\"boom\")}\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: keyring unavailable: boom\")\n}\n\nfunc TestSessionShowNotFound(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"show\", \"bogus-id\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: session bogus-id not found\")\n}\n\nfunc TestSessionsListEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"list\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Equal(t, \"no sessions\\n\", stderr)\n}\n\nfunc TestRunStdinPromptValidation(t *testing.T) {\n\tcmd := newRunCmd()\n\tcmd.SetIn(strings.NewReader(\"hello\\n\"))\n\tprompt, err := readPrompt(cmd, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello\", prompt)\n}\n\nfunc executeRoot(t *testing.T, args ...string) (string, string, error) {\n\tt.Helper()\n\troot := newRootCmd()\n\tvar stdout bytes.Buffer\n\tvar stderr bytes.Buffer\n\troot.SetOut(&stdout)\n\troot.SetErr(&stderr)\n\troot.SetArgs(args)\n\terr := executeCommand(context.Background(), root)\n\treturn stdout.String(), stderr.String(), err\n}\n\nfunc writeConfig(t *testing.T, body string) string {\n\tt.Helper()\n\tpath := filepath.Join(t.TempDir(), \"config.json\")\n\trequire.NoError(t, os.WriteFile(path, []byte(body), 0o600))\n\treturn path\n}\n\nfunc defaultTestConfig() string {\n\treturn `{\n \"providers\": [\n {\n \"name\": \"deepseek\",\n \"type\": \"openai_compatible\",\n \"base_url\": \"https://api.deepseek.com/v1\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"models\": [\"deepseek-chat\"]\n }\n ],\n \"models\": [\n {\n \"id\": \"deepseek-chat\",\n \"provider\": \"deepseek\",\n \"context_window\": 64000,\n \"input_price_per_mtok_usd\": 0.27,\n \"output_price_per_mtok_usd\": 1.10,\n \"supports_images\": false,\n \"supports_tools\": true\n }\n ],\n \"agents\": [\n {\n \"name\": \"coder\",\n \"model\": \"deepseek-chat\",\n \"system_prompt\": \"You are concise.\"\n }\n ],\n \"ledger\": {\n \"currency\": \"INR\",\n \"usd_inr_rate\": 83.5\n }\n}`\n}\n\ntype fakeKeyring struct {\n\tvalues map[string]string\n}\n\nfunc newFakeKeyring() *fakeKeyring {\n\treturn &fakeKeyring{values: make(map[string]string)}\n}\n\nfunc (k *fakeKeyring) Get(service, account string) (string, error) {\n\tvalue, ok := k.values[service+\"/\"+account]\n\tif !ok {\n\t\treturn \"\", os.ErrNotExist\n\t}\n\treturn value, nil\n}\n\nfunc (k *fakeKeyring) Set(service, account, secret string) error {\n\tk.values[service+\"/\"+account] = secret\n\treturn nil\n}\n\nfunc (k *fakeKeyring) Delete(service, account string) error {\n\tdelete(k.values, service+\"/\"+account)\n\treturn nil\n}\n\ntype failingKeyring struct {\n\terr error\n}\n\nfunc (k failingKeyring) Get(service, account string) (string, error) {\n\t_ = service\n\t_ = account\n\treturn \"\", k.err\n}\n\nfunc (k failingKeyring) Set(service, account, secret string) error {\n\t_ = service\n\t_ = account\n\t_ = secret\n\treturn k.err\n}\n\nfunc (k failingKeyring) Delete(service, account string) error {\n\t_ = service\n\t_ = account\n\treturn k.err\n}\n\nfunc TestContinueFlagPassesSessionID(t *testing.T) {\n\toldRunTUI := runTUI\n\tdefer func() { runTUI = oldRunTUI }()\n\n\t// Use a real on-disk DB so sessions survive across app.New calls.\n\tdir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dir)\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tprojDir := t.TempDir()\n\n\t// Seed one session via a short-lived app.New / Close pair.\n\tseedApp, err := app.New(context.Background(), app.Options{ConfigPath: configPath, ProjectDir: projDir})\n\trequire.NoError(t, err)\n\tsess := &session.Session{ID: \"abc123\", Title: \"Test\", ProjectPath: projDir}\n\trequire.NoError(t, seedApp.Sessions.Create(context.Background(), sess))\n\trequire.NoError(t, seedApp.Close(context.Background()))\n\n\t// Replace only runTUI — let buildApp call newApp for real so resolveInitialSession\n\t// reads from the same DB where we seeded the session above.\n\tvar gotID string\n\trunTUI = func(ctx context.Context, a *app.App, initialSessionID string) error {\n\t\tgotID = initialSessionID\n\t\treturn nil\n\t}\n\n\t_, _, err = executeRoot(t, \"--config\", configPath, \"--project-dir\", projDir, \"--continue\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"abc123\", gotID, \"--continue should pass the latest session ID to the TUI\")\n}\n\nfunc TestContinueFlagNoSessions(t *testing.T) {\n\toldRunTUI := runTUI\n\tdefer func() { runTUI = oldRunTUI }()\n\n\tdir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dir)\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\n\tvar gotID string\n\trunTUI = func(ctx context.Context, a *app.App, initialSessionID string) error {\n\t\tgotID = initialSessionID\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"--continue\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, gotID, \"--continue with no sessions should pass empty string (start fresh)\")\n}\n\nvar _ = cobra.Command{}\n"}, "c9a84dfd05e13aea751b051d956d4f54e7902ffd": {"t": 1785755419.899512, "n": 8781, "h": "d0ac4fcc48634003", "c": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/arbazkhan971/bharatcode/internal/app\"\n\t\"github.com/arbazkhan971/bharatcode/internal/session\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRootNoArgsStartsTUI(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar called bool\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\t_ = ctx\n\t\trequire.True(t, opts.YOLO)\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App, initialSessionID string) error {\n\t\t_ = ctx\n\t\t_ = application\n\t\t_ = initialSessionID\n\t\tcalled = true\n\t\treturn nil\n\t}\n\n\tstdout, stderr, err := executeRoot(t, \"--yolo\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Empty(t, stderr)\n\trequire.True(t, called)\n}\n\nfunc TestHelpTextListsSubcommands(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"--help\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"Usage:\")\n\tfor _, name := range []string{\"run\", \"login\", \"logout\", \"models\", \"sessions\", \"stats\", \"budget\", \"update-providers\", \"config\", \"version\"} {\n\t\trequire.Contains(t, stdout, name)\n\t}\n}\n\nfunc TestVersionFormat(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"version\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Regexp(t, regexp.MustCompile(`^bharatcode v\\d+\\.\\d+\\.\\d+(-[a-z0-9.-]+)? \\([0-9a-f]{7,}\\)\\n$`), stdout)\n}\n\nfunc TestModelsTable(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"models\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"PROVIDER\")\n\trequire.Contains(t, stdout, \"deepseek-chat\")\n\trequire.Contains(t, stdout, \"INPUT$/MTOK\")\n\trequire.Contains(t, stdout, \" \")\n}\n\nfunc TestStatsEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tdataDir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dataDir)\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"stats\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"no usage recorded\\n\", stdout)\n}\n\nfunc TestBudgetSetPersists(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"budget\", \"set\", \"--month\", \"500\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Monthly budget set to ₹500\\n\", stdout)\n\n\traw, err := os.ReadFile(configPath)\n\trequire.NoError(t, err)\n\tvar cfg map[string]any\n\trequire.NoError(t, json.Unmarshal(raw, &cfg))\n\tledger, ok := cfg[\"ledger\"].(map[string]any)\n\trequire.True(t, ok)\n\trequire.Equal(t, float64(500), ledger[\"max_inr_per_month\"])\n}\n\nfunc TestLoginLogoutFakeKeyring(t *testing.T) {\n\toldKeyring := keyring\n\tfake := newFakeKeyring()\n\tkeyring = fake\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged in to openai\\n\", stdout)\n\trequire.Equal(t, \"secret\", fake.values[\"bharatcode/openai\"])\n\n\tstdout, stderr, err = executeRoot(t, \"logout\", \"openai\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged out of openai\\n\", stdout)\n\trequire.NotContains(t, fake.values, \"bharatcode/openai\")\n}\n\nfunc TestLoginKeyringUnavailable(t *testing.T) {\n\toldKeyring := keyring\n\tkeyring = failingKeyring{err: errors.New(\"boom\")}\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: keyring unavailable: boom\")\n}\n\nfunc TestSessionShowNotFound(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"show\", \"bogus-id\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: session bogus-id not found\")\n}\n\nfunc TestSessionsListEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"list\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Equal(t, \"no sessions\\n\", stderr)\n}\n\nfunc TestRunStdinPromptValidation(t *testing.T) {\n\tcmd := newRunCmd()\n\tcmd.SetIn(strings.NewReader(\"hello\\n\"))\n\tprompt, err := readPrompt(cmd, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello\", prompt)\n}\n\nfunc executeRoot(t *testing.T, args ...string) (string, string, error) {\n\tt.Helper()\n\troot := newRootCmd()\n\tvar stdout bytes.Buffer\n\tvar stderr bytes.Buffer\n\troot.SetOut(&stdout)\n\troot.SetErr(&stderr)\n\troot.SetArgs(args)\n\terr := executeCommand(context.Background(), root)\n\treturn stdout.String(), stderr.String(), err\n}\n\nfunc writeConfig(t *testing.T, body string) string {\n\tt.Helper()\n\tpath := filepath.Join(t.TempDir(), \"config.json\")\n\trequire.NoError(t, os.WriteFile(path, []byte(body), 0o600))\n\treturn path\n}\n\nfunc defaultTestConfig() string {\n\treturn `{\n \"providers\": [\n {\n \"name\": \"deepseek\",\n \"type\": \"openai_compatible\",\n \"base_url\": \"https://api.deepseek.com/v1\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"models\": [\"deepseek-chat\"]\n }\n ],\n \"models\": [\n {\n \"id\": \"deepseek-chat\",\n \"provider\": \"deepseek\",\n \"context_window\": 64000,\n \"input_price_per_mtok_usd\": 0.27,\n \"output_price_per_mtok_usd\": 1.10,\n \"supports_images\": false,\n \"supports_tools\": true\n }\n ],\n \"agents\": [\n {\n \"name\": \"coder\",\n \"model\": \"deepseek-chat\",\n \"system_prompt\": \"You are concise.\"\n }\n ],\n \"ledger\": {\n \"currency\": \"INR\",\n \"usd_inr_rate\": 83.5\n }\n}`\n}\n\ntype fakeKeyring struct {\n\tvalues map[string]string\n}\n\nfunc newFakeKeyring() *fakeKeyring {\n\treturn &fakeKeyring{values: make(map[string]string)}\n}\n\nfunc (k *fakeKeyring) Get(service, account string) (string, error) {\n\tvalue, ok := k.values[service+\"/\"+account]\n\tif !ok {\n\t\treturn \"\", os.ErrNotExist\n\t}\n\treturn value, nil\n}\n\nfunc (k *fakeKeyring) Set(service, account, secret string) error {\n\tk.values[service+\"/\"+account] = secret\n\treturn nil\n}\n\nfunc (k *fakeKeyring) Delete(service, account string) error {\n\tdelete(k.values, service+\"/\"+account)\n\treturn nil\n}\n\ntype failingKeyring struct {\n\terr error\n}\n\nfunc (k failingKeyring) Get(service, account string) (string, error) {\n\t_ = service\n\t_ = account\n\treturn \"\", k.err\n}\n\nfunc (k failingKeyring) Set(service, account, secret string) error {\n\t_ = service\n\t_ = account\n\t_ = secret\n\treturn k.err\n}\n\nfunc (k failingKeyring) Delete(service, account string) error {\n\t_ = service\n\t_ = account\n\treturn k.err\n}\n\nfunc TestContinueFlagPassesSessionID(t *testing.T) {\n\toldRunTUI := runTUI\n\tdefer func() { runTUI = oldRunTUI }()\n\n\t// Use a real on-disk DB so sessions survive across app.New calls.\n\tdir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dir)\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tprojDir := t.TempDir()\n\n\t// Seed one session via a short-lived app.New / Close pair.\n\tseedApp, err := app.New(context.Background(), app.Options{ConfigPath: configPath, ProjectDir: projDir})\n\trequire.NoError(t, err)\n\tsess := &session.Session{ID: \"abc123\", Title: \"Test\", ProjectPath: projDir}\n\trequire.NoError(t, seedApp.Sessions.Create(context.Background(), sess))\n\trequire.NoError(t, seedApp.Close(context.Background()))\n\n\t// Replace only runTUI — let buildApp call newApp for real so resolveInitialSession\n\t// reads from the same DB where we seeded the session above.\n\tvar gotID string\n\trunTUI = func(ctx context.Context, a *app.App, initialSessionID string) error {\n\t\tgotID = initialSessionID\n\t\treturn nil\n\t}\n\n\t_, _, err = executeRoot(t, \"--config\", configPath, \"--project-dir\", projDir, \"--continue\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"abc123\", gotID, \"--continue should pass the latest session ID to the TUI\")\n}\n\nfunc TestContinueFlagNoSessions(t *testing.T) {\n\toldRunTUI := runTUI\n\tdefer func() { runTUI = oldRunTUI }()\n\n\tdir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dir)\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\n\tvar gotID string\n\trunTUI = func(ctx context.Context, a *app.App, initialSessionID string) error {\n\t\tgotID = initialSessionID\n\t\treturn nil\n\t}\n\n\t_, _, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"--continue\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, gotID, \"--continue with no sessions should pass empty string (start fresh)\")\n}\n\nvar _ = cobra.Command{}\n"}, "0e4b8e18e120a705101ab1d2789959ae8569f732": {"t": 1785755421.108669, "n": 6889, "h": "c9fe793ae60e8248", "c": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/arbazkhan971/bharatcode/internal/app\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRootNoArgsStartsTUI(t *testing.T) {\n\toldNewApp := newApp\n\toldRunTUI := runTUI\n\tdefer func() {\n\t\tnewApp = oldNewApp\n\t\trunTUI = oldRunTUI\n\t}()\n\n\tvar called bool\n\tnewApp = func(ctx context.Context, opts app.Options) (*app.App, error) {\n\t\t_ = ctx\n\t\trequire.True(t, opts.YOLO)\n\t\treturn nil, nil\n\t}\n\trunTUI = func(ctx context.Context, application *app.App) error {\n\t\t_ = ctx\n\t\t_ = application\n\t\tcalled = true\n\t\treturn nil\n\t}\n\n\tstdout, stderr, err := executeRoot(t, \"--yolo\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Empty(t, stderr)\n\trequire.True(t, called)\n}\n\nfunc TestHelpTextListsSubcommands(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"--help\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"Usage:\")\n\tfor _, name := range []string{\"run\", \"login\", \"logout\", \"models\", \"sessions\", \"stats\", \"budget\", \"update-providers\", \"config\", \"version\"} {\n\t\trequire.Contains(t, stdout, name)\n\t}\n}\n\nfunc TestVersionFormat(t *testing.T) {\n\tstdout, stderr, err := executeRoot(t, \"version\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Regexp(t, regexp.MustCompile(`^bharatcode v\\d+\\.\\d+\\.\\d+(-[a-z0-9.-]+)? \\([0-9a-f]{7,}\\)\\n$`), stdout)\n}\n\nfunc TestModelsTable(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"models\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Contains(t, stdout, \"PROVIDER\")\n\trequire.Contains(t, stdout, \"deepseek-chat\")\n\trequire.Contains(t, stdout, \"INPUT$/MTOK\")\n\trequire.Contains(t, stdout, \" \")\n}\n\nfunc TestStatsEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tdataDir := t.TempDir()\n\tt.Setenv(\"XDG_DATA_HOME\", dataDir)\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"stats\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"no usage recorded\\n\", stdout)\n}\n\nfunc TestBudgetSetPersists(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"budget\", \"set\", \"--month\", \"500\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Monthly budget set to ₹500\\n\", stdout)\n\n\traw, err := os.ReadFile(configPath)\n\trequire.NoError(t, err)\n\tvar cfg map[string]any\n\trequire.NoError(t, json.Unmarshal(raw, &cfg))\n\tledger, ok := cfg[\"ledger\"].(map[string]any)\n\trequire.True(t, ok)\n\trequire.Equal(t, float64(500), ledger[\"max_inr_per_month\"])\n}\n\nfunc TestLoginLogoutFakeKeyring(t *testing.T) {\n\toldKeyring := keyring\n\tfake := newFakeKeyring()\n\tkeyring = fake\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged in to openai\\n\", stdout)\n\trequire.Equal(t, \"secret\", fake.values[\"bharatcode/openai\"])\n\n\tstdout, stderr, err = executeRoot(t, \"logout\", \"openai\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stderr)\n\trequire.Equal(t, \"Logged out of openai\\n\", stdout)\n\trequire.NotContains(t, fake.values, \"bharatcode/openai\")\n}\n\nfunc TestLoginKeyringUnavailable(t *testing.T) {\n\toldKeyring := keyring\n\tkeyring = failingKeyring{err: errors.New(\"boom\")}\n\tdefer func() { keyring = oldKeyring }()\n\n\tstdout, stderr, err := executeRoot(t, \"login\", \"openai\", \"--token\", \"secret\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: keyring unavailable: boom\")\n}\n\nfunc TestSessionShowNotFound(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"show\", \"bogus-id\")\n\trequire.Error(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Contains(t, stderr, \"Error: session bogus-id not found\")\n}\n\nfunc TestSessionsListEmpty(t *testing.T) {\n\tconfigPath := writeConfig(t, defaultTestConfig())\n\tt.Setenv(\"XDG_DATA_HOME\", t.TempDir())\n\tstdout, stderr, err := executeRoot(t, \"--config\", configPath, \"--project-dir\", t.TempDir(), \"sessions\", \"list\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, stdout)\n\trequire.Equal(t, \"no sessions\\n\", stderr)\n}\n\nfunc TestRunStdinPromptValidation(t *testing.T) {\n\tcmd := newRunCmd()\n\tcmd.SetIn(strings.NewReader(\"hello\\n\"))\n\tprompt, err := readPrompt(cmd, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello\", prompt)\n}\n\nfunc executeRoot(t *testing.T, args ...string) (string, string, error) {\n\tt.Helper()\n\troot := newRootCmd()\n\tvar stdout bytes.Buffer\n\tvar stderr bytes.Buffer\n\troot.SetOut(&stdout)\n\troot.SetErr(&stderr)\n\troot.SetArgs(args)\n\terr := executeCommand(context.Background(), root)\n\treturn stdout.String(), stderr.String(), err\n}\n\nfunc writeConfig(t *testing.T, body string) string {\n\tt.Helper()\n\tpath := filepath.Join(t.TempDir(), \"config.json\")\n\trequire.NoError(t, os.WriteFile(path, []byte(body), 0o600))\n\treturn path\n}\n\nfunc defaultTestConfig() string {\n\treturn `{\n \"providers\": [\n {\n \"name\": \"deepseek\",\n \"type\": \"openai_compatible\",\n \"base_url\": \"https://api.deepseek.com/v1\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"models\": [\"deepseek-chat\"]\n }\n ],\n \"models\": [\n {\n \"id\": \"deepseek-chat\",\n \"provider\": \"deepseek\",\n \"context_window\": 64000,\n \"input_price_per_mtok_usd\": 0.27,\n \"output_price_per_mtok_usd\": 1.10,\n \"supports_images\": false,\n \"supports_tools\": true\n }\n ],\n \"agents\": [\n {\n \"name\": \"coder\",\n \"model\": \"deepseek-chat\",\n \"system_prompt\": \"You are concise.\"\n }\n ],\n \"ledger\": {\n \"currency\": \"INR\",\n \"usd_inr_rate\": 83.5\n }\n}`\n}\n\ntype fakeKeyring struct {\n\tvalues map[string]string\n}\n\nfunc newFakeKeyring() *fakeKeyring {\n\treturn &fakeKeyring{values: make(map[string]string)}\n}\n\nfunc (k *fakeKeyring) Get(service, account string) (string, error) {\n\tvalue, ok := k.values[service+\"/\"+account]\n\tif !ok {\n\t\treturn \"\", os.ErrNotExist\n\t}\n\treturn value, nil\n}\n\nfunc (k *fakeKeyring) Set(service, account, secret string) error {\n\tk.values[service+\"/\"+account] = secret\n\treturn nil\n}\n\nfunc (k *fakeKeyring) Delete(service, account string) error {\n\tdelete(k.values, service+\"/\"+account)\n\treturn nil\n}\n\ntype failingKeyring struct {\n\terr error\n}\n\nfunc (k failingKeyring) Get(service, account string) (string, error) {\n\t_ = service\n\t_ = account\n\treturn \"\", k.err\n}\n\nfunc (k failingKeyring) Set(service, account, secret string) error {\n\t_ = service\n\t_ = account\n\t_ = secret\n\treturn k.err\n}\n\nfunc (k failingKeyring) Delete(service, account string) error {\n\t_ = service\n\t_ = account\n\treturn k.err\n}\n\nvar _ = cobra.Command{}\n"}}, "internal/llm/compat_test.go": {"4520f16f286aaa74f7174c9b469caa636f1b3159": {"t": 1785746086.170105, "n": 14048, "h": "14189f922d724b56", "c": "package llm\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/arbazkhan971/bharatcode/internal/config\"\n\t\"github.com/arbazkhan971/bharatcode/internal/message\"\n)\n\n// ptr returns a pointer to v — a convenience for test literals.\nfunc ptr[T any](v T) *T { return &v }\n\n// TestCompatContextWindowOverride verifies that a model with a\n// Compat.ContextWindow set has its context window resolved to the override\n// value, while a model with no Compat block keeps the heuristic default.\nfunc TestCompatContextWindowOverride(t *testing.T) {\n\t// deepseek-chat: heuristic resolves to 131072 via the \"deepseek\" rule.\n\t// With a Compat override of 65536 it should use 65536.\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{{\n\t\t\tName: \"deepseek\",\n\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tModels: []string{\"deepseek-chat\"},\n\t\t}},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"deepseek-chat\",\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\t// Compat.ContextWindow overrides the heuristic.\n\t\t\t\tCompat: &config.ModelCompat{\n\t\t\t\t\tContextWindow: ptr(65536),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tmodels := reg.ListModels()\n\trequire.Len(t, models, 1)\n\trequire.Equal(t, 65536, models[0].ContextWindow,\n\t\t\"Compat.ContextWindow must override the heuristic\")\n\t// The Compat block must be threaded through to the llm.Model.\n\trequire.NotNil(t, models[0].Compat)\n\trequire.NotNil(t, models[0].Compat.ContextWindow)\n\trequire.Equal(t, 65536, *models[0].Compat.ContextWindow)\n}\n\n// TestCompatNoBlockUsesHeuristic verifies that a model with no Compat block\n// uses the inferContextWindow heuristic unchanged (purely additive check).\nfunc TestCompatNoBlockUsesHeuristic(t *testing.T) {\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{{\n\t\t\tName: \"deepseek\",\n\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tModels: []string{\"deepseek-chat\"},\n\t\t}},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"deepseek-chat\",\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\t// No Compat block — heuristic must apply.\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tmodels := reg.ListModels()\n\trequire.Len(t, models, 1)\n\trequire.Equal(t, inferContextWindow(\"deepseek-chat\"), models[0].ContextWindow,\n\t\t\"no Compat block must leave inferContextWindow as the sole source\")\n\trequire.Nil(t, models[0].Compat,\n\t\t\"no Compat block must leave Compat nil on the llm.Model\")\n}\n\n// TestCompatThinkingFormatHonored verifies that resolveThinkingFormat returns\n// the model's explicit Compat.ThinkingFormat when set, overriding any\n// URL-based auto-detection.\nfunc TestCompatThinkingFormatHonored(t *testing.T) {\n\t// openrouter.ai URL would normally auto-detect ThinkingFormatOpenRouter.\n\t// The model's explicit Compat.ThinkingFormat = \"deepseek\" must win.\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"some-model\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tThinkingFormat: config.ThinkingFormatDeepSeek,\n\t\t\t},\n\t\t},\n\t}\n\tgot := resolveThinkingFormat(models, \"some-model\", \"https://openrouter.ai/api/v1\")\n\trequire.Equal(t, config.ThinkingFormatDeepSeek, got,\n\t\t\"explicit Compat.ThinkingFormat must override URL auto-detection\")\n}\n\n// TestCompatThinkingFormatAutoDetect verifies that the URL-based auto-detection\n// fires when the model has no Compat block.\nfunc TestCompatThinkingFormatAutoDetect(t *testing.T) {\n\tcases := []struct {\n\t\tbaseURL string\n\t\twant config.ThinkingFormat\n\t}{\n\t\t{\"https://openrouter.ai/api/v1\", config.ThinkingFormatOpenRouter},\n\t\t{\"https://api.deepseek.com/v1\", config.ThinkingFormatDeepSeek},\n\t\t{\"https://zai.ai/v1\", config.ThinkingFormatDeepSeek},\n\t\t{\"https://dashscope.aliyuncs.com/compatible-mode/v1\", config.ThinkingFormatQwen},\n\t\t{\"https://api.openai.com/v1\", config.ThinkingFormatDefault},\n\t\t{\"https://api.groq.com/openai/v1\", config.ThinkingFormatDefault},\n\t}\n\t// No compat block on the model.\n\tmodels := []Model{{ID: \"some-model\"}}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.baseURL, func(t *testing.T) {\n\t\t\tgot := resolveThinkingFormat(models, \"some-model\", tc.baseURL)\n\t\t\trequire.Equal(t, tc.want, got,\n\t\t\t\t\"URL %q should auto-detect thinking format %q\", tc.baseURL, tc.want)\n\t\t})\n\t}\n}\n\n// TestCompatNoneThinkingFormatSuppresses verifies that ThinkingFormatNone\n// resolves correctly even on a URL that would otherwise auto-detect OpenRouter.\nfunc TestCompatNoneThinkingFormatSuppresses(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"some-model\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tThinkingFormat: config.ThinkingFormatNone,\n\t\t\t},\n\t\t},\n\t}\n\tgot := resolveThinkingFormat(models, \"some-model\", \"https://openrouter.ai/api/v1\")\n\trequire.Equal(t, config.ThinkingFormatNone, got,\n\t\t\"ThinkingFormatNone must suppress even the OpenRouter URL auto-detection\")\n}\n\n// TestCompatReasoningOverrideTrue verifies that Compat.Reasoning=true forces\n// the reasoning-model branch regardless of the id heuristic.\nfunc TestCompatReasoningOverrideTrue(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"my-quirky-reasoner\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tReasoning: ptr(true),\n\t\t\t},\n\t\t},\n\t}\n\t// The id \"my-quirky-reasoner\" would NOT be classified as a reasoning model\n\t// by the heuristic (it has no o-series or gpt-5 prefix).\n\trequire.False(t, isReasoningModel(\"my-quirky-reasoner\"),\n\t\t\"sanity: heuristic must not classify this id as reasoning\")\n\trequire.True(t, isReasoningModelForRequest(models, \"my-quirky-reasoner\"),\n\t\t\"Compat.Reasoning=true must override the heuristic\")\n}\n\n// TestCompatReasoningOverrideFalse verifies that Compat.Reasoning=false forces\n// the chat-model branch even for an id the heuristic would classify as reasoning.\nfunc TestCompatReasoningOverrideFalse(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"o3-mini\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tReasoning: ptr(false),\n\t\t\t},\n\t\t},\n\t}\n\t// The id \"o3-mini\" IS classified as a reasoning model by the heuristic.\n\trequire.True(t, isReasoningModel(\"o3-mini\"),\n\t\t\"sanity: heuristic must classify o3-mini as reasoning\")\n\trequire.False(t, isReasoningModelForRequest(models, \"o3-mini\"),\n\t\t\"Compat.Reasoning=false must override the heuristic\")\n}\n\n// TestCompatReasoningNilFallsThrough verifies that Compat.Reasoning=nil\n// defers to the id heuristic (no behavior change for existing models).\nfunc TestCompatReasoningNilFallsThrough(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"o3-mini\",\n\t\t\tCompat: &config.ModelCompat{}, // Compat block present but Reasoning is nil\n\t\t},\n\t}\n\trequire.True(t, isReasoningModelForRequest(models, \"o3-mini\"),\n\t\t\"nil Compat.Reasoning must fall through to the id heuristic\")\n}\n\n// TestCompatSupportsImagesOverride verifies that Compat.SupportsImages\n// overrides the catalog flag.\nfunc TestCompatSupportsImagesOverride(t *testing.T) {\n\t// A model whose catalog entry says SupportsImages: false, but Compat\n\t// overrides to true.\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{{\n\t\t\tName: \"myprovider\",\n\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\tBaseURL: \"https://example.invalid/v1\",\n\t\t\tModels: []string{\"my-vision-model\"},\n\t\t}},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"my-vision-model\",\n\t\t\t\tProvider: \"myprovider\",\n\t\t\t\tSupportsImages: false, // catalog says no images\n\t\t\t\tCompat: &config.ModelCompat{\n\t\t\t\t\tSupportsImages: ptr(true), // compat says yes\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tmodels := reg.ListModels()\n\trequire.Len(t, models, 1)\n\trequire.True(t, models[0].SupportsImages,\n\t\t\"Compat.SupportsImages=true must override the catalog SupportsImages=false\")\n}\n\n// TestCompatStrictToolsRequest verifies that StrictTools: true adds strict:true\n// to each tool definition in the built request body.\nfunc TestCompatStrictToolsRequest(t *testing.T) {\n\tcompat := &config.ModelCompat{StrictTools: true}\n\treq := Request{\n\t\tModel: \"some-model\",\n\t\tTools: []Tool{\n\t\t\t{Name: \"my_tool\", Description: \"does something\"},\n\t\t},\n\t}\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, compat, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, body.Tools, 1)\n\trequire.NotNil(t, body.Tools[0].Function.Strict,\n\t\t\"StrictTools must set Strict on every tool function\")\n\trequire.True(t, *body.Tools[0].Function.Strict,\n\t\t\"strict must be true\")\n}\n\n// TestCompatStrictToolsNotSetByDefault verifies that without StrictTools the\n// Strict field is nil (omitted), preserving baseline OpenAI behavior.\nfunc TestCompatStrictToolsNotSetByDefault(t *testing.T) {\n\treq := Request{\n\t\tModel: \"gpt-4o\",\n\t\tTools: []Tool{\n\t\t\t{Name: \"my_tool\"},\n\t\t},\n\t}\n\t// No compat block.\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, nil, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, body.Tools, 1)\n\trequire.Nil(t, body.Tools[0].Function.Strict,\n\t\t\"without StrictTools the Strict field must be nil (omitted)\")\n}\n\n// TestCompatMaxTokensOverride verifies that Compat.MaxTokens replaces the\n// request's MaxTokens in the built request body.\nfunc TestCompatMaxTokensOverride(t *testing.T) {\n\tcompat := &config.ModelCompat{\n\t\tMaxTokens: ptr(2048),\n\t}\n\treq := Request{\n\t\tModel: \"gpt-4o\",\n\t\tMaxTokens: 8192, // caller-requested value\n\t}\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, compat, nil)\n\trequire.NoError(t, err)\n\t// gpt-4o is not a reasoning model, so MaxTokens lands in body.MaxTokens.\n\trequire.Equal(t, 2048, body.MaxTokens,\n\t\t\"Compat.MaxTokens must override the request MaxTokens\")\n}\n\n// TestCompatNoBlockMaxTokensPreserved verifies that without a Compat block the\n// request MaxTokens passes through unchanged.\nfunc TestCompatNoBlockMaxTokensPreserved(t *testing.T) {\n\treq := Request{\n\t\tModel: \"gpt-4o\",\n\t\tMaxTokens: 8192,\n\t}\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, nil, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 8192, body.MaxTokens,\n\t\t\"without Compat block the original request MaxTokens must pass through\")\n}\n\n// TestCompatToolResultQuirkUserContent verifies that ToolResultQuirkUserContent\n// converts a ToolResultBlock into a user-role message instead of a tool message.\nfunc TestCompatToolResultQuirkUserContent(t *testing.T) {\n\tresult := message.ToolResultBlock{\n\t\tToolUseID: \"call_123\",\n\t\tContent: \"42\",\n\t}\n\tmsgs := toolResultMessages(result, config.ToolResultQuirkUserContent)\n\trequire.Len(t, msgs, 1)\n\trequire.Equal(t, \"user\", msgs[0].Role,\n\t\t\"ToolResultQuirkUserContent must produce a user-role message\")\n\trequire.Contains(t, msgs[0].Content.(string), \"call_123\",\n\t\t\"the tool call id must appear in the user message\")\n\trequire.Contains(t, msgs[0].Content.(string), \"42\",\n\t\t\"the tool result content must appear in the user message\")\n}\n\n// TestCompatToolResultQuirkNonePreservesStandard verifies that the zero-value\n// quirk (ToolResultQuirkNone) emits the standard tool-role message format.\nfunc TestCompatToolResultQuirkNonePreservesStandard(t *testing.T) {\n\tresult := message.ToolResultBlock{\n\t\tToolUseID: \"call_456\",\n\t\tContent: \"hello\",\n\t}\n\tmsgs := toolResultMessages(result, config.ToolResultQuirkNone)\n\trequire.Len(t, msgs, 1)\n\trequire.Equal(t, \"tool\", msgs[0].Role,\n\t\t\"ToolResultQuirkNone must produce a standard tool-role message\")\n\trequire.Equal(t, \"call_456\", msgs[0].ToolCallID,\n\t\t\"tool_call_id must be set on the standard tool message\")\n}\n\n// TestCompatRegistryRoundTrip is the end-to-end test called out in the feature\n// spec: a Model with a Compat block overriding ContextWindow and ThinkingFormat\n// is honored; a Model with no Compat block uses the heuristic defaults unchanged.\nfunc TestCompatRegistryRoundTrip(t *testing.T) {\n\tcustomWindow := 65536\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{\n\t\t\t{\n\t\t\t\tName: \"quirky\",\n\t\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\t\tModels: []string{\"deepseek-r1\", \"gpt-4o\"},\n\t\t\t},\n\t\t},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"deepseek-r1\",\n\t\t\t\tProvider: \"quirky\",\n\t\t\t\t// Compat overrides context window and pins the thinking format.\n\t\t\t\tCompat: &config.ModelCompat{\n\t\t\t\t\tContextWindow: &customWindow,\n\t\t\t\t\tThinkingFormat: config.ThinkingFormatDeepSeek,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID: \"gpt-4o\",\n\t\t\t\tProvider: \"quirky\",\n\t\t\t\t// No Compat block: must use heuristic defaults unchanged.\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tbyID := make(map[string]Model)\n\tfor _, m := range reg.ListModels() {\n\t\tbyID[m.ID] = m\n\t}\n\n\t// --- Model WITH Compat block ---\n\tr1 := byID[\"deepseek-r1\"]\n\trequire.Equal(t, customWindow, r1.ContextWindow,\n\t\t\"deepseek-r1: Compat.ContextWindow must override the heuristic\")\n\trequire.NotNil(t, r1.Compat, \"deepseek-r1: Compat must be non-nil on the llm.Model\")\n\trequire.Equal(t, config.ThinkingFormatDeepSeek, r1.Compat.ThinkingFormat,\n\t\t\"deepseek-r1: ThinkingFormat must be threaded through\")\n\n\t// resolveThinkingFormat must honor the explicit compat even though the\n\t// baseURL (deepseek.com) would produce the same result by auto-detection.\n\t// Use a different baseURL to prove the explicit flag wins over auto-detection.\n\tallModels := reg.ListModels()\n\tgotFmt := resolveThinkingFormat(allModels, \"deepseek-r1\", \"https://api.openai.com/v1\")\n\trequire.Equal(t, config.ThinkingFormatDeepSeek, gotFmt,\n\t\t\"resolveThinkingFormat must return the explicit Compat.ThinkingFormat even against an OpenAI base URL\")\n\n\t// --- Model WITHOUT Compat block ---\n\tgpt4o := byID[\"gpt-4o\"]\n\theuristicWindow := inferContextWindow(\"gpt-4o\")\n\trequire.Equal(t, heuristicWindow, gpt4o.ContextWindow,\n\t\t\"gpt-4o: no Compat block must leave the heuristic context window unchanged\")\n\trequire.Nil(t, gpt4o.Compat, \"gpt-4o: Compat must be nil when no compat block is configured\")\n\n\tgotFmtNoCompat := resolveThinkingFormat(allModels, \"gpt-4o\", \"https://api.openai.com/v1\")\n\trequire.Equal(t, config.ThinkingFormatDefault, gotFmtNoCompat,\n\t\t\"gpt-4o on OpenAI base URL must resolve to ThinkingFormatDefault (no change)\")\n}\n"}, "a5506bb43a58339c204eb6ed8724ec4319971ec1": {"t": 1785755415.9253263, "n": 14048, "h": "14189f922d724b56", "c": "package llm\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/arbazkhan971/bharatcode/internal/config\"\n\t\"github.com/arbazkhan971/bharatcode/internal/message\"\n)\n\n// ptr returns a pointer to v — a convenience for test literals.\nfunc ptr[T any](v T) *T { return &v }\n\n// TestCompatContextWindowOverride verifies that a model with a\n// Compat.ContextWindow set has its context window resolved to the override\n// value, while a model with no Compat block keeps the heuristic default.\nfunc TestCompatContextWindowOverride(t *testing.T) {\n\t// deepseek-chat: heuristic resolves to 131072 via the \"deepseek\" rule.\n\t// With a Compat override of 65536 it should use 65536.\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{{\n\t\t\tName: \"deepseek\",\n\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tModels: []string{\"deepseek-chat\"},\n\t\t}},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"deepseek-chat\",\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\t// Compat.ContextWindow overrides the heuristic.\n\t\t\t\tCompat: &config.ModelCompat{\n\t\t\t\t\tContextWindow: ptr(65536),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tmodels := reg.ListModels()\n\trequire.Len(t, models, 1)\n\trequire.Equal(t, 65536, models[0].ContextWindow,\n\t\t\"Compat.ContextWindow must override the heuristic\")\n\t// The Compat block must be threaded through to the llm.Model.\n\trequire.NotNil(t, models[0].Compat)\n\trequire.NotNil(t, models[0].Compat.ContextWindow)\n\trequire.Equal(t, 65536, *models[0].Compat.ContextWindow)\n}\n\n// TestCompatNoBlockUsesHeuristic verifies that a model with no Compat block\n// uses the inferContextWindow heuristic unchanged (purely additive check).\nfunc TestCompatNoBlockUsesHeuristic(t *testing.T) {\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{{\n\t\t\tName: \"deepseek\",\n\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\tModels: []string{\"deepseek-chat\"},\n\t\t}},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"deepseek-chat\",\n\t\t\t\tProvider: \"deepseek\",\n\t\t\t\t// No Compat block — heuristic must apply.\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tmodels := reg.ListModels()\n\trequire.Len(t, models, 1)\n\trequire.Equal(t, inferContextWindow(\"deepseek-chat\"), models[0].ContextWindow,\n\t\t\"no Compat block must leave inferContextWindow as the sole source\")\n\trequire.Nil(t, models[0].Compat,\n\t\t\"no Compat block must leave Compat nil on the llm.Model\")\n}\n\n// TestCompatThinkingFormatHonored verifies that resolveThinkingFormat returns\n// the model's explicit Compat.ThinkingFormat when set, overriding any\n// URL-based auto-detection.\nfunc TestCompatThinkingFormatHonored(t *testing.T) {\n\t// openrouter.ai URL would normally auto-detect ThinkingFormatOpenRouter.\n\t// The model's explicit Compat.ThinkingFormat = \"deepseek\" must win.\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"some-model\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tThinkingFormat: config.ThinkingFormatDeepSeek,\n\t\t\t},\n\t\t},\n\t}\n\tgot := resolveThinkingFormat(models, \"some-model\", \"https://openrouter.ai/api/v1\")\n\trequire.Equal(t, config.ThinkingFormatDeepSeek, got,\n\t\t\"explicit Compat.ThinkingFormat must override URL auto-detection\")\n}\n\n// TestCompatThinkingFormatAutoDetect verifies that the URL-based auto-detection\n// fires when the model has no Compat block.\nfunc TestCompatThinkingFormatAutoDetect(t *testing.T) {\n\tcases := []struct {\n\t\tbaseURL string\n\t\twant config.ThinkingFormat\n\t}{\n\t\t{\"https://openrouter.ai/api/v1\", config.ThinkingFormatOpenRouter},\n\t\t{\"https://api.deepseek.com/v1\", config.ThinkingFormatDeepSeek},\n\t\t{\"https://zai.ai/v1\", config.ThinkingFormatDeepSeek},\n\t\t{\"https://dashscope.aliyuncs.com/compatible-mode/v1\", config.ThinkingFormatQwen},\n\t\t{\"https://api.openai.com/v1\", config.ThinkingFormatDefault},\n\t\t{\"https://api.groq.com/openai/v1\", config.ThinkingFormatDefault},\n\t}\n\t// No compat block on the model.\n\tmodels := []Model{{ID: \"some-model\"}}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.baseURL, func(t *testing.T) {\n\t\t\tgot := resolveThinkingFormat(models, \"some-model\", tc.baseURL)\n\t\t\trequire.Equal(t, tc.want, got,\n\t\t\t\t\"URL %q should auto-detect thinking format %q\", tc.baseURL, tc.want)\n\t\t})\n\t}\n}\n\n// TestCompatNoneThinkingFormatSuppresses verifies that ThinkingFormatNone\n// resolves correctly even on a URL that would otherwise auto-detect OpenRouter.\nfunc TestCompatNoneThinkingFormatSuppresses(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"some-model\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tThinkingFormat: config.ThinkingFormatNone,\n\t\t\t},\n\t\t},\n\t}\n\tgot := resolveThinkingFormat(models, \"some-model\", \"https://openrouter.ai/api/v1\")\n\trequire.Equal(t, config.ThinkingFormatNone, got,\n\t\t\"ThinkingFormatNone must suppress even the OpenRouter URL auto-detection\")\n}\n\n// TestCompatReasoningOverrideTrue verifies that Compat.Reasoning=true forces\n// the reasoning-model branch regardless of the id heuristic.\nfunc TestCompatReasoningOverrideTrue(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"my-quirky-reasoner\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tReasoning: ptr(true),\n\t\t\t},\n\t\t},\n\t}\n\t// The id \"my-quirky-reasoner\" would NOT be classified as a reasoning model\n\t// by the heuristic (it has no o-series or gpt-5 prefix).\n\trequire.False(t, isReasoningModel(\"my-quirky-reasoner\"),\n\t\t\"sanity: heuristic must not classify this id as reasoning\")\n\trequire.True(t, isReasoningModelForRequest(models, \"my-quirky-reasoner\"),\n\t\t\"Compat.Reasoning=true must override the heuristic\")\n}\n\n// TestCompatReasoningOverrideFalse verifies that Compat.Reasoning=false forces\n// the chat-model branch even for an id the heuristic would classify as reasoning.\nfunc TestCompatReasoningOverrideFalse(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"o3-mini\",\n\t\t\tCompat: &config.ModelCompat{\n\t\t\t\tReasoning: ptr(false),\n\t\t\t},\n\t\t},\n\t}\n\t// The id \"o3-mini\" IS classified as a reasoning model by the heuristic.\n\trequire.True(t, isReasoningModel(\"o3-mini\"),\n\t\t\"sanity: heuristic must classify o3-mini as reasoning\")\n\trequire.False(t, isReasoningModelForRequest(models, \"o3-mini\"),\n\t\t\"Compat.Reasoning=false must override the heuristic\")\n}\n\n// TestCompatReasoningNilFallsThrough verifies that Compat.Reasoning=nil\n// defers to the id heuristic (no behavior change for existing models).\nfunc TestCompatReasoningNilFallsThrough(t *testing.T) {\n\tmodels := []Model{\n\t\t{\n\t\t\tID: \"o3-mini\",\n\t\t\tCompat: &config.ModelCompat{}, // Compat block present but Reasoning is nil\n\t\t},\n\t}\n\trequire.True(t, isReasoningModelForRequest(models, \"o3-mini\"),\n\t\t\"nil Compat.Reasoning must fall through to the id heuristic\")\n}\n\n// TestCompatSupportsImagesOverride verifies that Compat.SupportsImages\n// overrides the catalog flag.\nfunc TestCompatSupportsImagesOverride(t *testing.T) {\n\t// A model whose catalog entry says SupportsImages: false, but Compat\n\t// overrides to true.\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{{\n\t\t\tName: \"myprovider\",\n\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\tBaseURL: \"https://example.invalid/v1\",\n\t\t\tModels: []string{\"my-vision-model\"},\n\t\t}},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"my-vision-model\",\n\t\t\t\tProvider: \"myprovider\",\n\t\t\t\tSupportsImages: false, // catalog says no images\n\t\t\t\tCompat: &config.ModelCompat{\n\t\t\t\t\tSupportsImages: ptr(true), // compat says yes\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tmodels := reg.ListModels()\n\trequire.Len(t, models, 1)\n\trequire.True(t, models[0].SupportsImages,\n\t\t\"Compat.SupportsImages=true must override the catalog SupportsImages=false\")\n}\n\n// TestCompatStrictToolsRequest verifies that StrictTools: true adds strict:true\n// to each tool definition in the built request body.\nfunc TestCompatStrictToolsRequest(t *testing.T) {\n\tcompat := &config.ModelCompat{StrictTools: true}\n\treq := Request{\n\t\tModel: \"some-model\",\n\t\tTools: []Tool{\n\t\t\t{Name: \"my_tool\", Description: \"does something\"},\n\t\t},\n\t}\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, compat, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, body.Tools, 1)\n\trequire.NotNil(t, body.Tools[0].Function.Strict,\n\t\t\"StrictTools must set Strict on every tool function\")\n\trequire.True(t, *body.Tools[0].Function.Strict,\n\t\t\"strict must be true\")\n}\n\n// TestCompatStrictToolsNotSetByDefault verifies that without StrictTools the\n// Strict field is nil (omitted), preserving baseline OpenAI behavior.\nfunc TestCompatStrictToolsNotSetByDefault(t *testing.T) {\n\treq := Request{\n\t\tModel: \"gpt-4o\",\n\t\tTools: []Tool{\n\t\t\t{Name: \"my_tool\"},\n\t\t},\n\t}\n\t// No compat block.\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, nil, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, body.Tools, 1)\n\trequire.Nil(t, body.Tools[0].Function.Strict,\n\t\t\"without StrictTools the Strict field must be nil (omitted)\")\n}\n\n// TestCompatMaxTokensOverride verifies that Compat.MaxTokens replaces the\n// request's MaxTokens in the built request body.\nfunc TestCompatMaxTokensOverride(t *testing.T) {\n\tcompat := &config.ModelCompat{\n\t\tMaxTokens: ptr(2048),\n\t}\n\treq := Request{\n\t\tModel: \"gpt-4o\",\n\t\tMaxTokens: 8192, // caller-requested value\n\t}\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, compat, nil)\n\trequire.NoError(t, err)\n\t// gpt-4o is not a reasoning model, so MaxTokens lands in body.MaxTokens.\n\trequire.Equal(t, 2048, body.MaxTokens,\n\t\t\"Compat.MaxTokens must override the request MaxTokens\")\n}\n\n// TestCompatNoBlockMaxTokensPreserved verifies that without a Compat block the\n// request MaxTokens passes through unchanged.\nfunc TestCompatNoBlockMaxTokensPreserved(t *testing.T) {\n\treq := Request{\n\t\tModel: \"gpt-4o\",\n\t\tMaxTokens: 8192,\n\t}\n\tbody, err := buildOpenAIRequestCompat(req, imageStyleOpenAI, nil, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 8192, body.MaxTokens,\n\t\t\"without Compat block the original request MaxTokens must pass through\")\n}\n\n// TestCompatToolResultQuirkUserContent verifies that ToolResultQuirkUserContent\n// converts a ToolResultBlock into a user-role message instead of a tool message.\nfunc TestCompatToolResultQuirkUserContent(t *testing.T) {\n\tresult := message.ToolResultBlock{\n\t\tToolUseID: \"call_123\",\n\t\tContent: \"42\",\n\t}\n\tmsgs := toolResultMessages(result, config.ToolResultQuirkUserContent)\n\trequire.Len(t, msgs, 1)\n\trequire.Equal(t, \"user\", msgs[0].Role,\n\t\t\"ToolResultQuirkUserContent must produce a user-role message\")\n\trequire.Contains(t, msgs[0].Content.(string), \"call_123\",\n\t\t\"the tool call id must appear in the user message\")\n\trequire.Contains(t, msgs[0].Content.(string), \"42\",\n\t\t\"the tool result content must appear in the user message\")\n}\n\n// TestCompatToolResultQuirkNonePreservesStandard verifies that the zero-value\n// quirk (ToolResultQuirkNone) emits the standard tool-role message format.\nfunc TestCompatToolResultQuirkNonePreservesStandard(t *testing.T) {\n\tresult := message.ToolResultBlock{\n\t\tToolUseID: \"call_456\",\n\t\tContent: \"hello\",\n\t}\n\tmsgs := toolResultMessages(result, config.ToolResultQuirkNone)\n\trequire.Len(t, msgs, 1)\n\trequire.Equal(t, \"tool\", msgs[0].Role,\n\t\t\"ToolResultQuirkNone must produce a standard tool-role message\")\n\trequire.Equal(t, \"call_456\", msgs[0].ToolCallID,\n\t\t\"tool_call_id must be set on the standard tool message\")\n}\n\n// TestCompatRegistryRoundTrip is the end-to-end test called out in the feature\n// spec: a Model with a Compat block overriding ContextWindow and ThinkingFormat\n// is honored; a Model with no Compat block uses the heuristic defaults unchanged.\nfunc TestCompatRegistryRoundTrip(t *testing.T) {\n\tcustomWindow := 65536\n\tcfg := &config.Config{\n\t\tProviders: []config.Provider{\n\t\t\t{\n\t\t\t\tName: \"quirky\",\n\t\t\t\tType: config.ProviderOpenAICompatible,\n\t\t\t\tBaseURL: \"https://api.deepseek.com/v1\",\n\t\t\t\tModels: []string{\"deepseek-r1\", \"gpt-4o\"},\n\t\t\t},\n\t\t},\n\t\tModels: []config.Model{\n\t\t\t{\n\t\t\t\tID: \"deepseek-r1\",\n\t\t\t\tProvider: \"quirky\",\n\t\t\t\t// Compat overrides context window and pins the thinking format.\n\t\t\t\tCompat: &config.ModelCompat{\n\t\t\t\t\tContextWindow: &customWindow,\n\t\t\t\t\tThinkingFormat: config.ThinkingFormatDeepSeek,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID: \"gpt-4o\",\n\t\t\t\tProvider: \"quirky\",\n\t\t\t\t// No Compat block: must use heuristic defaults unchanged.\n\t\t\t},\n\t\t},\n\t\tLedger: config.LedgerConfig{Currency: \"INR\", UsdInrRate: 83.5},\n\t}\n\n\treg, err := NewRegistry(cfg)\n\trequire.NoError(t, err)\n\n\tbyID := make(map[string]Model)\n\tfor _, m := range reg.ListModels() {\n\t\tbyID[m.ID] = m\n\t}\n\n\t// --- Model WITH Compat block ---\n\tr1 := byID[\"deepseek-r1\"]\n\trequire.Equal(t, customWindow, r1.ContextWindow,\n\t\t\"deepseek-r1: Compat.ContextWindow must override the heuristic\")\n\trequire.NotNil(t, r1.Compat, \"deepseek-r1: Compat must be non-nil on the llm.Model\")\n\trequire.Equal(t, config.ThinkingFormatDeepSeek, r1.Compat.ThinkingFormat,\n\t\t\"deepseek-r1: ThinkingFormat must be threaded through\")\n\n\t// resolveThinkingFormat must honor the explicit compat even though the\n\t// baseURL (deepseek.com) would produce the same result by auto-detection.\n\t// Use a different baseURL to prove the explicit flag wins over auto-detection.\n\tallModels := reg.ListModels()\n\tgotFmt := resolveThinkingFormat(allModels, \"deepseek-r1\", \"https://api.openai.com/v1\")\n\trequire.Equal(t, config.ThinkingFormatDeepSeek, gotFmt,\n\t\t\"resolveThinkingFormat must return the explicit Compat.ThinkingFormat even against an OpenAI base URL\")\n\n\t// --- Model WITHOUT Compat block ---\n\tgpt4o := byID[\"gpt-4o\"]\n\theuristicWindow := inferContextWindow(\"gpt-4o\")\n\trequire.Equal(t, heuristicWindow, gpt4o.ContextWindow,\n\t\t\"gpt-4o: no Compat block must leave the heuristic context window unchanged\")\n\trequire.Nil(t, gpt4o.Compat, \"gpt-4o: Compat must be nil when no compat block is configured\")\n\n\tgotFmtNoCompat := resolveThinkingFormat(allModels, \"gpt-4o\", \"https://api.openai.com/v1\")\n\trequire.Equal(t, config.ThinkingFormatDefault, gotFmtNoCompat,\n\t\t\"gpt-4o on OpenAI base URL must resolve to ThinkingFormatDefault (no change)\")\n}\n"}}}}