| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "io" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "reasonix/internal/capdiag" |
| 13 | ) |
| 14 | |
| 15 | func TestDoctorCapabilitiesStaticJSON(t *testing.T) { |
| 16 | root := t.TempDir() |
| 17 | _ = os.WriteFile(filepath.Join(root, "AGENTS.md"), []byte("# a\n"), 0o644) |
| 18 | |
| 19 | stdout, stderr := captureStd(t, func() int { |
| 20 | return doctorCapabilitiesCommand([]string{"--root", root, "--json"}) |
| 21 | }) |
| 22 | if strings.TrimSpace(stderr) != "" && strings.Contains(stderr, "warning: --live") { |
| 23 | t.Fatalf("static mode should not emit live warning: %s", stderr) |
| 24 | } |
| 25 | var report capdiag.Report |
| 26 | if err := json.Unmarshal([]byte(stdout), &report); err != nil { |
| 27 | t.Fatalf("stdout not JSON: %v\n%s", err, stdout) |
| 28 | } |
| 29 | if report.SchemaVersion != 1 { |
| 30 | t.Fatalf("schema = %d", report.SchemaVersion) |
| 31 | } |
| 32 | if report.Live { |
| 33 | t.Fatal("live should be false") |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | func TestDoctorCapabilitiesTimeoutRequiresLive(t *testing.T) { |
| 38 | rc := doctorCapabilitiesCommand([]string{"--timeout", "3s"}) |
| 39 | if rc != 2 { |
| 40 | t.Fatalf("rc = %d, want 2", rc) |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | func TestDoctorCapabilitiesTimeoutBounds(t *testing.T) { |
| 45 | if rc := doctorCapabilitiesCommand([]string{"--live", "--timeout", "0.5s"}); rc != 2 { |
| 46 | t.Fatalf("below min rc = %d", rc) |
| 47 | } |
| 48 | if rc := doctorCapabilitiesCommand([]string{"--live", "--timeout", "120s"}); rc != 2 { |
| 49 | t.Fatalf("above max rc = %d", rc) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestDoctorCapabilitiesUnknownArg(t *testing.T) { |
| 54 | if rc := doctorCapabilitiesCommand([]string{"extra"}); rc != 2 { |
| 55 | t.Fatalf("rc = %d, want 2", rc) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestDoctorCapabilitiesTextOutput(t *testing.T) { |
| 60 | root := t.TempDir() |
| 61 | stdout, _ := captureStd(t, func() int { |
| 62 | return doctorCapabilitiesCommand([]string{"--root", root}) |
| 63 | }) |
| 64 | if !strings.Contains(stdout, "Summary") || !strings.Contains(stdout, "Issues") { |
| 65 | t.Fatalf("text output missing sections:\n%s", stdout) |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | func captureStd(t *testing.T, fn func() int) (stdout, stderr string) { |
| 70 | t.Helper() |
| 71 | oldOut, oldErr := os.Stdout, os.Stderr |
| 72 | rOut, wOut, _ := os.Pipe() |
| 73 | rErr, wErr, _ := os.Pipe() |
| 74 | os.Stdout, os.Stderr = wOut, wErr |
| 75 | // Drain while fn runs so a large JSON encode cannot fill the OS pipe |
| 76 | // buffer and deadlock (Windows CI timeout). |
| 77 | var bOut, bErr bytes.Buffer |
| 78 | doneOut := make(chan struct{}) |
| 79 | doneErr := make(chan struct{}) |
| 80 | go func() { |
| 81 | _, _ = io.Copy(&bOut, rOut) |
| 82 | close(doneOut) |
| 83 | }() |
| 84 | go func() { |
| 85 | _, _ = io.Copy(&bErr, rErr) |
| 86 | close(doneErr) |
| 87 | }() |
| 88 | _ = fn() |
| 89 | _ = wOut.Close() |
| 90 | _ = wErr.Close() |
| 91 | os.Stdout, os.Stderr = oldOut, oldErr |
| 92 | <-doneOut |
| 93 | <-doneErr |
| 94 | return bOut.String(), bErr.String() |
| 95 | } |
| 96 |