Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cmd/dispatch/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var (
// seams so tests can substitute them without touching the environment.
doctorCopilotVersionFn = defaultCopilotVersion
doctorSessionCountFn = defaultSessionCount
doctorWorkspacesFn = defaultDoctorWorkspaces
)

type versionOutput struct {
Expand Down Expand Up @@ -584,6 +585,16 @@ type doctorReport struct {
CopilotCLI doctorEntry `json:"copilot_cli"`
CopilotVersion string `json:"copilot_version"`
SessionCount int `json:"session_count"`
Workspaces workspaceReport `json:"workspaces"`
}

// workspaceReport summarizes whether stored session working directories still
// exist on disk.
type workspaceReport struct {
Total int `json:"total"`
Missing int `json:"missing"`
Samples []string `json:"samples,omitempty"`
Error string `json:"error,omitempty"`
}

// collectDoctorReport gathers the environment diagnostics once so they can be
Expand Down Expand Up @@ -620,6 +631,7 @@ func collectDoctorReport() doctorReport {
}

r.SessionCount = doctorSessionCountFn()
r.Workspaces = doctorWorkspacesFn()

return r
}
Expand Down Expand Up @@ -664,6 +676,35 @@ func defaultSessionCount() int {
return n
}

func defaultDoctorWorkspaces() workspaceReport {
store, err := data.Open()
if err != nil {
return workspaceReport{Error: err.Error()}
}
defer store.Close() //nolint:errcheck // read-only, best-effort close

folders, err := store.ListFolders(context.Background())
if err != nil {
return workspaceReport{Error: err.Error()}
}

r := workspaceReport{Total: len(folders)}
for _, folder := range folders {
if strings.TrimSpace(folder) == "" {
continue
}
if _, err := os.Stat(folder); err != nil {
if errors.Is(err, os.ErrNotExist) {
r.Missing++
if len(r.Samples) < 5 {
r.Samples = append(r.Samples, folder)
}
}
}
}
return r
}

// pathStatus stats a path and reports whether it is found, missing, or the
// wrong type (a file where a directory is expected, or vice versa).
func pathStatus(path string, wantDir bool) string {
Expand Down Expand Up @@ -700,6 +741,7 @@ func runDoctor(w io.Writer) {
fmt.Fprintf(w, "Copilot CLI version: not detected\n")
}
fmt.Fprintf(w, "Stored sessions: %d\n", r.SessionCount)
writeWorkspaceLine(w, r.Workspaces)
}

// runDoctorJSON writes the diagnostics as a single JSON object followed by a
Expand Down Expand Up @@ -740,6 +782,17 @@ func writeDoctorLine(w io.Writer, label string, e doctorEntry, wantDir bool) {
}
}

func writeWorkspaceLine(w io.Writer, r workspaceReport) {
if r.Error != "" {
fmt.Fprintf(w, "Missing workspaces: unknown (%s)\n", r.Error)
return
}
fmt.Fprintf(w, "Missing workspaces: %d of %d folders\n", r.Missing, r.Total)
for _, sample := range r.Samples {
fmt.Fprintf(w, " - %s\n", sample)
}
}

// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
// redirects stderr to it. When no log file is configured, stderr is sent to
// os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for
Expand Down
26 changes: 22 additions & 4 deletions cmd/dispatch/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,15 @@ func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
t.Setenv("DISPATCH_DB", db)
t.Setenv("DISPATCH_SESSION_STATE", stateDir)

origCount, origVer := doctorSessionCountFn, doctorCopilotVersionFn
origCount, origVer, origWorkspaces := doctorSessionCountFn, doctorCopilotVersionFn, doctorWorkspacesFn
doctorSessionCountFn = func() int { return 7 }
doctorCopilotVersionFn = func(string) string { return "1.2.3" }
t.Cleanup(func() { doctorSessionCountFn, doctorCopilotVersionFn = origCount, origVer })
doctorWorkspacesFn = func() workspaceReport {
return workspaceReport{Total: 3, Missing: 1, Samples: []string{filepath.Join(stateDir, "missing")}}
}
t.Cleanup(func() {
doctorSessionCountFn, doctorCopilotVersionFn, doctorWorkspacesFn = origCount, origVer, origWorkspaces
})

var buf bytes.Buffer
runDoctor(&buf)
Expand All @@ -213,6 +218,7 @@ func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
"Session state: found",
"Copilot CLI:",
"Stored sessions: 7",
"Missing workspaces: 1 of 3 folders",
} {
if !strings.Contains(out, want) {
t.Errorf("doctor output missing %q:\n%s", want, out)
Expand All @@ -229,9 +235,10 @@ func TestRunDoctorJSON_Shape(t *testing.T) {
t.Setenv("DISPATCH_DB", db)
t.Setenv("DISPATCH_SESSION_STATE", stateDir)

origCount := doctorSessionCountFn
origCount, origWorkspaces := doctorSessionCountFn, doctorWorkspacesFn
doctorSessionCountFn = func() int { return 3 }
t.Cleanup(func() { doctorSessionCountFn = origCount })
doctorWorkspacesFn = func() workspaceReport { return workspaceReport{Total: 2, Missing: 1, Samples: []string{"missing-dir"}} }
t.Cleanup(func() { doctorSessionCountFn, doctorWorkspacesFn = origCount, origWorkspaces })

var buf bytes.Buffer
if err := runDoctorJSON(&buf); err != nil {
Expand All @@ -257,11 +264,22 @@ func TestRunDoctorJSON_Shape(t *testing.T) {
if r.SessionCount != 3 {
t.Errorf("session_count = %d, want 3", r.SessionCount)
}
if r.Workspaces.Missing != 1 || len(r.Workspaces.Samples) != 1 {
t.Errorf("workspaces = %+v, want one missing sample", r.Workspaces)
}
if !strings.HasSuffix(buf.String(), "}\n") {
t.Errorf("JSON output should end with a single newline, got:\n%q", buf.String())
}
}

func TestWriteWorkspaceLine_Error(t *testing.T) {
var buf bytes.Buffer
writeWorkspaceLine(&buf, workspaceReport{Error: "store unavailable"})
if !strings.Contains(buf.String(), "Missing workspaces: unknown") {
t.Fatalf("workspace line = %q, want unknown status", buf.String())
}
}

func TestDefaultCopilotVersion_EmptyBinary(t *testing.T) {
if got := defaultCopilotVersion(""); got != "" {
t.Errorf("defaultCopilotVersion(\"\") = %q, want empty", got)
Expand Down
Loading