Skip to content

Commit ef0e242

Browse files
authored
Report missing workspaces in doctor (#362)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 340b4dd3-b4b3-4f1a-9163-66b16d96fa81
1 parent 96b54b6 commit ef0e242

2 files changed

Lines changed: 84 additions & 13 deletions

File tree

‎cmd/dispatch/cli.go‎

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ var (
4343
// seams so tests can substitute them without touching the environment.
4444
doctorCopilotVersionFn = defaultCopilotVersion
4545
doctorSessionCountFn = defaultSessionCount
46+
doctorWorkspacesFn = defaultDoctorWorkspaces
4647
)
4748

4849
type versionOutput struct {
@@ -588,15 +589,25 @@ type doctorEntry struct {
588589
// Both the text and JSON renderers consume this struct so their outputs stay
589590
// in sync.
590591
type doctorReport struct {
591-
Version string `json:"version"`
592-
OS string `json:"os"`
593-
OK bool `json:"ok"`
594-
Config doctorEntry `json:"config"`
595-
SessionStore doctorEntry `json:"session_store"`
596-
SessionState doctorEntry `json:"session_state"`
597-
CopilotCLI doctorEntry `json:"copilot_cli"`
598-
CopilotVersion string `json:"copilot_version"`
599-
SessionCount int `json:"session_count"`
592+
Version string `json:"version"`
593+
OS string `json:"os"`
594+
OK bool `json:"ok"`
595+
Config doctorEntry `json:"config"`
596+
SessionStore doctorEntry `json:"session_store"`
597+
SessionState doctorEntry `json:"session_state"`
598+
CopilotCLI doctorEntry `json:"copilot_cli"`
599+
CopilotVersion string `json:"copilot_version"`
600+
SessionCount int `json:"session_count"`
601+
Workspaces workspaceReport `json:"workspaces"`
602+
}
603+
604+
// workspaceReport summarizes whether stored session working directories still
605+
// exist on disk.
606+
type workspaceReport struct {
607+
Total int `json:"total"`
608+
Missing int `json:"missing"`
609+
Samples []string `json:"samples,omitempty"`
610+
Error string `json:"error,omitempty"`
600611
}
601612

602613
// collectDoctorReport gathers the environment diagnostics once so they can be
@@ -633,6 +644,7 @@ func collectDoctorReport() doctorReport {
633644
}
634645

635646
r.SessionCount = doctorSessionCountFn()
647+
r.Workspaces = doctorWorkspacesFn()
636648
r.OK = doctorEntryOK(r.Config) &&
637649
doctorEntryOK(r.SessionStore) &&
638650
doctorEntryOK(r.SessionState) &&
@@ -712,6 +724,35 @@ func defaultSessionCount() int {
712724
return n
713725
}
714726

727+
func defaultDoctorWorkspaces() workspaceReport {
728+
store, err := data.Open()
729+
if err != nil {
730+
return workspaceReport{Error: err.Error()}
731+
}
732+
defer store.Close() //nolint:errcheck // read-only, best-effort close
733+
734+
folders, err := store.ListFolders(context.Background())
735+
if err != nil {
736+
return workspaceReport{Error: err.Error()}
737+
}
738+
739+
r := workspaceReport{Total: len(folders)}
740+
for _, folder := range folders {
741+
if strings.TrimSpace(folder) == "" {
742+
continue
743+
}
744+
if _, err := os.Stat(folder); err != nil {
745+
if errors.Is(err, os.ErrNotExist) {
746+
r.Missing++
747+
if len(r.Samples) < 5 {
748+
r.Samples = append(r.Samples, folder)
749+
}
750+
}
751+
}
752+
}
753+
return r
754+
}
755+
715756
// pathStatus stats a path and reports whether it is found, missing, or the
716757
// wrong type (a file where a directory is expected, or vice versa).
717758
func pathStatus(path string, wantDir bool) string {
@@ -748,6 +789,7 @@ func runDoctor(w io.Writer) doctorReport {
748789
fmt.Fprintf(w, "Copilot CLI version: not detected\n")
749790
}
750791
fmt.Fprintf(w, "Stored sessions: %d\n", r.SessionCount)
792+
writeWorkspaceLine(w, r.Workspaces)
751793
fmt.Fprintf(w, "OK: %t\n", r.OK)
752794
return r
753795
}
@@ -791,6 +833,17 @@ func writeDoctorLine(w io.Writer, label string, e doctorEntry, wantDir bool) {
791833
}
792834
}
793835

836+
func writeWorkspaceLine(w io.Writer, r workspaceReport) {
837+
if r.Error != "" {
838+
fmt.Fprintf(w, "Missing workspaces: unknown (%s)\n", r.Error)
839+
return
840+
}
841+
fmt.Fprintf(w, "Missing workspaces: %d of %d folders\n", r.Missing, r.Total)
842+
for _, sample := range r.Samples {
843+
fmt.Fprintf(w, " - %s\n", sample)
844+
}
845+
}
846+
794847
// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
795848
// redirects stderr to it. When no log file is configured, stderr is sent to
796849
// os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for

‎cmd/dispatch/cli_test.go‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,15 @@ func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
196196
t.Setenv("DISPATCH_DB", db)
197197
t.Setenv("DISPATCH_SESSION_STATE", stateDir)
198198

199-
origCount, origVer := doctorSessionCountFn, doctorCopilotVersionFn
199+
origCount, origVer, origWorkspaces := doctorSessionCountFn, doctorCopilotVersionFn, doctorWorkspacesFn
200200
doctorSessionCountFn = func() int { return 7 }
201201
doctorCopilotVersionFn = func(string) string { return "1.2.3" }
202-
t.Cleanup(func() { doctorSessionCountFn, doctorCopilotVersionFn = origCount, origVer })
202+
doctorWorkspacesFn = func() workspaceReport {
203+
return workspaceReport{Total: 3, Missing: 1, Samples: []string{filepath.Join(stateDir, "missing")}}
204+
}
205+
t.Cleanup(func() {
206+
doctorSessionCountFn, doctorCopilotVersionFn, doctorWorkspacesFn = origCount, origVer, origWorkspaces
207+
})
203208

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

232-
origCount := doctorSessionCountFn
238+
origCount, origWorkspaces := doctorSessionCountFn, doctorWorkspacesFn
233239
doctorSessionCountFn = func() int { return 3 }
234-
t.Cleanup(func() { doctorSessionCountFn = origCount })
240+
doctorWorkspacesFn = func() workspaceReport { return workspaceReport{Total: 2, Missing: 1, Samples: []string{"missing-dir"}} }
241+
t.Cleanup(func() { doctorSessionCountFn, doctorWorkspacesFn = origCount, origWorkspaces })
235242

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

275+
func TestWriteWorkspaceLine_Error(t *testing.T) {
276+
var buf bytes.Buffer
277+
writeWorkspaceLine(&buf, workspaceReport{Error: "store unavailable"})
278+
if !strings.Contains(buf.String(), "Missing workspaces: unknown") {
279+
t.Fatalf("workspace line = %q, want unknown status", buf.String())
280+
}
281+
}
282+
265283
func TestParseDoctorArgs(t *testing.T) {
266284
opts, err := parseDoctorArgs([]string{"doctor", "--json", "--strict"})
267285
if err != nil {

0 commit comments

Comments
 (0)