fix: stop workspace tracker when work directory is deleted (#472)

This commit is contained in:
Carlos Florêncio
2026-03-26 15:08:49 +00:00
committed by GitHub
parent 7d900fea91
commit 4536fe0240
4 changed files with 68 additions and 1 deletions
@@ -39,6 +39,12 @@ func (wt *WorkspaceTracker) pollGitChanges(ctx context.Context) {
case <-wt.stopCh:
return
case <-ticker.C:
// Stop polling if the work directory was deleted (worktree removed)
if !wt.workDirExists() {
wt.logger.Warn("work directory no longer exists, stopping git polling",
zap.String("workDir", wt.workDir))
return
}
wt.checkGitChanges(ctx)
}
}
@@ -49,7 +49,16 @@ func (wt *WorkspaceTracker) monitorLoop(ctx context.Context) {
// Quick state check using mtime + diff-files
currentState, err := wt.getWorkspaceState(ctx)
if err != nil {
// Git command failed - skip this cycle and retry on next tick.
// Check if the work directory has been deleted (e.g., worktree removed
// after task was archived or PR branch was deleted). If so, stop the
// tracker to avoid spamming warnings every poll cycle.
if !wt.workDirExists() {
wt.logger.Warn("work directory no longer exists, stopping workspace tracker",
zap.String("workDir", wt.workDir))
return
}
// Git command failed for another reason - skip this cycle and retry on next tick.
// Don't update lastState so the change will be detected when git recovers.
wt.logger.Warn("failed to get workspace state, will retry next cycle",
zap.Error(err))
@@ -0,0 +1,43 @@
package process
import (
"context"
"os"
"testing"
"time"
)
func TestWorkspaceTracker_StopsWhenWorkDirDeleted(t *testing.T) {
isolateTestGitEnv(t)
repoDir, cleanup := setupTestRepo(t)
defer cleanup()
log := newTestLogger(t)
wt := NewWorkspaceTracker(repoDir, log)
wt.gitPollInterval = 100 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wt.Start(ctx)
// Delete the work directory to simulate worktree removal
if err := os.RemoveAll(repoDir); err != nil {
t.Fatalf("failed to remove workdir: %v", err)
}
// Both monitorLoop and pollGitChanges should exit within a few poll cycles
done := make(chan struct{})
go func() {
wt.wg.Wait()
close(done)
}()
select {
case <-done:
// Both goroutines exited — success
case <-time.After(5 * time.Second):
t.Fatal("workspace tracker goroutines did not stop after workdir was deleted")
}
}
@@ -104,6 +104,15 @@ func resolveGitIndexPath(workDir string) string {
return indexPath
}
// workDirExists checks whether the workspace directory still exists on disk.
// Used to detect deleted worktrees and stop polling loops gracefully.
// The workDir is validated at construction time via resolveExistingWorkDir,
// so this only checks for subsequent deletion (e.g., worktree cleanup).
func (wt *WorkspaceTracker) workDirExists() bool {
_, err := os.Stat(wt.workDir) //nolint:gosec // workDir is validated at construction via resolveExistingWorkDir
return !os.IsNotExist(err)
}
// Start begins monitoring the workspace using polling (no fsnotify).
// File changes are detected via git status polling, which is efficient and
// doesn't consume file descriptors like fsnotify/kqueue does on macOS.