summaryrefslogtreecommitdiff
path: root/internal/executor/claude_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-14 07:37:20 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-14 07:37:20 +0000
commit4029fdd82bdd657ed862c89f20eb03ff2594cde9 (patch)
tree5725975ffa6825018605ee336ebe8a7e3f02b1d4 /internal/executor/claude_test.go
parent02b35218d9aadcaa6a3b52f218b71577ab72c811 (diff)
fix: surface agent stderr, auto-retry restart-killed tasks, handle stale sandboxes
#1 - Diagnostics: tailFile() reads last 20 lines of subprocess stderr and appends to error message when claude/gemini exits non-zero. Previously all exit-1 failures were opaque; now the error_msg carries the actual subprocess output. #4 - Restart recovery: RecoverStaleRunning() now re-queues tasks after marking them FAILED, so tasks killed by a server restart automatically retry on the next boot rather than staying permanently FAILED. #2 - Stale sandbox: If a resume execution's preserved SandboxDir no longer exists (e.g. /tmp purge after reboot), clone a fresh sandbox instead of failing immediately with "no such file or directory". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor/claude_test.go')
-rw-r--r--internal/executor/claude_test.go86
1 files changed, 86 insertions, 0 deletions
diff --git a/internal/executor/claude_test.go b/internal/executor/claude_test.go
index 7ab0802..9bb873f 100644
--- a/internal/executor/claude_test.go
+++ b/internal/executor/claude_test.go
@@ -3,6 +3,7 @@ package executor
import (
"context"
"errors"
+ "fmt"
"io"
"log/slog"
"os"
@@ -579,6 +580,63 @@ func TestClaudeRunner_Run_ResumeUsesStoredSandboxDir(t *testing.T) {
}
}
+func TestClaudeRunner_Run_StaleSandboxDir_ClonesAfresh(t *testing.T) {
+ logDir := t.TempDir()
+ projectDir := t.TempDir()
+ initGitRepo(t, projectDir)
+
+ cwdFile := filepath.Join(logDir, "cwd.txt")
+ scriptPath := filepath.Join(t.TempDir(), "fake-claude.sh")
+ script := "#!/bin/sh\nprintf '%s' \"$PWD\" > " + cwdFile + "\n"
+ if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
+ t.Fatalf("write script: %v", err)
+ }
+
+ r := &ClaudeRunner{
+ BinaryPath: scriptPath,
+ Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
+ LogDir: logDir,
+ }
+ tk := &task.Task{
+ Agent: task.AgentConfig{
+ Type: "claude",
+ ProjectDir: projectDir,
+ SkipPlanning: true,
+ },
+ }
+ // Point to a sandbox that no longer exists (e.g. /tmp was purged).
+ staleSandbox := filepath.Join(t.TempDir(), "gone")
+ e := &storage.Execution{
+ ID: "resume-exec-2",
+ TaskID: "task-2",
+ ResumeSessionID: "session-abc",
+ ResumeAnswer: "ok",
+ SandboxDir: staleSandbox,
+ }
+
+ if err := r.Run(context.Background(), tk, e); err != nil {
+ t.Fatalf("Run with stale sandbox: %v", err)
+ }
+
+ got, err := os.ReadFile(cwdFile)
+ if err != nil {
+ t.Fatalf("cwd file not written: %v", err)
+ }
+ // Should have run in a fresh sandbox (not the stale path, not the raw projectDir).
+ // The sandbox is removed after teardown, so we only check what it wasn't.
+ cwd := string(got)
+ if cwd == staleSandbox {
+ t.Error("ran in stale sandbox dir that doesn't exist")
+ }
+ if cwd == projectDir {
+ t.Error("ran directly in project_dir; expected a fresh sandbox clone")
+ }
+ // cwd should look like a claudomator sandbox path.
+ if !strings.Contains(cwd, "claudomator-sandbox-") {
+ t.Errorf("expected sandbox path, got %q", cwd)
+ }
+}
+
func TestIsCompletionReport(t *testing.T) {
tests := []struct {
name string
@@ -621,6 +679,34 @@ func TestIsCompletionReport(t *testing.T) {
}
}
+func TestTailFile_ReturnsLastNLines(t *testing.T) {
+ f, err := os.CreateTemp("", "tailfile-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(f.Name())
+ for i := 1; i <= 30; i++ {
+ fmt.Fprintf(f, "line %d\n", i)
+ }
+ f.Close()
+
+ got := tailFile(f.Name(), 5)
+ lines := strings.Split(got, "\n")
+ if len(lines) != 5 {
+ t.Fatalf("want 5 lines, got %d: %q", len(lines), got)
+ }
+ if lines[0] != "line 26" || lines[4] != "line 30" {
+ t.Errorf("want lines 26-30, got: %q", got)
+ }
+}
+
+func TestTailFile_MissingFile_ReturnsEmpty(t *testing.T) {
+ got := tailFile("/nonexistent/path/file.log", 10)
+ if got != "" {
+ t.Errorf("want empty string for missing file, got %q", got)
+ }
+}
+
func TestGitSafe_PrependsSafeDirectory(t *testing.T) {
got := gitSafe("-C", "/some/path", "status")
want := []string{"-c", "safe.directory=*", "-C", "/some/path", "status"}