From 1cf9c4cb5e9c5940178db7dac074df2e32b1951b Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 16:51:21 -0700 Subject: [PATCH 1/3] fix(scan): keep diff previews inside the repository --- .../scripts/generate_rank_input.py | 6 ++ .../tests-ts/diff-rank-input.test.ts | 100 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 sdk/typescript/tests-ts/diff-rank-input.test.ts diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 535fe341..6800dd5b 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -661,6 +661,12 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: if status == "D": preview = "" elif path.is_file(): + try: + if path.is_symlink(): + continue + path.resolve(strict=True).relative_to(repo) + except (OSError, ValueError): + continue preview, is_binary = preview_for(path, args.preview_bytes) if is_binary: continue diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts new file mode 100644 index 00000000..f6a4bf4d --- /dev/null +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -0,0 +1,100 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "bun:test"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const temporaryRoots: string[] = []; + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) { + rmSync(root, { recursive: true, force: true }); + } +}); + +function git(repository: string, ...args: string[]): string { + return execFileSync( + "git", + [ + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.com", + ...args, + ], + { cwd: repository, encoding: "utf8" }, + ).trim(); +} + +test("diff previews stay inside the selected repository", () => { + const root = realpathSync( + mkdtempSync(join(tmpdir(), "codex-security-diff-rank-")), + ); + temporaryRoots.push(root); + const repository = join(root, "repository"); + const nested = join(repository, "src", "nested"); + mkdirSync(nested, { recursive: true }); + git(repository, "init", "-q"); + writeFileSync(join(repository, "src", "handler.py"), "value = 1\n"); + writeFileSync(join(repository, "src", "deleted.py"), "removed = True\n"); + writeFileSync(join(nested, "linked.py"), "value = 1\n"); + git(repository, "add", "."); + git(repository, "commit", "-qm", "base"); + const base = git(repository, "rev-parse", "HEAD"); + + writeFileSync(join(repository, "src", "handler.py"), "value = 2\n"); + writeFileSync(join(nested, "linked.py"), "value = 2\n"); + rmSync(join(repository, "src", "deleted.py")); + git(repository, "add", "."); + git(repository, "commit", "-qm", "selected changes"); + const head = git(repository, "rev-parse", "HEAD"); + + const externalFixture = join(root, "synthetic-fixture"); + mkdirSync(externalFixture); + writeFileSync(join(externalFixture, "linked.py"), "synthetic = True\n"); + rmSync(nested, { recursive: true }); + symlinkSync(externalFixture, nested, "junction"); + + const python = Bun.which("python3") ?? Bun.which("python") ?? Bun.which("py"); + expect(python).not.toBeNull(); + const output = join(root, "rank-input.jsonl"); + const result = spawnSync( + python!, + [ + "-B", + join(PLUGIN_ROOT, "scripts", "generate_rank_input.py"), + "make-diff-rank-input", + "--repo", + repository, + "--base", + base, + "--head", + head, + "--out", + output, + ], + { encoding: "utf8" }, + ); + + expect(result.status, result.stderr).toBe(0); + const rows = readFileSync(output, "utf8") + .trim() + .split("\n") + .map((row) => JSON.parse(row) as { path: string; preview: string }); + expect(rows.map((row) => row.path)).toEqual([ + "src/deleted.py", + "src/handler.py", + ]); + expect(rows.find((row) => row.path === "src/handler.py")?.preview).toBe( + "value = 2", + ); +}); From 53ad0168e4474222cf04a91bfa55c99adf74c903 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Wed, 12 Aug 2026 16:58:06 -0700 Subject: [PATCH 2/3] fix(scan): retain changed paths without unsafe previews --- .../scripts/generate_rank_input.py | 13 ++++++------ .../tests-ts/diff-rank-input.test.ts | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 6800dd5b..d66537b4 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -660,16 +660,17 @@ def make_diff_rank_input(args: argparse.Namespace) -> None: if status == "D": preview = "" + elif path.is_symlink(): + preview = "" elif path.is_file(): try: - if path.is_symlink(): - continue path.resolve(strict=True).relative_to(repo) except (OSError, ValueError): - continue - preview, is_binary = preview_for(path, args.preview_bytes) - if is_binary: - continue + preview = "" + else: + preview, is_binary = preview_for(path, args.preview_bytes) + if is_binary: + continue else: preview = "" rows.append({"path": rel.as_posix(), "area": args.area, "preview": preview}) diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index f6a4bf4d..86b9ebfd 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -46,15 +46,31 @@ test("diff previews stay inside the selected repository", () => { git(repository, "init", "-q"); writeFileSync(join(repository, "src", "handler.py"), "value = 1\n"); writeFileSync(join(repository, "src", "deleted.py"), "removed = True\n"); + writeFileSync(join(repository, "src", "entry.py"), "handler.py"); writeFileSync(join(nested, "linked.py"), "value = 1\n"); git(repository, "add", "."); + const originalLink = git(repository, "hash-object", "src/entry.py"); + git( + repository, + "update-index", + "--cacheinfo", + `120000,${originalLink},src/entry.py`, + ); git(repository, "commit", "-qm", "base"); const base = git(repository, "rev-parse", "HEAD"); writeFileSync(join(repository, "src", "handler.py"), "value = 2\n"); + writeFileSync(join(repository, "src", "entry.py"), "nested/linked.py"); writeFileSync(join(nested, "linked.py"), "value = 2\n"); rmSync(join(repository, "src", "deleted.py")); git(repository, "add", "."); + const updatedLink = git(repository, "hash-object", "src/entry.py"); + git( + repository, + "update-index", + "--cacheinfo", + `120000,${updatedLink},src/entry.py`, + ); git(repository, "commit", "-qm", "selected changes"); const head = git(repository, "rev-parse", "HEAD"); @@ -92,9 +108,14 @@ test("diff previews stay inside the selected repository", () => { .map((row) => JSON.parse(row) as { path: string; preview: string }); expect(rows.map((row) => row.path)).toEqual([ "src/deleted.py", + "src/entry.py", "src/handler.py", + "src/nested/linked.py", ]); expect(rows.find((row) => row.path === "src/handler.py")?.preview).toBe( "value = 2", ); + expect(rows.find((row) => row.path === "src/nested/linked.py")?.preview).toBe( + "", + ); }); From c0797e5a975ce01f4aed1fad0be20ceb729e9a2f Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 14 Aug 2026 06:45:09 -0700 Subject: [PATCH 3/3] fix(scan): align working-tree diff review inputs --- .../scripts/generate_in_scope_files.py | 19 ++------------- .../scripts/generate_rank_input.py | 11 +++++++++ .../tests-ts/compact-diff-scan.test.ts | 23 ++++++++++++++++++- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index 5c9d1188..d3239711 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -114,7 +114,7 @@ def generate_diff_in_scope_files( ) -> int: """Reuse the existing diff selection without generating previews or duplicate worklists.""" sys.path.insert(0, str(Path(__file__).resolve().parent)) - from generate_rank_input import git_changed_paths, path_is_excluded, run_git_changed_paths + from generate_rank_input import git_changed_paths, path_is_excluded from rank_preview import ( DEFAULT_PREVIEW_BYTES, TEXT_CODE_EXTENSIONS, @@ -125,22 +125,7 @@ def generate_diff_in_scope_files( rows: list[bytes] = [] try: - if mode == "local-patch": - changed = run_git_changed_paths(repository, [base]) - untracked = subprocess.run( - ["git", "-C", str(repository), "ls-files", "--others", "--exclude-standard", "-z"], - capture_output=True, - text=True, - check=True, - ) - changed.extend( - (repository / relative, "A") - for relative in untracked.stdout.split("\0") - if relative - ) - else: - changed = git_changed_paths(repository, base, head, mode) - + changed = git_changed_paths(repository, base, head, mode) eligible = [ (path, status) for path, status in changed diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py index 9d50ff27..92018d74 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_rank_input.py @@ -646,8 +646,19 @@ def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple if mode == "local-patch": unstaged = run_git_changed_paths(repo, [base]) staged = run_git_changed_paths(repo, ["--cached", base]) + untracked = subprocess.run( + ["git", "-C", str(repo), "ls-files", "--others", "--exclude-standard", "-z"], + capture_output=True, + text=True, + check=True, + ) combined = dict(staged) combined.update(unstaged) + combined.update( + (repo / relative, "A") + for relative in untracked.stdout.split("\0") + if relative + ) return sorted(combined.items()) raise SystemExit(f"Unknown diff mode: {mode}") diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 83a7736a..afcf871a 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -244,7 +244,7 @@ describe("compact diff scan", () => { ]); }); - test("includes staged, unstaged, and untracked working-tree changes", () => { + test("keeps staged, unstaged, and untracked working-tree inputs aligned", () => { const { root, repository } = createRepository(); writeSource(repository, "src/handler.py", "value = 1\n"); git(repository, "add", "."); @@ -276,6 +276,27 @@ describe("compact diff scan", () => { "src/staged.py", "src/untracked.py", ]); + + const reviewOutput = join(root, "rank-input.jsonl"); + const review = python( + "generate_rank_input.py", + "make-diff-rank-input", + "--repo", + repository, + "--base", + "HEAD", + "--mode", + "local-patch", + "--out", + reviewOutput, + ); + expect(review.status, review.stderr).toBe(0); + expect( + readFileSync(reviewOutput, "utf8") + .trim() + .split("\n") + .map((row) => (JSON.parse(row) as { path: string }).path), + ).toEqual(["src/handler.py", "src/staged.py", "src/untracked.py"]); }); test("keeps deleted inventory paths without accepting unsafe candidates", () => {