summaryrefslogtreecommitdiff
path: root/internal/executor/executor_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-10 22:13:49 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-10 22:13:49 +0000
commit06c9730441593c191f968f8041a0e76f56368a39 (patch)
treefb228a59295fe22987e3dcfaa51987ce5f3f0f48 /internal/executor/executor_test.go
parent3728fb07ddc9c3a85cf4f2152b35e090618f87db (diff)
fix(executor): periodically sweep stale dispatch workspace directories
ContainerRunner preserves a failed execution's workspace indefinitely for debugging, with no expiry -- this accumulated 161 stale directories (~17.6GB) and took the host to 100% disk full on 2026-07-10. Pool.RunWorkspaceCleanup sweeps claudomator-workspace-* dirs older than 24h every hour, started from serve.go, mirroring StoryOrchestrator.Run's ticker shape. Never removes a directory still referenced as a currently-BLOCKED task's sandbox_dir, regardless of age.
Diffstat (limited to 'internal/executor/executor_test.go')
-rw-r--r--internal/executor/executor_test.go70
1 files changed, 70 insertions, 0 deletions
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index a2228c3..c40f274 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -2170,3 +2170,73 @@ func TestPool_Shutdown_TimesOut(t *testing.T) {
}
close(unblock) // cleanup
}
+
+// makeStaleWorkspaceDir creates a real directory matching the
+// "claudomator-workspace-*" glob cleanupStaleWorkspaces looks for, backdates
+// its mtime by age, and registers it for cleanup at test end regardless of
+// whether the function under test already removed it.
+func makeStaleWorkspaceDir(t *testing.T, age time.Duration) string {
+ t.Helper()
+ dir, err := os.MkdirTemp("", "claudomator-workspace-*")
+ if err != nil {
+ t.Fatalf("MkdirTemp: %v", err)
+ }
+ t.Cleanup(func() { os.RemoveAll(dir) })
+ old := time.Now().Add(-age)
+ if err := os.Chtimes(dir, old, old); err != nil {
+ t.Fatalf("Chtimes: %v", err)
+ }
+ return dir
+}
+
+func TestPool_CleanupStaleWorkspaces_RemovesOldUnreferencedDir(t *testing.T) {
+ store := testStore(t)
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(2, map[string]Runner{"claude": &mockRunner{}}, store, logger)
+
+ dir := makeStaleWorkspaceDir(t, 48*time.Hour)
+
+ pool.cleanupStaleWorkspaces(24 * time.Hour)
+
+ if _, err := os.Stat(dir); !os.IsNotExist(err) {
+ t.Errorf("expected stale unreferenced workspace dir to be removed, stat err = %v", err)
+ }
+}
+
+func TestPool_CleanupStaleWorkspaces_KeepsRecentDir(t *testing.T) {
+ store := testStore(t)
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(2, map[string]Runner{"claude": &mockRunner{}}, store, logger)
+
+ dir := makeStaleWorkspaceDir(t, 1*time.Hour)
+
+ pool.cleanupStaleWorkspaces(24 * time.Hour)
+
+ if _, err := os.Stat(dir); err != nil {
+ t.Errorf("expected recent workspace dir to be kept, got stat err = %v", err)
+ }
+}
+
+func TestPool_CleanupStaleWorkspaces_KeepsBlockedTaskSandboxDirRegardlessOfAge(t *testing.T) {
+ store := testStore(t)
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(2, map[string]Runner{"claude": &mockRunner{}}, store, logger)
+
+ dir := makeStaleWorkspaceDir(t, 30*24*time.Hour) // deliberately far older than maxAge
+
+ tk := makeTask("blocked-with-sandbox")
+ tk.State = task.StateBlocked
+ store.CreateTask(tk)
+ store.CreateExecution(&storage.Execution{
+ ID: "exec-blocked-1", TaskID: tk.ID,
+ StartTime: time.Now().Add(-30 * 24 * time.Hour),
+ Status: "BLOCKED",
+ SandboxDir: dir,
+ })
+
+ pool.cleanupStaleWorkspaces(24 * time.Hour)
+
+ if _, err := os.Stat(dir); err != nil {
+ t.Errorf("expected a BLOCKED task's sandbox_dir to be preserved regardless of age, got stat err = %v", err)
+ }
+}