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/2] 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/2] 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( + "", + ); });