package doctor import ( "context" "errors" "io" "os" "path/filepath" "strings" "testing" "github.com/aritmolab/thothii/tools/tht/internal/compose" "github.com/aritmolab/thothii/tools/tht/internal/config" ) // Catches treating an unavailable Docker executable as a successful diagnosis. func TestRunReportsUnavailableDockerWithoutReturningAnExecutionError(t *testing.T) { installation := doctorInstallation(t, "") runner := &doctorRunner{dockerUnavailable: true} report, err := Run(context.Background(), installation, runner) if err != nil { t.Fatalf("Run() error = %v, want report", err) } if report.OK || checkStatus(report, "docker") != "failed" { t.Fatalf("Run() report = %#v, want failed Docker check", report) } } // Catches Docker availability short-circuiting a host file-permission failure. func TestRunChecksUnsafeFilesEvenWhenDockerIsUnavailable(t *testing.T) { installation := doctorInstallation(t, "") if err := os.Chmod(installation.EnvFile, 0o644); err != nil { t.Fatal(err) } report, err := Run(context.Background(), installation, &doctorRunner{dockerUnavailable: true}) if err != nil { t.Fatal(err) } if checkStatus(report, "files") != StatusFailed { t.Fatalf("Run() files check = %q, want failed; report = %#v", checkStatus(report, "files"), report) } } // Catches attempts to run in-container diagnostics when core is not running. func TestRunSkipsContainerDiagnosticsWhenCoreIsStopped(t *testing.T) { installation := doctorInstallation(t, "") runner := &doctorRunner{services: stoppedServices} report, err := Run(context.Background(), installation, runner) if err != nil { t.Fatal(err) } if report.OK || checkStatus(report, "workflow") != "skipped" || checkStatus(report, "pi") != "skipped" { t.Fatalf("Run() report = %#v, want stopped-core skips", report) } if strings.Contains(strings.Join(runner.calls, "\n"), " exec -T core ") { t.Fatalf("Run() invoked a container diagnostic while core was stopped: %v", runner.calls) } } // Catches host-Python diagnostics or omission of workflow/Pi checks once core is healthy. func TestRunUsesOnlyContainerLocalWorkflowAndPiDiagnosticsWhenCoreRuns(t *testing.T) { installation := doctorInstallation(t, "") runner := &doctorRunner{services: healthyServices} report, err := Run(context.Background(), installation, runner) if err != nil { t.Fatal(err) } if !report.OK || checkStatus(report, "workflow") != "passed" || checkStatus(report, "pi") != "passed" { t.Fatalf("Run() report = %#v, want successful container diagnostics", report) } assertChecklist(t, report, []string{"descriptor", "files", "docker", "compose", "configuration", "services", "core-http", "frontend-http", "workspace-registry", "workflow", "pi"}) calls := strings.Join(runner.calls, "\n") if !strings.Contains(calls, "exec -T core tht doctor --json") { t.Fatalf("Run() calls = %s, want core-local workflow doctor", calls) } if strings.Contains(calls, ".venv") || strings.Contains(calls, "python") { t.Fatalf("Run() calls = %s, must not require host Python", calls) } } // Catches a claimed valid registry based only on the rendered volume declaration. func TestRunFailsAnInvalidContainerLocalRegistryState(t *testing.T) { installation := doctorInstallation(t, "") runner := &doctorRunner{services: healthyServices, registryInvalid: true} report, err := Run(context.Background(), installation, runner) if err != nil { t.Fatal(err) } if report.OK || checkStatus(report, "workspace-registry") != StatusFailed { t.Fatalf("Run() report = %#v, want failed registry diagnostic", report) } if !strings.Contains(strings.Join(runner.calls, "\n"), "workspace-registry/state/active.json") { t.Fatalf("Run() calls = %v, want an actual registry-state read", runner.calls) } } // Catches Compose health being treated as proof that the HTTP listeners answer requests. func TestRunReportsEachHTTPReachabilityProbeFailure(t *testing.T) { installation := doctorInstallation(t, "") for _, endpoint := range []string{"core", "frontend"} { t.Run(endpoint, func(t *testing.T) { probe := &probeStub{failures: map[string]error{endpoint: errors.New(endpoint + " unreachable")}} report, err := RunWithProbe(context.Background(), installation, &doctorRunner{services: healthyServices}, probe) if err != nil { t.Fatal(err) } if report.OK || checkStatus(report, endpoint+"-http") != StatusFailed { t.Fatalf("RunWithProbe() report = %#v, want failed %s HTTP check", report, endpoint) } }) } } // Catches a workflow failure leaking a credential from a declared secret file into a report. func TestRunRedactsWorkflowDiagnosticFailures(t *testing.T) { installation := doctorInstallation(t, "WORKFLOW_TOKEN_FILE=%s\n") secretPath := filepath.Join(filepath.Dir(installation.EnvFile), "workflow-token") if err := os.WriteFile(secretPath, []byte("workflow-secret-value"), 0o600); err != nil { t.Fatal(err) } contents := "WORKFLOW_TOKEN_FILE=" + secretPath + "\n" if err := os.WriteFile(installation.EnvFile, []byte(contents), 0o600); err != nil { t.Fatal(err) } runner := &doctorRunner{services: healthyServices, workflowFailure: "workflow-secret-value"} report, err := Run(context.Background(), installation, runner) if err != nil { t.Fatal(err) } if report.OK || checkStatus(report, "workflow") != "failed" { t.Fatalf("Run() report = %#v, want failed workflow check", report) } if strings.Contains(reportText(report), "workflow-secret-value") { t.Fatalf("Run() report exposed a secret: %#v", report) } } func doctorInstallation(t *testing.T, _ string) config.Installation { t.Helper() base, err := filepath.EvalSymlinks(os.TempDir()) if err != nil { t.Fatal(err) } root, err := os.MkdirTemp(base, "tht-doctor-") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(root) }) project := filepath.Join(root, "project") for _, path := range []string{project, filepath.Join(project, "deploy"), filepath.Join(project, "docker")} { if err := os.MkdirAll(path, 0o755); err != nil { t.Fatal(err) } } for _, path := range []string{filepath.Join(project, "compose.yaml"), filepath.Join(project, "deploy", "compose.local.yaml"), filepath.Join(project, "docker", "core.Dockerfile")} { if err := os.WriteFile(path, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } } envFile := filepath.Join(root, "operator.env") if err := os.WriteFile(envFile, []byte("SAFE_VALUE=1\n"), 0o600); err != nil { t.Fatal(err) } return config.Installation{Path: filepath.Join(root, "thothii-installation.yaml"), Profile: "local", ProjectDirectory: project, EnvFile: envFile} } type doctorRunner struct { calls []string dockerUnavailable bool services string workflowFailure string registryInvalid bool } func (r *doctorRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) { call := strings.Join(args, " ") r.calls = append(r.calls, call) if r.dockerUnavailable { return compose.Result{ExitCode: 127}, errors.New("docker unavailable") } switch { case strings.Contains(call, "version --format {{.Client.Version}}"): return compose.Result{Stdout: "26.0.0\n"}, nil case strings.Contains(call, "compose version --short"): return compose.Result{Stdout: "v2.30.0\n"}, nil case strings.Contains(call, "config --quiet"): return compose.Result{}, nil case strings.Contains(call, "config --format json"): return compose.Result{Stdout: renderedConfig}, nil case strings.Contains(call, "ps --all --format json"): if r.services == "" { return compose.Result{Stdout: healthyServices}, nil } return compose.Result{Stdout: r.services}, nil case strings.Contains(call, "tht doctor --json"): if r.workflowFailure != "" { return compose.Result{Stderr: r.workflowFailure, ExitCode: 23}, errors.New("workflow failed") } return compose.Result{Stdout: `{"ok":true,"checks":[]}`}, nil case strings.Contains(call, "workspace-registry/state/active.json"): if r.registryInvalid { return compose.Result{ExitCode: 23, Stderr: "invalid workspace registry"}, errors.New("registry invalid") } return compose.Result{Stdout: "registry is valid\n"}, nil case strings.Contains(call, "pi --version") || strings.Contains(call, "PI_VERSION") || strings.Contains(call, "io.thothii.pi.version"): return compose.Result{Stdout: "0.80.3\n"}, nil case strings.Contains(call, "test -w /home/thoth/.pi") || strings.Contains(call, "test -r /home/thoth/.pi/agent/auth.json") || strings.Contains(call, "/health"): return compose.Result{Stdout: `{"ready":true}`}, nil case strings.Contains(call, "/pi-management/test"): return compose.Result{Stdout: `{"ready":true}`}, nil case strings.Contains(call, "ps -q core"): return compose.Result{Stdout: "core-id\n"}, nil } return compose.Result{}, nil } type probeStub struct{ failures map[string]error } func (p *probeStub) Probe(_ context.Context, endpoint HTTPProbeTarget) error { return p.failures[endpoint.Name] } func checkStatus(report Report, name string) string { for _, check := range report.Checks { if check.Name == name { return check.Status } } return "" } func reportText(report Report) string { parts := make([]string, 0, len(report.Checks)) for _, check := range report.Checks { parts = append(parts, check.Name+" "+check.Status+" "+check.Detail) } return strings.Join(parts, "\n") } func assertChecklist(t *testing.T, report Report, want []string) { t.Helper() got := make([]string, 0, len(report.Checks)) for _, check := range report.Checks { got = append(got, check.Name) } if strings.Join(got, ",") != strings.Join(want, ",") { t.Fatalf("doctor checklist = %v, want %v", got, want) } } const renderedConfig = `{"volumes":{"settings":{},"pi-state":{},"workspace-registry":{},"workspace-secrets":{},"sessions":{},"qdrant-data":{},"embedding-models":{}},"services":{"core":{"image":"thothii-core:local","environment":{"THT_LLM_URL":"https://llm.example.invalid"}}}}` const healthyServices = `[ {"Service":"core","State":"running","Health":"healthy"}, {"Service":"frontend","State":"running","Health":"healthy"}, {"Service":"qdrant","State":"running","Health":"healthy"}, {"Service":"embedding","State":"running","Health":"healthy"}, {"Service":"embedding-model-init","State":"exited","ExitCode":0} ]` const stoppedServices = `[ {"Service":"core","State":"exited","Health":""}, {"Service":"frontend","State":"running","Health":"healthy"} ]`