package cli import ( "bytes" "context" "errors" "os" "path/filepath" "strings" "testing" "gitea.lclr.dev/AI/mcp-framework/config" "gitea.lclr.dev/AI/mcp-framework/manifest" "gitea.lclr.dev/AI/mcp-framework/secretstore" ) type doctorProfile struct { BaseURL string `json:"base_url"` } func TestRunDoctorAggregatesCommonChecksAndExtras(t *testing.T) { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) t.Setenv("HOME", tempHome) store := config.NewStore[doctorProfile]("doctor-test") path, err := store.ConfigPath() if err != nil { t.Fatalf("ConfigPath returned error: %v", err) } cfg := config.FileConfig[doctorProfile]{ Version: config.CurrentVersion, CurrentProfile: "default", Profiles: map[string]doctorProfile{ "default": {BaseURL: "https://api.example.com"}, }, } if err := store.Save(path, cfg); err != nil { t.Fatalf("Save returned error: %v", err) } manifestDir := t.TempDir() if err := os.WriteFile(filepath.Join(manifestDir, "mcp.toml"), []byte("[update]\nlatest_release_url = \"https://example.com/latest\"\n"), 0o600); err != nil { t.Fatalf("WriteFile manifest: %v", err) } secretFactory := func() (secretstore.Store, error) { return secretstore.Open(secretstore.Options{ BackendPolicy: secretstore.BackendEnvOnly, LookupEnv: func(name string) (string, bool) { if name == "api-token" { return "secret", true } return "", false }, }) } report := RunDoctor(context.Background(), DoctorOptions{ ConfigCheck: NewConfigCheck(store), SecretStoreCheck: SecretStoreAvailabilityCheck(secretFactory), RequiredSecrets: []DoctorSecret{ {Name: "api-token", Label: "API token"}, }, SecretStoreFactory: secretFactory, ManifestDir: manifestDir, ConnectivityCheck: func(context.Context) DoctorResult { return DoctorResult{Name: "connectivity", Status: DoctorStatusOK, Summary: "service is reachable"} }, ExtraChecks: []DoctorCheck{ func(context.Context) DoctorResult { return DoctorResult{Name: "custom", Status: DoctorStatusWarn, Summary: "non blocking drift"} }, }, }) summary := report.Summary() if summary.OK != 5 || summary.Warn != 1 || summary.Fail != 0 || summary.Total != 6 { t.Fatalf("summary = %#v", summary) } } func TestNewConfigCheckWarnsWhenConfigIsMissing(t *testing.T) { tempHome := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tempHome) t.Setenv("HOME", tempHome) store := config.NewStore[doctorProfile]("doctor-test") result := NewConfigCheck(store)(context.Background()) if result.Status != DoctorStatusWarn { t.Fatalf("status = %q, want warn", result.Status) } if !strings.Contains(result.Detail, "config.json") { t.Fatalf("detail = %q, want config path", result.Detail) } } func TestRequiredSecretsCheckFailsWhenSecretIsMissing(t *testing.T) { check := RequiredSecretsCheck(func() (secretstore.Store, error) { return secretstore.Open(secretstore.Options{ BackendPolicy: secretstore.BackendEnvOnly, LookupEnv: func(name string) (string, bool) { return "", false }, }) }, []DoctorSecret{{Name: "API_TOKEN", Label: "API token"}}) result := check(context.Background()) if result.Status != DoctorStatusFail { t.Fatalf("status = %q, want fail", result.Status) } if !strings.Contains(result.Detail, "API_TOKEN") { t.Fatalf("detail = %q, want secret name", result.Detail) } } func TestManifestCheckUsesValidator(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "mcp.toml"), []byte("[update]\nlatest_release_url = \"https://example.com/latest\"\n"), 0o600); err != nil { t.Fatalf("WriteFile manifest: %v", err) } result := ManifestCheck(dir, func(_ manifest.File, path string) []string { _ = path return []string{"missing business section"} })(context.Background()) if result.Status != DoctorStatusFail { t.Fatalf("status = %q, want fail", result.Status) } } func TestRenderDoctorReportFormatsStatusesAndSummary(t *testing.T) { var out bytes.Buffer report := DoctorReport{ Results: []DoctorResult{ {Name: "config", Status: DoctorStatusOK, Summary: "config file is readable", Detail: "/tmp/config.json"}, {Name: "custom", Status: DoctorStatusWarn, Summary: "optional check skipped"}, {Name: "manifest", Status: DoctorStatusFail, Summary: "manifest is invalid", Detail: "parse error"}, }, } if err := RenderDoctorReport(&out, report); err != nil { t.Fatalf("RenderDoctorReport returned error: %v", err) } text := out.String() for _, needle := range []string{ "[OK] config: config file is readable", "[WARN] custom: optional check skipped", "[FAIL] manifest: manifest is invalid", "Summary: 1 ok, 1 warning(s), 1 failure(s), 3 total", } { if !strings.Contains(text, needle) { t.Fatalf("output = %q, want substring %q", text, needle) } } } func TestSecretStoreAvailabilityCheckFailsWhenFactoryFails(t *testing.T) { result := SecretStoreAvailabilityCheck(func() (secretstore.Store, error) { return nil, errors.New("backend missing") })(context.Background()) if result.Status != DoctorStatusFail { t.Fatalf("status = %q, want fail", result.Status) } if !strings.Contains(result.Detail, "backend missing") { t.Fatalf("detail = %q", result.Detail) } }