package cli
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitcode-mcp/internal/cache"
"gitcode-mcp/internal/config"
"gitcode-mcp/internal/rag"
)
type cliConfigSource struct {
env map[string]string
homeDir string
configDir string
cacheDir string
}
func newCLIConfigSource(t *testing.T) *cliConfigSource {
t.Helper()
root := t.TempDir()
return &cliConfigSource{
env: map[string]string{},
homeDir: filepath.Join(root, "home"),
configDir: filepath.Join(root, "config"),
cacheDir: filepath.Join(root, "cache"),
}
}
func (s *cliConfigSource) Env(key string) string { return s.env[key] }
func (s *cliConfigSource) UserHomeDir() (string, error) { return s.homeDir, nil }
func (s *cliConfigSource) UserConfigDir() (string, error) { return s.configDir, nil }
func (s *cliConfigSource) UserCacheDir() (string, error) { return s.cacheDir, nil }
func (s *cliConfigSource) ReadFile(path string) ([]byte, error) {
return os.ReadFile(path)
}
type statusReporter struct{ status config.CredentialStatus }
func (r statusReporter) Resolve(context.Context, config.EffectiveConfig) (config.SecretString, config.CredentialStatus, error) {
if r.status.Present {
return config.NewSecretString("test-token"), r.status, nil
}
return config.SecretString{}, r.status, nil
}
func (r statusReporter) Status(context.Context, config.EffectiveConfig) config.CredentialStatus {
return r.status
}
type cliRAGRuntime struct {
executablePath string
live bool
models []string
pullCalls int
smokeCalls int
}
func (r *cliRAGRuntime) LookPath(string) (string, error) {
if r.executablePath == "" {
return "", errors.New("not found")
}
return r.executablePath, nil
}
func (r *cliRAGRuntime) IsLive(context.Context, string, time.Duration) (bool, string) {
if r.live {
return true, ""
}
return false, "not live"
}
func (r *cliRAGRuntime) ListModels(context.Context, string, time.Duration) ([]string, error) {
return append([]string(nil), r.models...), nil
}
func (r *cliRAGRuntime) PullModel(_ context.Context, _, model string, _ time.Duration) error {
r.pullCalls++
r.models = append(r.models, model)
return nil
}
func (r *cliRAGRuntime) EmbeddingSmoke(context.Context, string, string, time.Duration) error {
r.smokeCalls++
return nil
}
func (r *cliRAGRuntime) Start(context.Context, config.RAGProviderConfig) (string, error) {
r.live = true
return "started", nil
}
var _ rag.Runtime = (*cliRAGRuntime)(nil)
func TestConfigAuthCommandsRedactedUX(t *testing.T) {
t.Run("SCN-CONFIG-INIT-YAML-ONLY", func(t *testing.T) {
src := newCLIConfigSource(t)
path := filepath.Join(t.TempDir(), "config.yaml")
src.env[config.EnvMCPConfigPath] = path
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"config", "init"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
if !strings.Contains(stdout.String(), "config_format: yaml") {
t.Fatalf("missing yaml output: %q", stdout.String())
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("config not written: %v", err)
}
if _, err := os.Stat(strings.TrimSuffix(path, ".yaml") + ".json"); !os.IsNotExist(err) {
t.Fatalf("json config should not be written")
}
stdout.Reset()
stderr.Reset()
if code := executeWithFactoryAndDeps([]string{"config", "init"}, &stdout, &stderr, nil, localCommandDeps{Source: src}); code == 0 {
t.Fatalf("overwrite without flag succeeded")
}
})
t.Run("SCN-CONFIG-LOCATE-GITCODE-MCP-CONFIG", func(t *testing.T) {
src := newCLIConfigSource(t)
path := filepath.Join(t.TempDir(), "active.yaml")
if err := os.WriteFile(path, []byte("gitcode_base_url: https://example.invalid\n"), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = path
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"config", "locate"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
for _, want := range []string{path, "config_source: explicit-yaml", "config_format: yaml", "config_exists: true"} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("locate missing %q in %q", want, stdout.String())
}
}
})
t.Run("SCN-CONFIG-ENV-OVERRIDES-WIN", func(t *testing.T) {
src := newCLIConfigSource(t)
path := filepath.Join(t.TempDir(), "active.yaml")
secret := "file-contained-secret"
if err := os.WriteFile(path, []byte("cache_path: /tmp/file-cache.db\ngitcode_base_url: "+secret+"\ncredential:\n store: env\n"), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = path
src.env[config.EnvMCPCacheDir] = filepath.Join(t.TempDir(), "cache-dir")
src.env[config.EnvAPIURL] = "https://api.example.invalid"
src.env[config.EnvToken] = "secret-token-value"
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"config", "show", "--redacted"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"config_source: explicit-yaml", "cache_path_source: env:GITCODE_MCP_CACHE_DIR", "gitcode_base_url_source: env:GITCODE_API_URL", "credential_store_mode: env", "token_present: true"} {
if !strings.Contains(out, want) {
t.Fatalf("show missing %q in %q", want, out)
}
}
for _, forbidden := range []string{"secret-token-value", secret} {
if strings.Contains(out, forbidden) {
t.Fatalf("leaked %q in %q", forbidden, out)
}
}
})
t.Run("SCN-AUTH-KEYRING-UNAVAILABLE", func(t *testing.T) {
src := newCLIConfigSource(t)
rawErr := "raw dbus failure details"
reporter := statusReporter{status: config.CredentialStatus{Source: "keyring", Present: false, StoreMode: "auto", ErrorClass: "credential-store-unavailable", Remediation: "Use GITCODE_TOKEN or credential.store env."}}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"auth", "status"}, &stdout, &stderr, nil, localCommandDeps{Source: src, CredentialReporter: reporter})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"credential_source: keyring", "token_present: false", "credential_error_class: credential-store-unavailable", "remediation:"} {
if !strings.Contains(out, want) {
t.Fatalf("auth missing %q in %q", want, out)
}
}
if strings.Contains(out, rawErr) {
t.Fatalf("raw provider error leaked: %q", out)
}
})
t.Run("SCN-AUTH-STATUS-ENV", func(t *testing.T) {
src := newCLIConfigSource(t)
src.env[config.EnvToken] = "secret-token-value"
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"auth", "status"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"credential_source: env:GITCODE_TOKEN", "token_present: true", "available_sources: env:GITCODE_TOKEN"} {
if !strings.Contains(out, want) {
t.Fatalf("auth missing %q in %q", want, out)
}
}
if strings.Contains(out, "secret-token-value") {
t.Fatalf("auth status leaked token: %q", out)
}
})
t.Run("SCN-AUTH-STATUS-REDACTS-DIAGNOSTIC-SURFACES", func(t *testing.T) {
src := newCLIConfigSource(t)
src.env[config.EnvToken] = "secret-token-value"
src.env["GITCODE_E2E_OWNER"] = "private-owner"
src.env["GITCODE_E2E_REPO"] = "private-repo"
reporter := statusReporter{status: config.CredentialStatus{Source: "env:GITCODE_TOKEN", Present: true, StoreMode: "env", Remediation: "Authorization: Bearer secret-token-value for private-owner/private-repo"}}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"auth", "status"}, &stdout, &stderr, nil, localCommandDeps{Source: src, CredentialReporter: reporter})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, forbidden := range []string{"secret-token-value", "private-owner", "private-repo", "Bearer secret-token-value"} {
if strings.Contains(out, forbidden) {
t.Fatalf("auth status leaked %q: %q", forbidden, out)
}
}
if !strings.Contains(out, "[REDACTED]") {
t.Fatalf("auth status missing redaction marker: %q", out)
}
})
t.Run("SCN-AUTH-STATUS-NO-TOKEN", func(t *testing.T) {
src := newCLIConfigSource(t)
reporter := statusReporter{status: config.CredentialStatus{Source: "missing", Present: false, StoreMode: "auto", ErrorClass: "token-missing", AvailableSources: []string{"env:GITCODE_TOKEN", "keyring", "none"}}}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"auth", "status"}, &stdout, &stderr, nil, localCommandDeps{Source: src, CredentialReporter: reporter})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"credential_source: missing", "token_present: false", "available_sources:", "env:GITCODE_TOKEN", "keyring", "credential_error_class: token-missing"} {
if !strings.Contains(out, want) {
t.Fatalf("auth missing %q in %q", want, out)
}
}
})
t.Run("SCN-AUTH-STATUS-JSON", func(t *testing.T) {
src := newCLIConfigSource(t)
src.env[config.EnvToken] = "secret-token-value"
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"auth", "status", "--format", "json"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"\"source\": \"env:GITCODE_TOKEN\"", "\"present\": true", "\"available_sources\""} {
if !strings.Contains(out, want) {
t.Fatalf("auth json missing %q in %q", want, out)
}
}
if strings.Contains(out, "secret-token-value") {
t.Fatalf("auth json leaked token: %q", out)
}
})
}
func TestRAGSetupCommand(t *testing.T) {
t.Run("SCN-RAG-SETUP-DRY-RUN-MISSING-MODEL", func(t *testing.T) {
src := newCLIConfigSource(t)
runtime := &cliRAGRuntime{executablePath: "/usr/local/bin/ollama", live: true}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"rag", "setup", "--dry-run"}, &stdout, &stderr, nil, localCommandDeps{Source: src, RAGRuntime: runtime})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{"status: missing_model", "profile: qwen3-ollama-0_6b-1024", "provider: ollama", "actions: run gitcode-mcp rag setup --yes"} {
if !strings.Contains(out, want) {
t.Fatalf("rag setup output missing %q in %q", want, out)
}
}
if runtime.pullCalls != 0 || runtime.smokeCalls != 0 {
t.Fatalf("dry-run mutated runtime: %#v", runtime)
}
})
t.Run("SCN-RAG-SETUP-YES-PULLS-AND-SMOKES", func(t *testing.T) {
src := newCLIConfigSource(t)
runtime := &cliRAGRuntime{executablePath: "/usr/local/bin/ollama", live: true}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"rag", "setup", "--yes"}, &stdout, &stderr, nil, localCommandDeps{Source: src, RAGRuntime: runtime})
if code != 0 {
t.Fatalf("code=%d stderr=%q stdout=%q", code, stderr.String(), stdout.String())
}
if runtime.pullCalls != 1 || runtime.smokeCalls != 1 || !strings.Contains(stdout.String(), "status: ready") {
t.Fatalf("runtime=%#v stdout=%q", runtime, stdout.String())
}
if !strings.Contains(stderr.String(), "pulling model") {
t.Fatalf("stderr=%q", stderr.String())
}
if !strings.Contains(stdout.String(), "next_actions:") || !strings.Contains(stdout.String(), "--repo OWNER/REPO") {
t.Fatalf("stdout=%q", stdout.String())
}
})
t.Run("SCN-RAG-SETUP-JSON-REMAINS-SINGLE-DOCUMENT", func(t *testing.T) {
src := newCLIConfigSource(t)
runtime := &cliRAGRuntime{executablePath: "/usr/local/bin/ollama", live: true}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"rag", "setup", "--yes", "--format", "json"}, &stdout, &stderr, nil, localCommandDeps{Source: src, RAGRuntime: runtime})
if code != 0 {
t.Fatalf("code=%d stderr=%q stdout=%q", code, stderr.String(), stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("json progress leaked to stderr: %q", stderr.String())
}
var payload map[string]any
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("invalid json: %v output=%q", err, stdout.String())
}
if payload["status"] != "ready" {
t.Fatalf("payload=%#v", payload)
}
})
}
func TestRuntimeAuditDoctorCommand(t *testing.T) {
t.Run("SCN-RUNTIME-AUDIT-CLI-TEXT", func(t *testing.T) {
src := newCLIConfigSource(t)
path := filepath.Join(t.TempDir(), "active.yaml")
if err := os.WriteFile(path, []byte("cache_path: /tmp/runtime-cache.db\ncredential:\n store: env\n"), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = path
src.env[config.EnvToken] = "secret-token-value"
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor", "--runtime-audit", "--repo", "fixture-repo"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"repo_id: fixture-repo", "config:", "version: 0.1.0", "config_source: explicit-yaml", "config_format: yaml", "config_exists: true", "cache_path: /tmp/runtime-cache.db", "credential_source: env:GITCODE_TOKEN", "token_present: true", "handoff_fields:", "cache: not_reported_by_owner", "repo: not_reported_by_owner", "mcp: not_reported_by_owner", "index: not_reported_by_owner"} {
if !strings.Contains(out, want) {
t.Fatalf("doctor output missing %q in %q", want, out)
}
}
if strings.Contains(out, "secret-token-value") || strings.Contains(out, "cache: ok") || strings.Contains(out, "repo: ok") || strings.Contains(out, "mcp: ok") || strings.Contains(out, "index: ok") {
t.Fatalf("doctor output leaked or synthesized success: %q", out)
}
})
t.Run("SCN-RUNTIME-AUDIT-CLI-JSON", func(t *testing.T) {
src := newCLIConfigSource(t)
reporter := statusReporter{status: config.CredentialStatus{Source: "keyring", Present: false, StoreMode: "auto", ErrorClass: "credential-store-unavailable", Remediation: "Use GITCODE_TOKEN or credential.store env."}}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor", "--runtime-audit", "--repo", "fixture-repo", "--format", "json"}, &stdout, &stderr, nil, localCommandDeps{Source: src, CredentialReporter: reporter})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{"\"repo_id\": \"fixture-repo\"", "\"config\"", "\"handoff_fields\"", "\"credential-store-unavailable\"", "\"token_present\": false"} {
if !strings.Contains(out, want) {
t.Fatalf("doctor json missing %q in %q", want, out)
}
}
for _, forbidden := range []string{"\"cache\":", "\"repo\":", "\"mcp\":", "\"index\":", "raw dbus failure details"} {
if strings.Contains(out, forbidden) {
t.Fatalf("doctor json contained forbidden %q in %q", forbidden, out)
}
}
})
}
func TestConfigCommandDoesNotOpenService(t *testing.T) {
src := newCLIConfigSource(t)
called := false
factory := func(context.Context, string) (queryService, func() error, error) {
called = true
return nil, nil, nil
}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"auth", "status"}, &stdout, &stderr, factory, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
stdout.Reset()
stderr.Reset()
code = executeWithFactoryAndDeps([]string{"doctor", "--runtime-audit", "--repo", "fixture-repo"}, &stdout, &stderr, factory, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("doctor code=%d stderr=%q", code, stderr.String())
}
if called {
t.Fatalf("local command opened service")
}
}
func TestFeedbackStatusAndTrustedSetupFlow(t *testing.T) {
src := newCLIConfigSource(t)
configPath := filepath.Join(src.configDir, "gitcode-mcp", "config.yaml")
cachePath := filepath.Join(src.cacheDir, "gitcode-mcp", "cache.db")
if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(configPath, []byte("# retained operator note\ncache_path: "+cachePath+"\nformat: text\n"), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = configPath
store, err := cache.NewSQLiteStore(context.Background(), cachePath)
if err != nil {
t.Fatal(err)
}
if err := store.AddRepository(context.Background(), cache.RepositoryBinding{RepoID: "example/feedback", Owner: "example", Name: "feedback", APIBaseURL: "https://api.gitcode.com/api/v5", Scopes: []cache.RepositoryScope{cache.RepositoryScopeIssues}}); err != nil {
t.Fatal(err)
}
_ = store.Close()
reporter := statusReporter{status: config.CredentialStatus{Source: "keyring", Present: true, StoreMode: "auto"}}
deps := localCommandDeps{Source: src, CredentialReporter: reporter}
var stdout, stderr bytes.Buffer
if code := executeWithFactoryAndDeps([]string{"feedback", "status", "--format", "json"}, &stdout, &stderr, nil, deps); code != 0 || !strings.Contains(stdout.String(), `"state": "disabled"`) || !strings.Contains(stdout.String(), `"prepare_available": true`) {
t.Fatalf("status code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
if code := executeWithFactoryAndDeps([]string{"feedback", "setup", "--repo", "example/feedback", "--format", "json"}, &stdout, &stderr, nil, deps); code != 0 || !strings.Contains(stdout.String(), `"status": "confirmation_required"`) {
t.Fatalf("plan code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
var renderedPlan config.FeedbackSetupPlan
if err := json.Unmarshal(stdout.Bytes(), &renderedPlan); err != nil || renderedPlan.PlanID == "" {
t.Fatalf("plan decode=%#v err=%v", renderedPlan, err)
}
beforeApply, _ := os.ReadFile(configPath)
if strings.Contains(string(beforeApply), "feedback:") {
t.Fatalf("plan mutated config: %s", beforeApply)
}
stdout.Reset()
stderr.Reset()
if code := executeWithFactoryAndDeps([]string{"feedback", "setup", "--repo", "example/feedback", "--yes", "--plan-id", renderedPlan.PlanID, "--idempotency-key", "feedback-setup-example", "--format", "json"}, &stdout, &stderr, nil, deps); code != 0 || !strings.Contains(stdout.String(), `"status": "configured"`) || !strings.Contains(stdout.String(), `"state": "ready"`) {
t.Fatalf("apply code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
after, err := os.ReadFile(configPath)
if err != nil || !strings.Contains(string(after), "# retained operator note") || !strings.Contains(string(after), "repo_id: example/feedback") {
t.Fatalf("config=%s err=%v", after, err)
}
stdout.Reset()
stderr.Reset()
replayPlan, err := config.PlanFeedbackSetup(src, "example/feedback")
if err != nil {
t.Fatal(err)
}
if code := executeWithFactoryAndDeps([]string{"feedback", "setup", "--repo", "example/feedback", "--yes", "--plan-id", replayPlan.PlanID, "--idempotency-key", "feedback-setup-example", "--format", "json"}, &stdout, &stderr, nil, deps); code != 0 || !strings.Contains(stdout.String(), `"status": "configured"`) || !strings.Contains(stdout.String(), `"replayed": true`) {
t.Fatalf("replay code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
prepareArgs := []string{"feedback", "prepare", "--title", "Readiness remains consistent", "--category", "ux_friction", "--surface", "cli", "--reporter-type", "agent", "--observed", "status and prepare disagreed", "--expected", "one readiness state", "--impact", "agent selected the wrong handoff", "--format", "json"}
if code := executeWithFactoryAndDeps(prepareArgs, &stdout, &stderr, nil, deps); code != 0 || !strings.Contains(stdout.String(), `"status": "prepared"`) || !strings.Contains(stdout.String(), `"configured": true`) || !strings.Contains(stdout.String(), `"state": "ready"`) {
t.Fatalf("prepare readiness code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
if code := executeWithFactoryAndDeps([]string{"feedback", "--format", "json", "--offline", "status"}, &stdout, &stderr, nil, deps); code != 0 || !strings.Contains(stdout.String(), `"state": "provider_unavailable"`) {
t.Fatalf("reordered offline status code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
missingCredentialDeps := deps
missingCredentialDeps.CredentialReporter = statusReporter{status: config.CredentialStatus{Source: "none", Present: false}}
if code := executeWithFactoryAndDeps([]string{"feedback", "status", "--format", "json"}, &stdout, &stderr, nil, missingCredentialDeps); code != 0 || !strings.Contains(stdout.String(), `"state": "credential_missing"`) {
t.Fatalf("missing credential status code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String())
}
}
func TestFeedbackSetupRejectsUnboundTargetAndWrongConfirmationWithoutMutation(t *testing.T) {
src := newCLIConfigSource(t)
configPath := filepath.Join(src.configDir, "gitcode-mcp", "config.yaml")
cachePath := filepath.Join(src.cacheDir, "gitcode-mcp", "cache.db")
if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil {
t.Fatal(err)
}
before := "cache_path: " + cachePath + "\nformat: text\n"
if err := os.WriteFile(configPath, []byte(before), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = configPath
store, err := cache.NewSQLiteStore(context.Background(), cachePath)
if err != nil {
t.Fatal(err)
}
if err := store.AddRepository(context.Background(), cache.RepositoryBinding{RepoID: "example/bound", Owner: "example", Name: "bound"}); err != nil {
t.Fatal(err)
}
_ = store.Close()
deps := localCommandDeps{Source: src, CredentialReporter: statusReporter{status: config.CredentialStatus{Present: true}}}
for _, args := range [][]string{
{"feedback", "setup", "--repo", "example/unbound", "--format", "json"},
{"feedback", "setup", "--repo", "example/bound", "--yes", "--plan-id", "wrong-plan", "--idempotency-key", "setup-bound", "--format", "json"},
} {
var stdout, stderr bytes.Buffer
if code := executeWithFactoryAndDeps(args, &stdout, &stderr, nil, deps); code == 0 {
t.Fatalf("args=%v unexpectedly succeeded: %s", args, stdout.String())
}
after, err := os.ReadFile(configPath)
if err != nil || string(after) != before {
t.Fatalf("args=%v mutated config: %q err=%v stderr=%s", args, after, err, stderr.String())
}
}
}
func TestDoctorCommandFull(t *testing.T) {
t.Run("SCN-004-001-json-empty-cache", func(t *testing.T) {
src := newCLIConfigSource(t)
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor", "--format", "json"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{`"version"`, `"config"`, `"cache"`, `"credential"`, `"repo"`, `"sync"`, `"index"`, `"mcp"`, `"live_provider"`, `"auth_probe"`} {
if !strings.Contains(out, want) {
t.Fatalf("doctor json missing %q in %q", want, out)
}
}
})
t.Run("SCN-004-002-no-binding", func(t *testing.T) {
src := newCLIConfigSource(t)
reporter := statusReporter{status: config.CredentialStatus{Source: "missing", Present: false, StoreMode: "auto", ErrorClass: "token-missing", AvailableSources: []string{"env:GITCODE_TOKEN", "keyring", "none"}}}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor"}, &stdout, &stderr, nil, localCommandDeps{Source: src, CredentialReporter: reporter})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
if !strings.Contains(out, "no_repo_bound") {
t.Fatalf("doctor missing no_repo_bound in %q", out)
}
if !strings.Contains(out, "bind_hint") {
t.Fatalf("doctor missing bind_hint in %q", out)
}
})
t.Run("SCN-004-003-no-token", func(t *testing.T) {
src := newCLIConfigSource(t)
reporter := statusReporter{status: config.CredentialStatus{Source: "missing", Present: false, StoreMode: "auto", ErrorClass: "token-missing", AvailableSources: []string{"env:GITCODE_TOKEN", "keyring", "none"}}}
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor"}, &stdout, &stderr, nil, localCommandDeps{Source: src, CredentialReporter: reporter})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
if !strings.Contains(out, "no_token_configured") {
t.Fatalf("doctor missing no_token_configured in %q", out)
}
if !strings.Contains(out, "available_sources") {
t.Fatalf("doctor missing available_sources in %q", out)
}
})
t.Run("SCN-004-004-token-redacted", func(t *testing.T) {
src := newCLIConfigSource(t)
src.env[config.EnvToken] = "secret-token-value"
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor", "--format", "json"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String()
if !strings.Contains(out, `"token_present": true`) {
t.Fatalf("doctor json missing token_present in %q", out)
}
if !strings.Contains(out, `"status": "token_configured"`) {
t.Fatalf("doctor json missing token_configured in %q", out)
}
if strings.Contains(out, "secret-token-value") {
t.Fatalf("doctor leaked token value: %q", out)
}
})
t.Run("SCN-004-005-runtime-audit-compat", func(t *testing.T) {
src := newCLIConfigSource(t)
src.env[config.EnvToken] = "secret-token-value"
path := filepath.Join(t.TempDir(), "active.yaml")
if err := os.WriteFile(path, []byte("cache_path: /tmp/runtime-cache.db\ncredential:\n store: env\n"), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = path
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor", "--runtime-audit", "--repo", "fixture-repo"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{"repo_id: fixture-repo", "version: 0.1.0", "token_present: true", "cache: not_reported_by_owner", "repo: not_reported_by_owner", "mcp: not_reported_by_owner", "index: not_reported_by_owner"} {
if !strings.Contains(out, want) {
t.Fatalf("doctor --runtime-audit missing %q in %q", want, out)
}
}
if strings.Contains(out, "secret-token-value") {
t.Fatalf("doctor --runtime-audit leaked token: %q", out)
}
})
t.Run("SCN-004-006-full-text-output", func(t *testing.T) {
src := newCLIConfigSource(t)
src.env[config.EnvToken] = "secret-token-value"
path := filepath.Join(t.TempDir(), "active.yaml")
if err := os.WriteFile(path, []byte("cache_path: /tmp/full-doctor-cache.db\ncredential:\n store: env\n"), 0o600); err != nil {
t.Fatal(err)
}
src.env[config.EnvMCPConfigPath] = path
var stdout, stderr bytes.Buffer
code := executeWithFactoryAndDeps([]string{"doctor"}, &stdout, &stderr, nil, localCommandDeps{Source: src})
if code != 0 {
t.Fatalf("code=%d stderr=%q", code, stderr.String())
}
out := stdout.String() + stderr.String()
for _, want := range []string{
"version: 0.1.0",
"config:",
"cache:",
"credential:",
"repo:",
"sync:",
"index:",
"mcp:",
"live_provider:",
"auth_probe:",
"status:",
"token_configured",
"transport_stdio: supported",
"transport_http: supported",
"server_version: 0.1.0",
} {
if !strings.Contains(out, want) {
t.Fatalf("doctor text missing %q in %q", want, out)
}
}
if strings.Contains(out, "secret-token-value") {
t.Fatalf("doctor leaked token: %q", out)
}
})
}