fix(cli): strengthen aggregate doctor diagnostics

This commit is contained in:
2026-08-15 23:47:00 +02:00
parent 7ac99b4f7e
commit 0cb6a3f915
4 changed files with 232 additions and 18 deletions
+74
View File
@@ -27,6 +27,22 @@ func TestRunReportsUnavailableDockerWithoutReturningAnExecutionError(t *testing.
}
}
// 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, "")
@@ -56,6 +72,7 @@ func TestRunUsesOnlyContainerLocalWorkflowAndPiDiagnosticsWhenCoreRuns(t *testin
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)
@@ -65,6 +82,40 @@ func TestRunUsesOnlyContainerLocalWorkflowAndPiDiagnosticsWhenCoreRuns(t *testin
}
}
// 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")
@@ -124,6 +175,7 @@ type doctorRunner struct {
dockerUnavailable bool
services string
workflowFailure string
registryInvalid bool
}
func (r *doctorRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) {
@@ -151,6 +203,11 @@ func (r *doctorRunner) Run(_ context.Context, args []string, _ io.Reader) (compo
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"):
@@ -163,6 +220,12 @@ func (r *doctorRunner) Run(_ context.Context, args []string, _ io.Reader) (compo
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 {
@@ -180,6 +243,17 @@ func reportText(report Report) string {
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 = `[