From 147daa62bc5482a757909ee1202a3584e9c741ee Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 20 Oct 2026 17:43:17 +0000 Subject: [PATCH 1/2] test: keep line-ending defaults local to Git fixtures --- sdk/typescript/tests-ts/api-policy.test.ts | 2 ++ sdk/typescript/tests-ts/api.test.ts | 19 +++++++++++++++++++ .../tests-ts/check-markdown.test.ts | 1 + .../tests-ts/cli-project-config.test.ts | 1 + .../tests-ts/compact-diff-scan.test.ts | 1 + .../tests-ts/component-scan.test.ts | 8 ++++++++ .../tests-ts/custom-validation.test.ts | 1 + .../tests-ts/diff-rank-input.test.ts | 3 +++ .../tests-ts/hosted-remediation.test.ts | 1 + sdk/typescript/tests-ts/hosted-scan.test.ts | 1 + .../tests-ts/saved-scan-bootstrap.test.ts | 8 ++++++++ sdk/typescript/tests-ts/scan-resume.test.ts | 1 + .../tests-ts/security-policy.test.ts | 1 + sdk/typescript/tests-ts/targets.test.ts | 1 + 14 files changed, 49 insertions(+) diff --git a/sdk/typescript/tests-ts/api-policy.test.ts b/sdk/typescript/tests-ts/api-policy.test.ts index 5b6f30489b..d3dd9b8132 100644 --- a/sdk/typescript/tests-ts/api-policy.test.ts +++ b/sdk/typescript/tests-ts/api-policy.test.ts @@ -600,6 +600,7 @@ describe("CodexSecurity policy API", () => { ? ["--separate-git-dir", join(f.root, "other-git-data")] : []), ); + policyGit(owner, "config", "--local", "core.autocrlf", "false"); policyGit(owner, "commit", "--allow-empty", "--quiet", "-m", "initial"); let checkout = owner; if (kind === "linked") { @@ -692,6 +693,7 @@ describe("CodexSecurity policy API", () => { onPrepare, }); policyGit(f.repository, "init", "--quiet"); + policyGit(f.repository, "config", "--local", "core.autocrlf", "false"); const nested = await addPolicySubmodule( f.repository, join(f.root, "submodule-source"), diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 2e475f1772..fb9c457e1a 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2975,6 +2975,14 @@ describe("CodexSecurity orchestration", () => { const linked = join(root, "linked"); await mkdir(normal, { mode: 0o700 }); execFileSync("git", ["init", "-q", normal]); + execFileSync("git", [ + "-C", + normal, + "config", + "--local", + "core.autocrlf", + "false", + ]); await writeFile(join(normal, "tracked.txt"), "tracked\n"); execFileSync("git", ["-C", normal, "add", "."]); execFileSync("git", [ @@ -3072,6 +3080,14 @@ describe("CodexSecurity orchestration", () => { test("scrubs Git overrides from direct target normalization", async () => { const { root, repository } = await repositoryDirectories(); execFileSync("git", ["init", "-q", repository]); + execFileSync("git", [ + "-C", + repository, + "config", + "--local", + "core.autocrlf", + "false", + ]); await writeFile(join(repository, "tracked.txt"), "tracked\n"); execFileSync("git", ["-C", repository, "add", "."]); execFileSync("git", [ @@ -7519,6 +7535,9 @@ describe("CodexSecurity orchestration", () => { await mkdir(ignored); await mkdir(vendored); execFileSync("git", ["init", "-q"], { cwd: repository }); + execFileSync("git", ["config", "--local", "core.autocrlf", "false"], { + cwd: repository, + }); await Promise.all([ writeFile( join(repository, ".gitignore"), diff --git a/sdk/typescript/tests-ts/check-markdown.test.ts b/sdk/typescript/tests-ts/check-markdown.test.ts index 16311efa50..611e0bf7c5 100644 --- a/sdk/typescript/tests-ts/check-markdown.test.ts +++ b/sdk/typescript/tests-ts/check-markdown.test.ts @@ -57,6 +57,7 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un if (process.platform !== "win32") await symlink("untracked.md", join(sdk, "linked.md")); gitText(["init", "--quiet"], { cwd: root }); + gitText(["config", "--local", "core.autocrlf", "false"], { cwd: root }); gitText(["add", "--", "."], { cwd: root }); await rm(join(sdk, "deleted.md")); await writeFile(join(sdk, "untracked.md"), invalid); diff --git a/sdk/typescript/tests-ts/cli-project-config.test.ts b/sdk/typescript/tests-ts/cli-project-config.test.ts index 62414a57bb..554499e54b 100644 --- a/sdk/typescript/tests-ts/cli-project-config.test.ts +++ b/sdk/typescript/tests-ts/cli-project-config.test.ts @@ -485,6 +485,7 @@ test("bulk scans apply config and linked operator prompts, preserve CSV scope ov ); for (const args of [ ["init", "-q", input.repository], + ["-C", input.repository, "config", "--local", "core.autocrlf", "false"], ["-C", input.repository, "add", "."], [ "-C", diff --git a/sdk/typescript/tests-ts/compact-diff-scan.test.ts b/sdk/typescript/tests-ts/compact-diff-scan.test.ts index 99d87a6077..ecd2901319 100644 --- a/sdk/typescript/tests-ts/compact-diff-scan.test.ts +++ b/sdk/typescript/tests-ts/compact-diff-scan.test.ts @@ -21,6 +21,7 @@ function createRepository(): { root: string; repository: string } { const repository = join(root, "repository"); mkdirSync(repository); git(repository, "init", "-q"); + git(repository, "config", "--local", "core.autocrlf", "false"); return { root, repository }; } diff --git a/sdk/typescript/tests-ts/component-scan.test.ts b/sdk/typescript/tests-ts/component-scan.test.ts index d854d4f4a7..cec5e410b7 100644 --- a/sdk/typescript/tests-ts/component-scan.test.ts +++ b/sdk/typescript/tests-ts/component-scan.test.ts @@ -1559,6 +1559,14 @@ test("retains tracked Unicode aliases when scoped Git matching is incomplete", a await mkdir(join(paths.repository, nonCased)); await writeFile(join(paths.repository, nonCased, "app.ts"), "export {};\n"); execFileSync("git", ["-C", paths.repository, "init", "-q"]); + execFileSync("git", [ + "-C", + paths.repository, + "config", + "--local", + "core.autocrlf", + "false", + ]); execFileSync("git", ["-C", paths.repository, "add", "--force", "."]); expect(await scopedInventory(paths, nonCased)).toEqual({ paths: [nonCased + "/app.ts"], diff --git a/sdk/typescript/tests-ts/custom-validation.test.ts b/sdk/typescript/tests-ts/custom-validation.test.ts index be243c3553..2ca4edfa37 100644 --- a/sdk/typescript/tests-ts/custom-validation.test.ts +++ b/sdk/typescript/tests-ts/custom-validation.test.ts @@ -585,6 +585,7 @@ describe("custom validation", () => { ...args, ]); git("init", "-q"); + git("config", "--local", "core.autocrlf", "false"); git("add", "."); git("commit", "-qm", "base"); await writeFile( diff --git a/sdk/typescript/tests-ts/diff-rank-input.test.ts b/sdk/typescript/tests-ts/diff-rank-input.test.ts index 992e0c05a2..5ea3f07ec5 100644 --- a/sdk/typescript/tests-ts/diff-rank-input.test.ts +++ b/sdk/typescript/tests-ts/diff-rank-input.test.ts @@ -28,6 +28,7 @@ test("diff previews stay inside the selected repository", () => { const nested = join(repository, "src", "nested"); mkdirSync(nested, { recursive: true }); git(repository, "init", "-q"); + git(repository, "config", "--local", "core.autocrlf", "false"); 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"); @@ -112,6 +113,7 @@ test("preserves Unicode Git paths and legacy-encoded commit metadata", () => { const source = join(repository, "src", "変更.py"); mkdirSync(join(repository, "src"), { recursive: true }); git(repository, "init", "-q"); + git(repository, "config", "--local", "core.autocrlf", "false"); writeFileSync(source, "value = 1\n"); git(repository, "add", "."); git(repository, "commit", "-qm", "base"); @@ -204,6 +206,7 @@ testPosix( mkdirSync(shimDirectory, { recursive: true }); mkdirSync(externalBin); git(repository, "init", "-q"); + git(repository, "config", "--local", "core.autocrlf", "false"); writeFileSync(join(repository, "source.py"), "value = 1\n"); git(repository, "add", "."); git(repository, "commit", "-qm", "base"); diff --git a/sdk/typescript/tests-ts/hosted-remediation.test.ts b/sdk/typescript/tests-ts/hosted-remediation.test.ts index 296c504282..94f584a445 100644 --- a/sdk/typescript/tests-ts/hosted-remediation.test.ts +++ b/sdk/typescript/tests-ts/hosted-remediation.test.ts @@ -38,6 +38,7 @@ async function fixture( const repository = join(root, "repository"); writeSource(repository, "guard.ts", "export const allowed = false;\n"); git(repository, "init", "-q"); + git(repository, "config", "--local", "core.autocrlf", "false"); git(repository, "add", "."); git(repository, "commit", "-qm", "fixture"); return remediationInput( diff --git a/sdk/typescript/tests-ts/hosted-scan.test.ts b/sdk/typescript/tests-ts/hosted-scan.test.ts index 4310b50fe1..0c9df3604a 100644 --- a/sdk/typescript/tests-ts/hosted-scan.test.ts +++ b/sdk/typescript/tests-ts/hosted-scan.test.ts @@ -56,6 +56,7 @@ async function fixture( ]) writeSource(repository, name, "export const value = 1;\n"); git(repository, "init", "-q"); + git(repository, "config", "--local", "core.autocrlf", "false"); git(repository, "add", "."); git(repository, "commit", "-qm", "fixture"); return { diff --git a/sdk/typescript/tests-ts/saved-scan-bootstrap.test.ts b/sdk/typescript/tests-ts/saved-scan-bootstrap.test.ts index 09d65cf760..a3f586fbe8 100644 --- a/sdk/typescript/tests-ts/saved-scan-bootstrap.test.ts +++ b/sdk/typescript/tests-ts/saved-scan-bootstrap.test.ts @@ -41,6 +41,14 @@ async function fixture(git = false) { ); if (git) { execFileSync("git", ["init", "-q", repository]); + execFileSync("git", [ + "-C", + repository, + "config", + "--local", + "core.autocrlf", + "false", + ]); execFileSync("git", ["-C", repository, "add", "."]); execFileSync("git", [ "-C", diff --git a/sdk/typescript/tests-ts/scan-resume.test.ts b/sdk/typescript/tests-ts/scan-resume.test.ts index 868b4005e6..4cccbbd92a 100644 --- a/sdk/typescript/tests-ts/scan-resume.test.ts +++ b/sdk/typescript/tests-ts/scan-resume.test.ts @@ -67,6 +67,7 @@ async function interruptedScan( const git = (...args: string[]) => gitText(["-C", repository, ...args]).trim(); git("init", "-q"); + git("config", "--local", "core.autocrlf", "false"); git("add", "."); git( "-c", diff --git a/sdk/typescript/tests-ts/security-policy.test.ts b/sdk/typescript/tests-ts/security-policy.test.ts index 5168e3d051..33a916bb70 100644 --- a/sdk/typescript/tests-ts/security-policy.test.ts +++ b/sdk/typescript/tests-ts/security-policy.test.ts @@ -42,6 +42,7 @@ afterEach(cleanup); function createPolicyWorktree(repository: string, root: string, name: string) { policyGit(repository, "init", "--quiet"); + policyGit(repository, "config", "--local", "core.autocrlf", "false"); policyGit(repository, "commit", "--allow-empty", "--quiet", "-m", "initial"); const linked = join(root, name); policyGit( diff --git a/sdk/typescript/tests-ts/targets.test.ts b/sdk/typescript/tests-ts/targets.test.ts index 6a506394a2..322a4544c3 100644 --- a/sdk/typescript/tests-ts/targets.test.ts +++ b/sdk/typescript/tests-ts/targets.test.ts @@ -50,6 +50,7 @@ async function repository(name = "repo"): Promise { await mkdir(join(repo, "src"), { recursive: true }); await writeFile(join(repo, "src", "app.ts"), "export const ok = true;\n"); git(repo, "init", "-b", "main"); + git(repo, "config", "--local", "core.autocrlf", "false"); git(repo, "config", "user.email", "test@example.com"); git(repo, "config", "user.name", "Test"); git(repo, "add", "."); From 587c86fde0ff6bcab47cdcd314891f1880f46e38 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 22 Oct 2026 14:47:55 +0000 Subject: [PATCH 2/2] fix: preserve exact scan file identity --- sdk/typescript/src/contract.ts | 34 +++-- .../tests-ts/custom-validation.test.ts | 131 ++++++++++++++++++ 2 files changed, 151 insertions(+), 14 deletions(-) diff --git a/sdk/typescript/src/contract.ts b/sdk/typescript/src/contract.ts index 4530a03b3d..9ff3c0ecfa 100644 --- a/sdk/typescript/src/contract.ts +++ b/sdk/typescript/src/contract.ts @@ -40,13 +40,13 @@ const DOCUMENTS = { const PRODUCER_NAME = "codex-security-plugin"; interface CheckedScanFile { path: string; - metadata: Stats; - parents: Array<{ path: string; metadata: Stats }>; + metadata: BigIntStats; + parents: Array<{ path: string; metadata: BigIntStats }>; } interface ScanRoot { path: string; - metadata: Stats; + metadata: BigIntStats; } export interface ScanExpectation { @@ -411,7 +411,7 @@ async function requireCheckedScanFile( throwIfAborted(signal); for (const part of parts.slice(0, -1)) { current = join(current, part); - const metadata = await lstat(current); + const metadata = await lstat(current, { bigint: true }); throwIfAborted(signal); if (!metadata.isDirectory()) { throw new Error("unsafe parent"); @@ -419,7 +419,7 @@ async function requireCheckedScanFile( parents.push({ path: current, metadata }); } const path = join(scanDir, ...parts); - const metadata = await lstat(path); + const metadata = await lstat(path, { bigint: true }); throwIfAborted(signal); if (!metadata.isFile()) { throw new ContractValidationError( @@ -637,13 +637,13 @@ async function requireScanRoot( throwIfAborted(signal); const absolute = resolve(scanDirectory); try { - const metadata = await lstat(absolute); + const metadata = await lstat(absolute, { bigint: true }); throwIfAborted(signal); const canonical = await realpath(absolute); throwIfAborted(signal); - const current = await lstat(absolute); + const current = await lstat(absolute, { bigint: true }); throwIfAborted(signal); - const returned = await lstat(canonical); + const returned = await lstat(canonical, { bigint: true }); throwIfAborted(signal); if ( !metadata.isDirectory() || @@ -657,7 +657,10 @@ async function requireScanRoot( throw new Error("not a directory"); } try { - requirePrivateOutputDirectory(returned, canonical); + requirePrivateOutputDirectory( + { mode: Number(returned.mode), uid: Number(returned.uid) }, + canonical, + ); await requireSecureOutputAncestry(canonical); } catch (error) { throw new ContractValidationError( @@ -683,7 +686,7 @@ async function verifyScanRoot( signal?: AbortSignal, ): Promise { try { - const current = await lstat(root.path); + const current = await lstat(root.path, { bigint: true }); throwIfAborted(signal); if ( !current.isDirectory() || @@ -692,7 +695,10 @@ async function verifyScanRoot( ) { throw new Error("scan directory changed while reading"); } - requirePrivateOutputDirectory(current, root.path); + requirePrivateOutputDirectory( + { mode: Number(current.mode), uid: Number(current.uid) }, + root.path, + ); await requireSecureOutputAncestry(root.path); } catch (error) { throwIfAborted(signal); @@ -956,7 +962,7 @@ async function openCheckedScanFile( checked.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK, ); - const opened = await file.stat(); + const opened = await file.stat({ bigint: true }); throwIfAborted(signal); if ( !opened.isFile() || @@ -968,7 +974,7 @@ async function openCheckedScanFile( ); } for (const parent of checked.parents) { - const current = await lstat(parent.path); + const current = await lstat(parent.path, { bigint: true }); throwIfAborted(signal); if ( !current.isDirectory() || @@ -980,7 +986,7 @@ async function openCheckedScanFile( ); } } - const current = await lstat(checked.path); + const current = await lstat(checked.path, { bigint: true }); throwIfAborted(signal); if ( !current.isFile() || diff --git a/sdk/typescript/tests-ts/custom-validation.test.ts b/sdk/typescript/tests-ts/custom-validation.test.ts index 2ca4edfa37..64f1d3c365 100644 --- a/sdk/typescript/tests-ts/custom-validation.test.ts +++ b/sdk/typescript/tests-ts/custom-validation.test.ts @@ -18,6 +18,7 @@ import { runCustomValidation, type CustomValidationResult, } from "../src/custom-validation.js"; +import { readScanFile } from "../src/contract.js"; import { customDiscoveryPrompt, customValidationConfig, @@ -237,6 +238,89 @@ describe("custom validation", () => { }, ); + test.each(["unchanged", "replaced"])( + "compares exact provisional file identity when numeric IDs collide: %s", + async (state) => { + const title = `compares exact provisional file identity when numeric IDs collide: ${state}`; + if (runTestInSubprocess(import.meta.path, title)) return; + const f = await fixture(); + const path = join(f.scanDir, "coverage.json"); + const before = 2n ** 60n; + const after = before + 1n; + expect(Number(before)).toBe(Number(after)); + const files = await import("node:fs/promises"); + const original = { + lstat: files.lstat, + open: files.open, + realpath: files.realpath, + }; + let replaced = false; + const inspecting = spyOn(files, "lstat").mockImplementation((async ( + ...args: Parameters + ) => { + const metadata = await original.lstat(...args); + if (args[0] === path) { + const identity = replaced ? after : before; + Object.defineProperty(metadata, "ino", { + value: + typeof metadata.ino === "bigint" ? identity : Number(identity), + }); + } + return metadata; + }) as typeof original.lstat); + const opening = spyOn(files, "open").mockImplementation( + async (...args) => { + const file = await original.open(...args); + if (args[0] === path) { + const identity = replaced ? after : before; + const stat = file.stat.bind(file); + spyOn(file, "stat").mockImplementation((async ( + ...options: Parameters + ) => { + const metadata = await stat(...options); + Object.defineProperty(metadata, "ino", { + value: + typeof metadata.ino === "bigint" + ? identity + : Number(identity), + }); + return metadata; + }) as typeof stat); + } + return file; + }, + ); + const resolving = spyOn(files, "realpath").mockImplementation((async ( + ...args: Parameters + ) => { + const canonical = await original.realpath(...args); + if (args[0] === path && state === "replaced" && !replaced) { + await rename(path, `${path}.original`); + await writeFile(path, await readFile(`${path}.original`)); + replaced = true; + } + return canonical; + }) as typeof original.realpath); + const run = mock(async () => JSON.stringify(result("reportable"))); + try { + if (state === "replaced") { + await expect(runCustomValidation({ ...f, run })).rejects.toThrow( + "expected the checked regular file", + ); + expect(replaced).toBe(true); + expect(run).not.toHaveBeenCalled(); + } else { + await runCustomValidation({ ...f, run }); + expect(run).toHaveBeenCalledTimes(1); + } + } finally { + resolving.mockRestore(); + opening.mockRestore(); + inspecting.mockRestore(); + } + }, + ); + test.each(["scan-manifest.json", "findings.json", "coverage.json"])( "reads the checked %s descriptor after pathname replacement", async (name) => { @@ -276,6 +360,53 @@ describe("custom validation", () => { }, ); + test("rejects scan root replacement when numeric directory IDs collide", async () => { + const title = + "rejects scan root replacement when numeric directory IDs collide"; + if (runTestInSubprocess(import.meta.path, title)) return; + const f = await fixture(); + const before = 2n ** 60n; + const after = before + 1n; + expect(Number(before)).toBe(Number(after)); + const files = await import("node:fs/promises"); + const original = { lstat: files.lstat, realpath: files.realpath }; + let replaced = false; + const inspecting = spyOn(files, "lstat").mockImplementation((async ( + ...args: Parameters + ) => { + const metadata = await original.lstat(...args); + if (args[0] === f.scanDir) { + const identity = replaced ? after : before; + Object.defineProperty(metadata, "ino", { + value: typeof metadata.ino === "bigint" ? identity : Number(identity), + }); + } + return metadata; + }) as typeof original.lstat); + const resolving = spyOn(files, "realpath").mockImplementation((async ( + ...args: Parameters + ) => { + const canonical = await original.realpath(...args); + if (args[0] === f.scanDir && !replaced) { + replaced = true; + await rename(f.scanDir, `${f.scanDir}.original`); + await mkdir(f.scanDir, { mode: 0o700 }); + } + return canonical; + }) as typeof original.realpath); + try { + await expect( + readScanFile(f.scanDir, "coverage.json", "coverage.json"), + ).rejects.toThrow( + "Scan directory must be an existing non-symlink directory", + ); + expect(replaced).toBe(true); + } finally { + resolving.mockRestore(); + inspecting.mockRestore(); + } + }); + test.each(["malformed", "directory"])( "rejects a %s provisional document before invoking validation", async (kind) => {