From 7b60c0bbe6a8388e556dd3de23e97a899f6a504b Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 5 Apr 2026 15:45:20 -0700 Subject: [PATCH] fix: E2E exit reason precedence + worktree prune race condition Two fixes for E2E test reliability: 1. session-runner.ts: error_max_turns was misclassified as error_api because is_error flag was checked before subtype. Now known subtypes like error_max_turns are preserved even when is_error is set. The is_error override only applies when subtype=success (API failure). 2. worktree.ts: pruneStale() now skips worktrees < 1 hour old to avoid deleting worktrees from concurrent test runs still in progress. Previously any second test execution would kill the first's worktrees. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/worktree.ts | 5 +++++ test/helpers/session-runner.ts | 3 ++- test/worktree.test.ts | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/worktree.ts b/lib/worktree.ts index 1e68884b..8854a840 100644 --- a/lib/worktree.ts +++ b/lib/worktree.ts @@ -259,6 +259,11 @@ export class WorktreeManager { const entryPath = path.join(worktreeBase, entry); try { + // Skip recent worktrees (< 1 hour old) to avoid killing + // worktrees from concurrent test runs still in progress + const stat = fs.statSync(entryPath); + const ageMs = Date.now() - stat.mtimeMs; + if (ageMs < 3600_000) continue; fs.rmSync(entryPath, { recursive: true, force: true }); } catch { /* non-fatal */ } } diff --git a/test/helpers/session-runner.ts b/test/helpers/session-runner.ts index 7101e30c..c0f2ac00 100644 --- a/test/helpers/session-runner.ts +++ b/test/helpers/session-runner.ts @@ -303,12 +303,13 @@ export async function runSkillTest(options: { // Use resultLine for structured result data if (resultLine) { - if (resultLine.is_error) { + if (resultLine.subtype === 'success' && resultLine.is_error) { // claude -p can return subtype=success with is_error=true (e.g. API connection failure) exitReason = 'error_api'; } else if (resultLine.subtype === 'success') { exitReason = 'success'; } else if (resultLine.subtype) { + // Preserve known subtypes like error_max_turns even if is_error is set exitReason = resultLine.subtype; } } diff --git a/test/worktree.test.ts b/test/worktree.test.ts index be1533ae..47a58d23 100644 --- a/test/worktree.test.ts +++ b/test/worktree.test.ts @@ -231,6 +231,9 @@ describe('WorktreeManager', () => { spawnSync('git', ['worktree', 'remove', '--force', oldPath], { cwd: repo, stdio: 'pipe' }); // Recreate the directory to simulate orphaned state fs.mkdirSync(oldPath, { recursive: true }); + // Backdate mtime to simulate a stale worktree (> 1 hour old) + const staleTime = new Date(Date.now() - 7200_000); + fs.utimesSync(oldRunDir, staleTime, staleTime); // New manager should prune the old run's directory const newMgr = new WorktreeManager(repo);