From 594fd1e8da9befb42ab0a1fcf151e2b8d655e7ce Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Mon, 6 Apr 2026 20:45:33 -0700 Subject: [PATCH] Fix working-tree review crash on untracked directories (#166) * fix: skip untracked directories in review context * fix: skip broken untracked symlinks in reviews --- plugins/codex/scripts/lib/git.mjs | 17 ++++++++++++++-- tests/git.test.mjs | 33 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/plugins/codex/scripts/lib/git.mjs b/plugins/codex/scripts/lib/git.mjs index 1c0529a..6fca213 100644 --- a/plugins/codex/scripts/lib/git.mjs +++ b/plugins/codex/scripts/lib/git.mjs @@ -135,12 +135,25 @@ function formatSection(title, body) { function formatUntrackedFile(cwd, relativePath) { const absolutePath = path.join(cwd, relativePath); - const stat = fs.statSync(absolutePath); + let stat; + try { + stat = fs.statSync(absolutePath); + } catch { + return `### ${relativePath}\n(skipped: broken symlink or unreadable file)`; + } + if (stat.isDirectory()) { + return `### ${relativePath}\n(skipped: directory)`; + } if (stat.size > MAX_UNTRACKED_BYTES) { return `### ${relativePath}\n(skipped: ${stat.size} bytes exceeds ${MAX_UNTRACKED_BYTES} byte limit)`; } - const buffer = fs.readFileSync(absolutePath); + let buffer; + try { + buffer = fs.readFileSync(absolutePath); + } catch { + return `### ${relativePath}\n(skipped: broken symlink or unreadable file)`; + } if (!isProbablyText(buffer)) { return `### ${relativePath}\n(skipped: binary file)`; } diff --git a/tests/git.test.mjs b/tests/git.test.mjs index 7ea1a04..dd6a7a4 100644 --- a/tests/git.test.mjs +++ b/tests/git.test.mjs @@ -68,3 +68,36 @@ test("resolveReviewTarget requires an explicit base when no default branch can b /Unable to detect the repository default branch\. Pass --base or use --scope working-tree\./ ); }); + +test("collectReviewContext skips untracked directories in working tree review", () => { + const cwd = makeTempDir(); + initGitRepo(cwd); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n"); + run("git", ["add", "app.js"], { cwd }); + run("git", ["commit", "-m", "init"], { cwd }); + + const nestedRepoDir = path.join(cwd, ".claude", "worktrees", "agent-test"); + fs.mkdirSync(nestedRepoDir, { recursive: true }); + initGitRepo(nestedRepoDir); + + const target = resolveReviewTarget(cwd, { scope: "working-tree" }); + const context = collectReviewContext(cwd, target); + + assert.match(context.content, /### \.claude\/worktrees\/agent-test\/\n\(skipped: directory\)/); +}); + +test("collectReviewContext skips broken untracked symlinks instead of crashing", () => { + const cwd = makeTempDir(); + initGitRepo(cwd); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n"); + run("git", ["add", "app.js"], { cwd }); + run("git", ["commit", "-m", "init"], { cwd }); + fs.symlinkSync("missing-target", path.join(cwd, "broken-link")); + + const target = resolveReviewTarget(cwd, {}); + const context = collectReviewContext(cwd, target); + + assert.equal(target.mode, "working-tree"); + assert.match(context.content, /### broken-link/); + assert.match(context.content, /skipped: broken symlink or unreadable file/i); +});