From a3c03d9c8853bc328c11d154feb004404a2f1651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97SO?= <142557582+Linxiushen@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:45:08 +0800 Subject: [PATCH 1/4] fix: pin adversarial branch review context to commit revisions --- plugins/codex/scripts/lib/git.mjs | 19 +++++---- tests/git.test.mjs | 64 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/plugins/codex/scripts/lib/git.mjs b/plugins/codex/scripts/lib/git.mjs index c401f0ca1..0a85a8ebb 100644 --- a/plugins/codex/scripts/lib/git.mjs +++ b/plugins/codex/scripts/lib/git.mjs @@ -67,11 +67,13 @@ function measureCombinedGitOutputBytes(cwd, argSets, maxBytes) { } function buildBranchComparison(cwd, baseRef) { - const mergeBase = gitChecked(cwd, ["merge-base", "HEAD", baseRef]).stdout.trim(); + const headCommit = gitChecked(cwd, ["rev-parse", "--verify", "HEAD"]).stdout.trim(); + const baseRevision = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", baseRef]).stdout.trim(); + const mergeBase = gitChecked(cwd, ["merge-base", headCommit, baseRevision]).stdout.trim(); return { mergeBase, - commitRange: `${mergeBase}..HEAD`, - reviewRange: `${baseRef}...HEAD` + commitRange: `${mergeBase}..${headCommit}`, + reviewRange: `${baseRevision}...${headCommit}` }; } @@ -262,7 +264,7 @@ function collectWorkingTreeContext(cwd, state, options = {}) { function collectBranchContext(cwd, baseRef, options = {}) { const includeDiff = options.includeDiff !== false; const comparison = options.comparison ?? buildBranchComparison(cwd, baseRef); - const currentBranch = getCurrentBranch(cwd); + const currentBranch = options.currentBranch ?? getCurrentBranch(cwd); const changedFiles = gitChecked(cwd, ["diff", "--name-only", comparison.commitRange]).stdout.trim().split("\n").filter(Boolean); const logOutput = gitChecked(cwd, ["log", "--oneline", "--decorate", comparison.commitRange]).stdout.trim(); const diffStat = gitChecked(cwd, ["diff", "--stat", comparison.commitRange]).stdout.trim(); @@ -294,7 +296,10 @@ function buildAdversarialCollectionGuidance(options = {}) { return "Use the repository context below as primary evidence."; } - return "The repository context below is a lightweight summary. Inspect the target diff yourself with read-only git commands before finalizing findings."; + const pinnedRange = options.comparison + ? ` For this branch review, use the pinned commit range ${options.comparison.commitRange}.` + : ""; + return `The repository context below is a lightweight summary. Inspect the target diff yourself with read-only git commands before finalizing findings.${pinnedRange}`; } export function collectReviewContext(cwd, target, options = {}) { @@ -330,7 +335,7 @@ export function collectReviewContext(cwd, target, options = {}) { maxInlineDiffBytes ); includeDiff = options.includeDiff ?? (fileCount <= maxInlineFiles && diffBytes <= maxInlineDiffBytes); - details = collectBranchContext(repoRoot, target.baseRef, { includeDiff, comparison }); + details = collectBranchContext(repoRoot, target.baseRef, { includeDiff, comparison, currentBranch }); } return { @@ -341,7 +346,7 @@ export function collectReviewContext(cwd, target, options = {}) { fileCount: details.changedFiles.length, diffBytes, inputMode: includeDiff ? "inline-diff" : "self-collect", - collectionGuidance: buildAdversarialCollectionGuidance({ includeDiff }), + collectionGuidance: buildAdversarialCollectionGuidance({ includeDiff, comparison: details.comparison }), ...details }; } diff --git a/tests/git.test.mjs b/tests/git.test.mjs index 5b5c266ee..c944e74ff 100644 --- a/tests/git.test.mjs +++ b/tests/git.test.mjs @@ -2,10 +2,74 @@ import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import assert from "node:assert/strict"; +import childProcess from "node:child_process"; +import { syncBuiltinESMExports } from "node:module"; import { collectReviewContext, resolveReviewTarget } from "../plugins/codex/scripts/lib/git.mjs"; import { initGitRepo, makeTempDir, run } from "./helpers.mjs"; +for (const [mode, options] of [["inline", {}], ["lightweight", { maxInlineFiles: 0 }]]) { + test(`branch review keeps one commit snapshot when HEAD and base change during ${mode} collection`, (t) => { + const cwd = makeTempDir(); + const git = (args) => { + const result = run("git", args, { cwd, shell: false }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + initGitRepo(cwd); + fs.writeFileSync(path.join(cwd, "base.js"), "export const base = true;\n"); + git(["add", "."]); + git(["commit", "-m", "base"]); + git(["checkout", "-b", "feature/reviewed"]); + fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'REVIEWED_CHANGE';\n"); + git(["add", "."]); + git(["commit", "-m", "reviewed change"]); + git(["checkout", "-b", "feature/other"]); + fs.writeFileSync(path.join(cwd, "other.js"), "export const value = 'OTHER_BRANCH_CHANGE';\n"); + git(["add", "."]); + git(["commit", "-m", "other branch change"]); + git(["checkout", "feature/reviewed"]); + + const spawnSync = childProcess.spawnSync; + let switched = false; + t.mock.method(childProcess, "spawnSync", (command, args, commandOptions) => { + const result = spawnSync(command, args, commandOptions); + if (!switched && command === "git" && args[0] === "diff" && args[1] === "--name-only") { + switched = true; + const checkout = spawnSync("git", ["checkout", "feature/other"], { + cwd, encoding: "utf8", shell: false, windowsHide: true + }); + assert.equal(checkout.status, 0, checkout.stderr); + const updateBase = spawnSync("git", ["update-ref", "refs/heads/main", "HEAD"], { + cwd, encoding: "utf8", shell: false, windowsHide: true + }); + assert.equal(updateBase.status, 0, updateBase.stderr); + } + return result; + }); + syncBuiltinESMExports(); + t.after(() => { + t.mock.restoreAll(); + syncBuiltinESMExports(); + }); + + const context = collectReviewContext(cwd, { mode: "branch", baseRef: "main" }, options); + + assert.equal(switched, true); + assert.equal(git(["branch", "--show-current"]), "feature/other"); + assert.deepEqual(context.changedFiles, ["reviewed.js"]); + assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "reviewed.js"); + assert.equal(context.branch, "feature/reviewed"); + assert.match(context.summary, /Reviewing branch feature\/reviewed /); + assert.doesNotMatch(context.content, /other branch change|other\.js|OTHER_BRANCH_CHANGE/); + if (mode === "inline") { + assert.match(context.content, /REVIEWED_CHANGE/); + } else { + assert.ok(context.collectionGuidance.includes(context.comparison.commitRange)); + } + }); +} + test("resolveReviewTarget prefers working tree when repo is dirty", () => { const cwd = makeTempDir(); initGitRepo(cwd); From 4ed3792704c4628a02535a0c4d02e5bdf39873ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97SO?= <142557582+Linxiushen@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:54:33 +0800 Subject: [PATCH 2/4] fix: pin branch review content and peel base tags --- plugins/codex/scripts/lib/git.mjs | 11 +++++---- tests/git.test.mjs | 39 ++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/plugins/codex/scripts/lib/git.mjs b/plugins/codex/scripts/lib/git.mjs index 0a85a8ebb..5ea27aabd 100644 --- a/plugins/codex/scripts/lib/git.mjs +++ b/plugins/codex/scripts/lib/git.mjs @@ -67,13 +67,14 @@ function measureCombinedGitOutputBytes(cwd, argSets, maxBytes) { } function buildBranchComparison(cwd, baseRef) { - const headCommit = gitChecked(cwd, ["rev-parse", "--verify", "HEAD"]).stdout.trim(); - const baseRevision = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", baseRef]).stdout.trim(); - const mergeBase = gitChecked(cwd, ["merge-base", headCommit, baseRevision]).stdout.trim(); + const headCommit = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"]).stdout.trim(); + const baseCommit = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]).stdout.trim(); + const mergeBase = gitChecked(cwd, ["merge-base", headCommit, baseCommit]).stdout.trim(); return { + headCommit, mergeBase, commitRange: `${mergeBase}..${headCommit}`, - reviewRange: `${baseRevision}...${headCommit}` + reviewRange: `${baseCommit}...${headCommit}` }; } @@ -297,7 +298,7 @@ function buildAdversarialCollectionGuidance(options = {}) { } const pinnedRange = options.comparison - ? ` For this branch review, use the pinned commit range ${options.comparison.commitRange}.` + ? ` For this branch review, use the pinned commit range ${options.comparison.commitRange}. Read surrounding file content with \`git show ${options.comparison.headCommit}:\`; the working tree may belong to a different branch.` : ""; return `The repository context below is a lightweight summary. Inspect the target diff yourself with read-only git commands before finalizing findings.${pinnedRange}`; } diff --git a/tests/git.test.mjs b/tests/git.test.mjs index c944e74ff..f65b24f61 100644 --- a/tests/git.test.mjs +++ b/tests/git.test.mjs @@ -24,7 +24,9 @@ for (const [mode, options] of [["inline", {}], ["lightweight", { maxInlineFiles: fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'REVIEWED_CHANGE';\n"); git(["add", "."]); git(["commit", "-m", "reviewed change"]); + const reviewedHead = git(["rev-parse", "HEAD"]); git(["checkout", "-b", "feature/other"]); + fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'OTHER_BRANCH_CONTEXT';\n"); fs.writeFileSync(path.join(cwd, "other.js"), "export const value = 'OTHER_BRANCH_CHANGE';\n"); git(["add", "."]); git(["commit", "-m", "other branch change"]); @@ -61,15 +63,50 @@ for (const [mode, options] of [["inline", {}], ["lightweight", { maxInlineFiles: assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "reviewed.js"); assert.equal(context.branch, "feature/reviewed"); assert.match(context.summary, /Reviewing branch feature\/reviewed /); - assert.doesNotMatch(context.content, /other branch change|other\.js|OTHER_BRANCH_CHANGE/); + assert.doesNotMatch(context.content, /other branch change|other\.js|OTHER_BRANCH_CHANGE|OTHER_BRANCH_CONTEXT/); + assert.match(fs.readFileSync(path.join(cwd, "reviewed.js"), "utf8"), /OTHER_BRANCH_CONTEXT/); if (mode === "inline") { assert.match(context.content, /REVIEWED_CHANGE/); } else { assert.ok(context.collectionGuidance.includes(context.comparison.commitRange)); + assert.ok(context.collectionGuidance.includes(`git show ${reviewedHead}:`)); + assert.equal(context.comparison.headCommit, reviewedHead); + assert.match(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /REVIEWED_CHANGE/); + assert.doesNotMatch(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /OTHER_BRANCH_CONTEXT/); } }); } +test("branch review peels annotated base tags to commit snapshots", () => { + const cwd = makeTempDir(); + const git = (args) => { + const result = run("git", args, { cwd, shell: false }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + initGitRepo(cwd); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log('base');\n"); + git(["add", "."]); + git(["commit", "-m", "base"]); + const baseCommit = git(["rev-parse", "HEAD"]); + git(["tag", "-a", "review-base", "-m", "annotated base"]); + const tagObject = git(["rev-parse", "review-base"]); + assert.notEqual(tagObject, baseCommit); + assert.equal(git(["cat-file", "-t", tagObject]), "tag"); + git(["checkout", "-b", "feature/reviewed"]); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log('reviewed');\n"); + git(["add", "."]); + git(["commit", "-m", "reviewed change"]); + const headCommit = git(["rev-parse", "HEAD"]); + + const context = collectReviewContext(cwd, { mode: "branch", baseRef: "review-base" }, { maxInlineFiles: 0 }); + + assert.equal(context.comparison.reviewRange, `${baseCommit}...${headCommit}`); + assert.equal(context.comparison.commitRange, `${baseCommit}..${headCommit}`); + assert.equal(git(["cat-file", "-t", context.comparison.reviewRange.split("...")[0]]), "commit"); + assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "app.js"); +}); + test("resolveReviewTarget prefers working tree when repo is dirty", () => { const cwd = makeTempDir(); initGitRepo(cwd); From 8151d49d923d3ca4425d420930ec96343dc1e0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97SO?= <142557582+Linxiushen@users.noreply.github.com> Date: Wed, 11 Nov 2026 13:29:59 +0800 Subject: [PATCH 3/4] fix: pin surrounding reads for inline branch reviews --- plugins/codex/scripts/lib/git.mjs | 8 ++++---- tests/git.test.mjs | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/codex/scripts/lib/git.mjs b/plugins/codex/scripts/lib/git.mjs index 5ea27aabd..abb7c56a9 100644 --- a/plugins/codex/scripts/lib/git.mjs +++ b/plugins/codex/scripts/lib/git.mjs @@ -293,13 +293,13 @@ function collectBranchContext(cwd, baseRef, options = {}) { } function buildAdversarialCollectionGuidance(options = {}) { - if (options.includeDiff !== false) { - return "Use the repository context below as primary evidence."; - } - const pinnedRange = options.comparison ? ` For this branch review, use the pinned commit range ${options.comparison.commitRange}. Read surrounding file content with \`git show ${options.comparison.headCommit}:\`; the working tree may belong to a different branch.` : ""; + if (options.includeDiff !== false) { + return `Use the repository context below as primary evidence.${pinnedRange}`; + } + return `The repository context below is a lightweight summary. Inspect the target diff yourself with read-only git commands before finalizing findings.${pinnedRange}`; } diff --git a/tests/git.test.mjs b/tests/git.test.mjs index f65b24f61..46c9010c4 100644 --- a/tests/git.test.mjs +++ b/tests/git.test.mjs @@ -65,14 +65,13 @@ for (const [mode, options] of [["inline", {}], ["lightweight", { maxInlineFiles: assert.match(context.summary, /Reviewing branch feature\/reviewed /); assert.doesNotMatch(context.content, /other branch change|other\.js|OTHER_BRANCH_CHANGE|OTHER_BRANCH_CONTEXT/); assert.match(fs.readFileSync(path.join(cwd, "reviewed.js"), "utf8"), /OTHER_BRANCH_CONTEXT/); + assert.ok(context.collectionGuidance.includes(context.comparison.commitRange)); + assert.ok(context.collectionGuidance.includes(`git show ${reviewedHead}:`)); + assert.equal(context.comparison.headCommit, reviewedHead); + assert.match(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /REVIEWED_CHANGE/); + assert.doesNotMatch(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /OTHER_BRANCH_CONTEXT/); if (mode === "inline") { assert.match(context.content, /REVIEWED_CHANGE/); - } else { - assert.ok(context.collectionGuidance.includes(context.comparison.commitRange)); - assert.ok(context.collectionGuidance.includes(`git show ${reviewedHead}:`)); - assert.equal(context.comparison.headCommit, reviewedHead); - assert.match(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /REVIEWED_CHANGE/); - assert.doesNotMatch(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /OTHER_BRANCH_CONTEXT/); } }); } From 24fcd197018ebd1fe6c744619b87b8952ef391ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97SO?= <142557582+Linxiushen@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:25:24 +0800 Subject: [PATCH 4/4] fix: bind branch comparison to captured branch ref --- plugins/codex/scripts/lib/git.mjs | 9 +- tests/git.test.mjs | 175 ++++++++++++++++-------------- 2 files changed, 99 insertions(+), 85 deletions(-) diff --git a/plugins/codex/scripts/lib/git.mjs b/plugins/codex/scripts/lib/git.mjs index abb7c56a9..cbee92b91 100644 --- a/plugins/codex/scripts/lib/git.mjs +++ b/plugins/codex/scripts/lib/git.mjs @@ -66,8 +66,9 @@ function measureCombinedGitOutputBytes(cwd, argSets, maxBytes) { return totalBytes; } -function buildBranchComparison(cwd, baseRef) { - const headCommit = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"]).stdout.trim(); +function buildBranchComparison(cwd, baseRef, currentBranch = "HEAD") { + const headRef = currentBranch === "HEAD" ? "HEAD" : `refs/heads/${currentBranch}`; + const headCommit = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", `${headRef}^{commit}`]).stdout.trim(); const baseCommit = gitChecked(cwd, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`]).stdout.trim(); const mergeBase = gitChecked(cwd, ["merge-base", headCommit, baseCommit]).stdout.trim(); return { @@ -264,8 +265,8 @@ function collectWorkingTreeContext(cwd, state, options = {}) { function collectBranchContext(cwd, baseRef, options = {}) { const includeDiff = options.includeDiff !== false; - const comparison = options.comparison ?? buildBranchComparison(cwd, baseRef); const currentBranch = options.currentBranch ?? getCurrentBranch(cwd); + const comparison = options.comparison ?? buildBranchComparison(cwd, baseRef, currentBranch); const changedFiles = gitChecked(cwd, ["diff", "--name-only", comparison.commitRange]).stdout.trim().split("\n").filter(Boolean); const logOutput = gitChecked(cwd, ["log", "--oneline", "--decorate", comparison.commitRange]).stdout.trim(); const diffStat = gitChecked(cwd, ["diff", "--stat", comparison.commitRange]).stdout.trim(); @@ -328,7 +329,7 @@ export function collectReviewContext(cwd, target, options = {}) { diffBytes <= maxInlineDiffBytes); details = collectWorkingTreeContext(repoRoot, state, { includeDiff }); } else { - const comparison = buildBranchComparison(repoRoot, target.baseRef); + const comparison = buildBranchComparison(repoRoot, target.baseRef, currentBranch); const fileCount = gitChecked(repoRoot, ["diff", "--name-only", comparison.commitRange]).stdout.trim().split("\n").filter(Boolean).length; diffBytes = measureGitOutputBytes( repoRoot, diff --git a/tests/git.test.mjs b/tests/git.test.mjs index 46c9010c4..f98d69c58 100644 --- a/tests/git.test.mjs +++ b/tests/git.test.mjs @@ -9,7 +9,82 @@ import { collectReviewContext, resolveReviewTarget } from "../plugins/codex/scri import { initGitRepo, makeTempDir, run } from "./helpers.mjs"; for (const [mode, options] of [["inline", {}], ["lightweight", { maxInlineFiles: 0 }]]) { - test(`branch review keeps one commit snapshot when HEAD and base change during ${mode} collection`, (t) => { + for (const switchPoint of ["after comparison", "after branch label"]) { + test(`branch review keeps one commit snapshot during ${mode} collection ${switchPoint}`, (t) => { + const cwd = makeTempDir(); + const git = (args) => { + const result = run("git", args, { cwd, shell: false }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + }; + initGitRepo(cwd); + fs.writeFileSync(path.join(cwd, "base.js"), "export const base = true;\n"); + git(["add", "."]); + git(["commit", "-m", "base"]); + git(["checkout", "-b", "feature/reviewed"]); + fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'REVIEWED_CHANGE';\n"); + git(["add", "."]); + git(["commit", "-m", "reviewed change"]); + const reviewedHead = git(["rev-parse", "HEAD"]); + git(["checkout", "-b", "feature/other"]); + fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'OTHER_BRANCH_CONTEXT';\n"); + fs.writeFileSync(path.join(cwd, "other.js"), "export const value = 'OTHER_BRANCH_CHANGE';\n"); + git(["add", "."]); + git(["commit", "-m", "other branch change"]); + git(["checkout", "feature/reviewed"]); + git(["tag", "feature/reviewed", "feature/other"]); + + const spawnSync = childProcess.spawnSync; + let switched = false; + t.mock.method(childProcess, "spawnSync", (command, args, commandOptions) => { + const result = spawnSync(command, args, commandOptions); + if (!switched && command === "git" && (switchPoint === "after comparison" + ? args[0] === "diff" && args[1] === "--name-only" + : args[0] === "branch" && args[1] === "--show-current")) { + switched = true; + const checkout = spawnSync("git", ["checkout", "feature/other"], { + cwd, encoding: "utf8", shell: false, windowsHide: true + }); + assert.equal(checkout.status, 0, checkout.stderr); + if (switchPoint === "after comparison") { + const updateBase = spawnSync("git", ["update-ref", "refs/heads/main", "HEAD"], { + cwd, encoding: "utf8", shell: false, windowsHide: true + }); + assert.equal(updateBase.status, 0, updateBase.stderr); + } + } + return result; + }); + syncBuiltinESMExports(); + t.after(() => { + t.mock.restoreAll(); + syncBuiltinESMExports(); + }); + + const context = collectReviewContext(cwd, { mode: "branch", baseRef: "main" }, options); + + assert.equal(switched, true); + assert.equal(git(["branch", "--show-current"]), "feature/other"); + assert.deepEqual(context.changedFiles, ["reviewed.js"]); + assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "reviewed.js"); + assert.equal(context.branch, "feature/reviewed"); + assert.match(context.summary, /Reviewing branch feature\/reviewed /); + assert.doesNotMatch(context.content, /other branch change|other\.js|OTHER_BRANCH_CHANGE|OTHER_BRANCH_CONTEXT/); + assert.match(fs.readFileSync(path.join(cwd, "reviewed.js"), "utf8"), /OTHER_BRANCH_CONTEXT/); + assert.ok(context.collectionGuidance.includes(context.comparison.commitRange)); + assert.ok(context.collectionGuidance.includes(`git show ${reviewedHead}:`)); + assert.equal(context.comparison.headCommit, reviewedHead); + assert.match(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /REVIEWED_CHANGE/); + assert.doesNotMatch(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /OTHER_BRANCH_CONTEXT/); + if (mode === "inline") { + assert.match(context.content, /REVIEWED_CHANGE/); + } + }); + } +} + +for (const detached of [false, true]) { + test(`branch review peels annotated base tags to commit snapshots (${detached ? "detached" : "attached"})`, () => { const cwd = makeTempDir(); const git = (args) => { const result = run("git", args, { cwd, shell: false }); @@ -17,94 +92,32 @@ for (const [mode, options] of [["inline", {}], ["lightweight", { maxInlineFiles: return result.stdout.trim(); }; initGitRepo(cwd); - fs.writeFileSync(path.join(cwd, "base.js"), "export const base = true;\n"); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log('base');\n"); git(["add", "."]); git(["commit", "-m", "base"]); + const baseCommit = git(["rev-parse", "HEAD"]); + git(["tag", "-a", "review-base", "-m", "annotated base"]); + const tagObject = git(["rev-parse", "review-base"]); + assert.notEqual(tagObject, baseCommit); + assert.equal(git(["cat-file", "-t", tagObject]), "tag"); git(["checkout", "-b", "feature/reviewed"]); - fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'REVIEWED_CHANGE';\n"); + fs.writeFileSync(path.join(cwd, "app.js"), "console.log('reviewed');\n"); git(["add", "."]); git(["commit", "-m", "reviewed change"]); - const reviewedHead = git(["rev-parse", "HEAD"]); - git(["checkout", "-b", "feature/other"]); - fs.writeFileSync(path.join(cwd, "reviewed.js"), "export const value = 'OTHER_BRANCH_CONTEXT';\n"); - fs.writeFileSync(path.join(cwd, "other.js"), "export const value = 'OTHER_BRANCH_CHANGE';\n"); - git(["add", "."]); - git(["commit", "-m", "other branch change"]); - git(["checkout", "feature/reviewed"]); - - const spawnSync = childProcess.spawnSync; - let switched = false; - t.mock.method(childProcess, "spawnSync", (command, args, commandOptions) => { - const result = spawnSync(command, args, commandOptions); - if (!switched && command === "git" && args[0] === "diff" && args[1] === "--name-only") { - switched = true; - const checkout = spawnSync("git", ["checkout", "feature/other"], { - cwd, encoding: "utf8", shell: false, windowsHide: true - }); - assert.equal(checkout.status, 0, checkout.stderr); - const updateBase = spawnSync("git", ["update-ref", "refs/heads/main", "HEAD"], { - cwd, encoding: "utf8", shell: false, windowsHide: true - }); - assert.equal(updateBase.status, 0, updateBase.stderr); - } - return result; - }); - syncBuiltinESMExports(); - t.after(() => { - t.mock.restoreAll(); - syncBuiltinESMExports(); - }); + const headCommit = git(["rev-parse", "HEAD"]); + if (detached) git(["checkout", "--detach", headCommit]); + + const context = collectReviewContext(cwd, { mode: "branch", baseRef: "review-base" }, { maxInlineFiles: 0 }); - const context = collectReviewContext(cwd, { mode: "branch", baseRef: "main" }, options); - - assert.equal(switched, true); - assert.equal(git(["branch", "--show-current"]), "feature/other"); - assert.deepEqual(context.changedFiles, ["reviewed.js"]); - assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "reviewed.js"); - assert.equal(context.branch, "feature/reviewed"); - assert.match(context.summary, /Reviewing branch feature\/reviewed /); - assert.doesNotMatch(context.content, /other branch change|other\.js|OTHER_BRANCH_CHANGE|OTHER_BRANCH_CONTEXT/); - assert.match(fs.readFileSync(path.join(cwd, "reviewed.js"), "utf8"), /OTHER_BRANCH_CONTEXT/); - assert.ok(context.collectionGuidance.includes(context.comparison.commitRange)); - assert.ok(context.collectionGuidance.includes(`git show ${reviewedHead}:`)); - assert.equal(context.comparison.headCommit, reviewedHead); - assert.match(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /REVIEWED_CHANGE/); - assert.doesNotMatch(git(["show", `${context.comparison.headCommit}:reviewed.js`]), /OTHER_BRANCH_CONTEXT/); - if (mode === "inline") { - assert.match(context.content, /REVIEWED_CHANGE/); - } + assert.equal(context.branch, detached ? "HEAD" : "feature/reviewed"); + assert.equal(context.comparison.headCommit, headCommit); + assert.equal(context.comparison.reviewRange, `${baseCommit}...${headCommit}`); + assert.equal(context.comparison.commitRange, `${baseCommit}..${headCommit}`); + assert.equal(git(["cat-file", "-t", context.comparison.reviewRange.split("...")[0]]), "commit"); + assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "app.js"); }); -} -test("branch review peels annotated base tags to commit snapshots", () => { - const cwd = makeTempDir(); - const git = (args) => { - const result = run("git", args, { cwd, shell: false }); - assert.equal(result.status, 0, result.stderr); - return result.stdout.trim(); - }; - initGitRepo(cwd); - fs.writeFileSync(path.join(cwd, "app.js"), "console.log('base');\n"); - git(["add", "."]); - git(["commit", "-m", "base"]); - const baseCommit = git(["rev-parse", "HEAD"]); - git(["tag", "-a", "review-base", "-m", "annotated base"]); - const tagObject = git(["rev-parse", "review-base"]); - assert.notEqual(tagObject, baseCommit); - assert.equal(git(["cat-file", "-t", tagObject]), "tag"); - git(["checkout", "-b", "feature/reviewed"]); - fs.writeFileSync(path.join(cwd, "app.js"), "console.log('reviewed');\n"); - git(["add", "."]); - git(["commit", "-m", "reviewed change"]); - const headCommit = git(["rev-parse", "HEAD"]); - - const context = collectReviewContext(cwd, { mode: "branch", baseRef: "review-base" }, { maxInlineFiles: 0 }); - - assert.equal(context.comparison.reviewRange, `${baseCommit}...${headCommit}`); - assert.equal(context.comparison.commitRange, `${baseCommit}..${headCommit}`); - assert.equal(git(["cat-file", "-t", context.comparison.reviewRange.split("...")[0]]), "commit"); - assert.equal(git(["diff", "--name-only", context.comparison.reviewRange]), "app.js"); -}); +} test("resolveReviewTarget prefers working tree when repo is dirty", () => { const cwd = makeTempDir();