From a3d0ef12eac9370318a57e08493e4a71faef9cb1 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 21:09:37 -0700 Subject: [PATCH 1/9] test: stabilize randomized SDK test runs --- .github/workflows/node-ci.yml | 6 +- sdk/typescript/AGENTS.md | 6 +- sdk/typescript/package.json | 1 + sdk/typescript/tests-ts/config.test.ts | 150 ++++++++++++++++--------- 4 files changed, 104 insertions(+), 59 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index f04481a9f..af5af74a4 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -27,6 +27,8 @@ jobs: node: "24.0.0" - os: ubuntu-latest node: "24" + # Exercise a fresh seed without adding another full-suite job. + test_script: test:randomized - os: ubuntu-latest node: "26.0.0" - os: ubuntu-latest @@ -68,14 +70,14 @@ jobs: - name: Typecheck run: pnpm --dir sdk/typescript run types - - name: Test + - name: Test (${{ matrix.test_script || 'test' }}) timeout-minutes: 10 env: TEMP: ${{ runner.temp }} TMP: ${{ runner.temp }} TMPDIR: ${{ runner.temp }} CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: "false" - run: pnpm --dir sdk/typescript run test + run: pnpm --dir sdk/typescript run ${{ matrix.test_script || 'test' }} - name: Check formatting run: pnpm --dir sdk/typescript run format diff --git a/sdk/typescript/AGENTS.md b/sdk/typescript/AGENTS.md index bb3207e19..f2c82678c 100644 --- a/sdk/typescript/AGENTS.md +++ b/sdk/typescript/AGENTS.md @@ -38,9 +38,11 @@ accepted and rejected inputs, and each real bug or security boundary. From the SDK directory, run a focused test while iterating, then run the package checks: ```bash -bun test tests-ts/.test.ts -bun test --randomize --seed 12345 +bun test --timeout 30000 tests-ts/.test.ts +pnpm run test:randomized --seed 12345 pnpm run types pnpm run format pnpm run test ``` + +To reproduce a randomized failure, use the seed printed in Bun's test summary. diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 7f8d04e1f..3f60eeee6 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -49,6 +49,7 @@ "lint": "tsc --noEmit", "prepack": "node --run build", "test": "bun test --timeout 30000 ./tests-ts", + "test:randomized": "bun test --timeout 30000 --randomize ./tests-ts", "test:package": "node scripts/smoke-package.mjs", "types": "pnpm run generate:models:check && tsc --noEmit" }, diff --git a/sdk/typescript/tests-ts/config.test.ts b/sdk/typescript/tests-ts/config.test.ts index f3fc3fb2d..325d259ab 100644 --- a/sdk/typescript/tests-ts/config.test.ts +++ b/sdk/typescript/tests-ts/config.test.ts @@ -62,6 +62,45 @@ function runPinnedCodex(codexHome: string, arguments_: readonly string[]) { ); } +function macOsSandboxUnavailable(): boolean { + if (process.platform !== "darwin") return false; + + // Check the host independently of the generated scan permission profile. + const result = Bun.spawnSync( + [ + "/usr/bin/sandbox-exec", + "-p", + "(version 1) (allow default)", + "/usr/bin/true", + ], + { stdout: "pipe", stderr: "pipe" }, + ); + return ( + result.exitCode !== 0 && + new TextDecoder().decode(result.stderr).trim() === + "sandbox-exec: sandbox_apply: Operation not permitted" + ); +} + +async function scanSandboxFixture() { + const root = await temporaryDirectory(); + const codexHome = join(root, "codex-home"); + const workspace = join(root, "workspace"); + const stateDirectory = join(root, "state"); + await Promise.all( + [codexHome, workspace, stateDirectory].map((path) => mkdir(path)), + ); + await writeCodexConfig( + join(codexHome, "config.toml"), + scanRuntimeCodexConfig( + await mergedCodexConfig({}), + stateDirectory, + codexHome, + ), + ); + return { root, codexHome, workspace }; +} + describe("Codex configuration", () => { test("never grants scan execution approvals by default", async () => { expect(await mergedCodexConfig({})).toMatchObject({ @@ -335,63 +374,64 @@ describe("Codex configuration", () => { }); }); - test("denies writes outside the scan workspace and state directory", async () => { - const root = await temporaryDirectory(); - const codexHome = join(root, "codex-home"); - const workspace = join(root, "workspace"); - const stateDirectory = join(root, "state"); - await Promise.all( - [codexHome, workspace, stateDirectory].map((path) => mkdir(path)), - ); - await writeCodexConfig( - join(codexHome, "config.toml"), - scanRuntimeCodexConfig( - await mergedCodexConfig({}), - stateDirectory, - codexHome, - ), - ); - const node = Bun.which("node"); - expect(node).not.toBeNull(); - const attemptWrite = (path: string) => - runPinnedCodex(codexHome, [ - "sandbox", - "--config", - "permissions.codex_security_scan.network.enabled=true", - "--permission-profile", - "codex_security_scan", - "--cd", - workspace, - node!, - "-e", - "require('node:fs').writeFileSync(process.argv[1], 'probe')", - path, - ]); - - const allowed = join(workspace, "inside.txt"); - const permitted = attemptWrite(allowed); - const outside = join(root, "outside.txt"); - expect(attemptWrite(outside).exitCode).not.toBe(0); - await expect(stat(outside)).rejects.toMatchObject({ code: "ENOENT" }); - if (permitted.exitCode !== 0) { - const details = new TextDecoder().decode(permitted.stderr); - if ( - process.platform === "linux" && - /bwrap: (?:setting up uid map: Permission denied|loopback: Failed RTM_NEWADDR: Operation not permitted)/u.test( - details, - ) - ) { - expect(runPinnedCodex(codexHome, ["features", "list"]).exitCode).toBe( - 0, + test("writes scan permissions accepted by the pinned Codex CLI", async () => { + const { codexHome, workspace } = await scanSandboxFixture(); + const result = runPinnedCodex(codexHome, [ + "--cd", + workspace, + "features", + "list", + ]); + expect(result.exitCode, new TextDecoder().decode(result.stderr)).toBe(0); + expect(result.stdout.length).toBeGreaterThan(0); + }); + + test.skipIf(macOsSandboxUnavailable())( + "denies writes outside the scan workspace and state directory", + async () => { + const { root, codexHome, workspace } = await scanSandboxFixture(); + const node = Bun.which("node"); + expect(node).not.toBeNull(); + const attemptWrite = (path: string) => + runPinnedCodex(codexHome, [ + "sandbox", + "--config", + "permissions.codex_security_scan.network.enabled=true", + "--permission-profile", + "codex_security_scan", + "--cd", + workspace, + node!, + "-e", + "require('node:fs').writeFileSync(process.argv[1], 'probe')", + path, + ]); + + const allowed = join(workspace, "inside.txt"); + const permitted = attemptWrite(allowed); + const outside = join(root, "outside.txt"); + expect(attemptWrite(outside).exitCode).not.toBe(0); + await expect(stat(outside)).rejects.toMatchObject({ code: "ENOENT" }); + if (permitted.exitCode !== 0) { + const details = new TextDecoder().decode(permitted.stderr); + if ( + process.platform === "linux" && + /bwrap: (?:setting up uid map: Permission denied|loopback: Failed RTM_NEWADDR: Operation not permitted)/u.test( + details, + ) + ) { + expect(runPinnedCodex(codexHome, ["features", "list"]).exitCode).toBe( + 0, + ); + return; + } + throw new Error( + `The pinned Codex CLI rejected an allowed scan write: ${details}`, ); - return; } - throw new Error( - `The pinned Codex CLI rejected an allowed scan write: ${details}`, - ); - } - expect(await readFile(allowed, "utf8")).toBe("probe"); - }); + expect(await readFile(allowed, "utf8")).toBe("probe"); + }, + ); test("writes Windows sandbox settings accepted by the pinned Codex CLI", async () => { const root = await temporaryDirectory(); From 5e4e01444f1ee5807af03190153942e2a9a71072 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 21:12:02 -0700 Subject: [PATCH 2/9] test: cover randomized CI command selection --- sdk/typescript/tests-ts/skeleton.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index dc72f52fe..c39b49a69 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -74,13 +74,19 @@ describe("TypeScript package skeleton", () => { expect(packageJson.scripts.test).toBe( "bun test --timeout 30000 ./tests-ts", ); + expect(packageJson.scripts["test:randomized"]).toBe( + "bun test --timeout 30000 --randomize ./tests-ts", + ); expect(ciWorkflow).toContain( "run: node sdk/typescript/scripts/run-windows-ci-tests.mjs ${{ matrix.shard }}", ); expect(ciWorkflow).toContain( "name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }}", ); - expect(ciWorkflow).toContain("run: pnpm --dir sdk/typescript run test"); + expect(ciWorkflow).toContain("test_script: test:randomized"); + expect(ciWorkflow).toContain( + "run: pnpm --dir sdk/typescript run ${{ matrix.test_script || 'test' }}", + ); expect(ciWorkflow).not.toContain("--timeout 60000"); }); From dbe870e972198be66f816ceb27e7908a58745438 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 21:31:19 -0700 Subject: [PATCH 3/9] test: reuse the normal runner for randomized tests --- sdk/typescript/package.json | 2 +- sdk/typescript/tests-ts/skeleton.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 3f60eeee6..43c4fbe66 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -49,7 +49,7 @@ "lint": "tsc --noEmit", "prepack": "node --run build", "test": "bun test --timeout 30000 ./tests-ts", - "test:randomized": "bun test --timeout 30000 --randomize ./tests-ts", + "test:randomized": "pnpm run test --randomize", "test:package": "node scripts/smoke-package.mjs", "types": "pnpm run generate:models:check && tsc --noEmit" }, diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index c39b49a69..d8e315d7f 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -75,7 +75,7 @@ describe("TypeScript package skeleton", () => { "bun test --timeout 30000 ./tests-ts", ); expect(packageJson.scripts["test:randomized"]).toBe( - "bun test --timeout 30000 --randomize ./tests-ts", + "pnpm run test --randomize", ); expect(ciWorkflow).toContain( "run: node sdk/typescript/scripts/run-windows-ci-tests.mjs ${{ matrix.shard }}", From a96cf76ad78b89e9c97285228fee468b6dcb347c Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 21:46:48 -0700 Subject: [PATCH 4/9] test: randomize SDK tests by default --- .github/workflows/node-ci.yml | 6 ++---- sdk/typescript/AGENTS.md | 4 ++-- sdk/typescript/bunfig.toml | 2 ++ sdk/typescript/package.json | 1 - sdk/typescript/tests-ts/skeleton.test.ts | 15 +++++++-------- 5 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 sdk/typescript/bunfig.toml diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index af5af74a4..f04481a9f 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -27,8 +27,6 @@ jobs: node: "24.0.0" - os: ubuntu-latest node: "24" - # Exercise a fresh seed without adding another full-suite job. - test_script: test:randomized - os: ubuntu-latest node: "26.0.0" - os: ubuntu-latest @@ -70,14 +68,14 @@ jobs: - name: Typecheck run: pnpm --dir sdk/typescript run types - - name: Test (${{ matrix.test_script || 'test' }}) + - name: Test timeout-minutes: 10 env: TEMP: ${{ runner.temp }} TMP: ${{ runner.temp }} TMPDIR: ${{ runner.temp }} CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: "false" - run: pnpm --dir sdk/typescript run ${{ matrix.test_script || 'test' }} + run: pnpm --dir sdk/typescript run test - name: Check formatting run: pnpm --dir sdk/typescript run format diff --git a/sdk/typescript/AGENTS.md b/sdk/typescript/AGENTS.md index f2c82678c..fb7b4cb05 100644 --- a/sdk/typescript/AGENTS.md +++ b/sdk/typescript/AGENTS.md @@ -39,10 +39,10 @@ From the SDK directory, run a focused test while iterating, then run the package ```bash bun test --timeout 30000 tests-ts/.test.ts -pnpm run test:randomized --seed 12345 +pnpm run test --seed 12345 pnpm run types pnpm run format pnpm run test ``` -To reproduce a randomized failure, use the seed printed in Bun's test summary. +Tests run in random order by default. To reproduce a failure, use the seed printed in Bun's test summary. diff --git a/sdk/typescript/bunfig.toml b/sdk/typescript/bunfig.toml new file mode 100644 index 000000000..d1639b19a --- /dev/null +++ b/sdk/typescript/bunfig.toml @@ -0,0 +1,2 @@ +[test] +randomize = true diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 43c4fbe66..7f8d04e1f 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -49,7 +49,6 @@ "lint": "tsc --noEmit", "prepack": "node --run build", "test": "bun test --timeout 30000 ./tests-ts", - "test:randomized": "pnpm run test --randomize", "test:package": "node scripts/smoke-package.mjs", "types": "pnpm run generate:models:check && tsc --noEmit" }, diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index d8e315d7f..807ecc753 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -1,5 +1,6 @@ import { readFile } from "node:fs/promises"; import { describe, expect, test } from "bun:test"; +import { parse } from "smol-toml"; import { CodexSecurity, CodexSecurityError, VERSION } from "../src/index.js"; import { main } from "../src/cli.js"; @@ -62,7 +63,7 @@ describe("TypeScript package skeleton", () => { } }); - test("uses the default test timeout consistently across CI platforms", async () => { + test("randomizes tests with the default timeout across CI platforms", async () => { const packageJson = JSON.parse( await readFile(new URL("../package.json", import.meta.url), "utf8"), ); @@ -70,23 +71,21 @@ describe("TypeScript package skeleton", () => { new URL("../../../.github/workflows/node-ci.yml", import.meta.url), "utf8", ); + const bunConfig = parse( + await readFile(new URL("../bunfig.toml", import.meta.url), "utf8"), + ); expect(packageJson.scripts.test).toBe( "bun test --timeout 30000 ./tests-ts", ); - expect(packageJson.scripts["test:randomized"]).toBe( - "pnpm run test --randomize", - ); + expect(bunConfig).toMatchObject({ test: { randomize: true } }); expect(ciWorkflow).toContain( "run: node sdk/typescript/scripts/run-windows-ci-tests.mjs ${{ matrix.shard }}", ); expect(ciWorkflow).toContain( "name: windows-latest / node-${{ matrix.node == '22.13.0' && '22' || matrix.node }}", ); - expect(ciWorkflow).toContain("test_script: test:randomized"); - expect(ciWorkflow).toContain( - "run: pnpm --dir sdk/typescript run ${{ matrix.test_script || 'test' }}", - ); + expect(ciWorkflow).toContain("run: pnpm --dir sdk/typescript run test"); expect(ciWorkflow).not.toContain("--timeout 60000"); }); From d9cdf42e13a49d98fd51c19b71d7dbb5b5627113 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 22:05:00 -0700 Subject: [PATCH 5/9] test: isolate the file-input regression spy --- sdk/typescript/tests-ts/cli-skills.test.ts | 39 ++++++++++++++-------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/sdk/typescript/tests-ts/cli-skills.test.ts b/sdk/typescript/tests-ts/cli-skills.test.ts index 288847de3..df7dc240b 100644 --- a/sdk/typescript/tests-ts/cli-skills.test.ts +++ b/sdk/typescript/tests-ts/cli-skills.test.ts @@ -12,6 +12,7 @@ import { skillCommandFailure, } from "../src/cli.js"; import { capture, dependencies } from "./cli-fixtures.js"; +import { runMockInSubprocess } from "./support/isolated-mock.js"; describe("CLI skill commands", () => { test("runs validation and patch skills with file and literal inputs", async () => { @@ -189,6 +190,14 @@ describe("CLI skill commands", () => { ? ["symbolic link"] : ["symbolic link", "FIFO"], )("rejects finding files replaced with a %s", async (replacement) => { + if ( + runMockInSubprocess( + import.meta.path, + `rejects finding files replaced with a ${replacement}`, + ) + ) { + return; + } const root = await mkdtemp(join(tmpdir(), "codex-security-skill-inputs-")); try { const repository = join(root, "repository"); @@ -200,6 +209,7 @@ describe("CLI skill commands", () => { const canonicalSelected = await filesystem.realpath(selected); const originalOpen = filesystem.open; + let replaced = false; const opening = spyOn(filesystem, "open").mockImplementation( async (...args: Parameters) => { if (String(args[0]) === canonicalSelected) { @@ -207,6 +217,7 @@ describe("CLI skill commands", () => { await rm(selected); if (replacement === "FIFO") execFileSync("mkfifo", [selected]); else await symlink(external, selected); + replaced = true; } return await originalOpen(...args); }, @@ -215,20 +226,20 @@ describe("CLI skill commands", () => { try { let started = false; const stderr = capture(); - expect( - await main( - ["validate", "finding.txt"], - capture().stream, - stderr.stream, - dependencies({ - currentDirectory: repository, - onCodex: () => { - started = true; - return 0; - }, - }), - ), - ).toBe(2); + const status = await main( + ["validate", "finding.txt"], + capture().stream, + stderr.stream, + dependencies({ + currentDirectory: repository, + onCodex: () => { + started = true; + return 0; + }, + }), + ); + expect(replaced, "the file-open replacement hook ran").toBe(true); + expect(status).toBe(2); expect(stderr.text()).not.toContain("SYNTHETIC_EXTERNAL_FINDING"); expect(started).toBe(false); } finally { From a6b70bd0ef9c8c127afce34439e5a1ac0ff8a03f Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:04:44 -0700 Subject: [PATCH 6/9] test: synchronize parallel scan assertions --- sdk/typescript/tests-ts/api.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 37896482a..e8d6269f3 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -4578,10 +4578,7 @@ describe("CodexSecurity orchestration", () => { expect(before["deep_scan"]).toMatchObject({ workers: index + 2, }); - await Promise.race([ - concurrentScans, - new Promise((resolve) => setTimeout(resolve, 5_000)), - ]); + await concurrentScans; const after = parseToml( await readFile(deepScanConfigPath!, "utf8"), ); @@ -4604,7 +4601,9 @@ describe("CodexSecurity orchestration", () => { try { const results = await Promise.allSettled( clients.map((client, index) => - client.run(repository, { mode: "deep", workers: index + 2 }), + client + .run(repository, { mode: "deep", workers: index + 2 }) + .finally(releaseScans), ), ); for (const result of results) { @@ -4627,6 +4626,7 @@ describe("CodexSecurity orchestration", () => { ), ).toBe(true); } finally { + releaseScans(); await Promise.all(clients.map(async (client) => await client.close())); } }); From 646803de612c644ce8a7333eab6ae36e2787c5d9 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:22:06 -0700 Subject: [PATCH 7/9] fix(cli): compare exact input file identities --- sdk/typescript/src/cli.ts | 42 +++++------ sdk/typescript/tests-ts/cli-skills.test.ts | 83 ++++++++++++++++++++++ 2 files changed, 105 insertions(+), 20 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 5b9211919..30a5f986b 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7,7 +7,7 @@ import { existsSync, lstatSync, realpathSync, - type Stats, + type BigIntStats, writeSync, } from "node:fs"; import { @@ -598,9 +598,9 @@ async function readPromptFiles( async function readRegularInputFile( path: string, repository: string, - metadata?: Pick, + metadata?: Pick, ): Promise { - const selected = metadata ?? (await lstat(path)); + const selected = metadata ?? (await lstat(path, { bigint: true })); if (!selected.isFile()) { throw new CodexSecurityError("Input files must be regular files."); } @@ -627,7 +627,7 @@ async function readRegularInputFile( (constants.O_NONBLOCK ?? 0), ); try { - const opened = await file.stat(); + const opened = await file.stat({ bigint: true }); if ( !opened.isFile() || opened.dev !== selected.dev || @@ -3244,22 +3244,24 @@ async function runSkill( localDeviceRoot !== normalizedDeviceRoot); if (!windowsNetworkPath) { const path = resolve(directory, input); - const metadata = await lstat(path).catch((error: unknown) => { - if ( - typeof error === "object" && - error !== null && - "code" in error && - (error.code === "ENOENT" || - error.code === "ENOTDIR" || - error.code === "ENAMETOOLONG" || - error.code === "EINVAL") - ) { - return undefined; - } - throw new CodexSecurityError( - "Could not read the finding or issue input.", - ); - }); + const metadata = await lstat(path, { bigint: true }).catch( + (error: unknown) => { + if ( + typeof error === "object" && + error !== null && + "code" in error && + (error.code === "ENOENT" || + error.code === "ENOTDIR" || + error.code === "ENAMETOOLONG" || + error.code === "EINVAL") + ) { + return undefined; + } + throw new CodexSecurityError( + "Could not read the finding or issue input.", + ); + }, + ); if (metadata !== undefined) { if (!metadata.isFile()) { throw new CodexSecurityError( diff --git a/sdk/typescript/tests-ts/cli-skills.test.ts b/sdk/typescript/tests-ts/cli-skills.test.ts index 77f4ab189..ddc49c24a 100644 --- a/sdk/typescript/tests-ts/cli-skills.test.ts +++ b/sdk/typescript/tests-ts/cli-skills.test.ts @@ -449,6 +449,89 @@ describe("CLI skill commands", () => { } }); + test("rejects input replacements whose numeric file IDs collide", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "rejects input replacements whose numeric file IDs collide", + ) + ) { + return; + } + const root = await mkdtemp(join(tmpdir(), "codex-security-file-identity-")); + const selected = join(root, "finding.txt"); + const replacement = join(root, "replacement.txt"); + const selectedInode = 2n ** 60n; + const replacementInode = selectedInode + 1n; + expect(Number(selectedInode)).toBe(Number(replacementInode)); + await writeFile(selected, "ordinary finding\n"); + await writeFile(replacement, "SYNTHETIC_REPLACEMENT_FINDING\n"); + const canonicalSelected = await filesystem.realpath(selected); + const originalLstat = filesystem.lstat; + const originalOpen = filesystem.open; + let restoreOpenedStat: (() => void) | undefined; + let replaced = false; + const reading = spyOn(filesystem, "lstat").mockImplementation((async ( + ...args: Parameters + ) => { + const metadata = await originalLstat(...args); + if (String(args[0]) === selected) { + metadata.ino = + typeof metadata.ino === "bigint" + ? selectedInode + : Number(selectedInode); + } + return metadata; + }) as typeof filesystem.lstat); + const opening = spyOn(filesystem, "open").mockImplementation( + async (...args: Parameters) => { + if (String(args[0]) !== canonicalSelected) { + return await originalOpen(...args); + } + replaced = true; + const file = await originalOpen(replacement, args[1], args[2]); + const originalStat = file.stat.bind(file); + const openedStat = spyOn(file, "stat").mockImplementation((async ( + ...statArgs: Parameters + ) => { + const metadata = await originalStat(...statArgs); + metadata.ino = + typeof metadata.ino === "bigint" + ? replacementInode + : Number(replacementInode); + return metadata; + }) as typeof file.stat); + restoreOpenedStat = () => openedStat.mockRestore(); + return file; + }, + ); + try { + let started = false; + const stderr = capture(); + const status = await main( + ["validate", "finding.txt"], + capture().stream, + stderr.stream, + dependencies({ + currentDirectory: root, + onCodex: () => { + started = true; + return 0; + }, + }), + ); + expect(replaced).toBe(true); + expect(status).toBe(2); + expect(stderr.text()).not.toContain("SYNTHETIC_REPLACEMENT_FINDING"); + expect(started).toBe(false); + } finally { + restoreOpenedStat?.(); + opening.mockRestore(); + reading.mockRestore(); + await rm(root, { recursive: true, force: true }); + } + }); + test.each( process.platform === "win32" ? ["symbolic link"] From 433be334e9002093ec97cb205b573ac28da16ed9 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:22:15 -0700 Subject: [PATCH 8/9] test: close pending logins reliably --- sdk/typescript/tests-ts/api.test.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index e8d6269f3..1c5af7e12 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -6515,20 +6515,17 @@ setInterval(() => {}, 1000); ); const login = client.loginApiKey("secret-key"); void login.catch(() => undefined); - for (let attempt = 0; attempt < 100; attempt += 1) { - const started = await import("node:fs/promises").then(({ stat }) => - stat(ready).catch(() => null), - ); - if (started !== null) break; - await new Promise((resolve) => setTimeout(resolve, 10)); + try { + const deadline = Date.now() + 10_000; + while (!existsSync(ready) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 25)); + } + expect(existsSync(ready), "the fake login process started").toBe(true); + await client.close(); + await expect(login).rejects.toThrow(); + await expect(stat(codexHome)).resolves.toBeDefined(); + } finally { + await client.close(); } - await expect( - import("node:fs/promises").then(({ stat }) => stat(ready)), - ).resolves.toBeDefined(); - await client.close(); - await expect(login).rejects.toThrow(); - await expect( - import("node:fs/promises").then(({ stat }) => stat(codexHome)), - ).resolves.toBeDefined(); - }); + }, 30_000); }); From 909e4d0f9eceb319b154e8681549eaed98bf2c43 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:22:34 -0700 Subject: [PATCH 9/9] fix(sdk): exclude internal helpers from public declarations --- sdk/typescript/src/api.ts | 1 + sdk/typescript/tsconfig.build.json | 1 + 2 files changed, 2 insertions(+) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index a498b213b..3ada669f8 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -2232,6 +2232,7 @@ interface ScanEventRunOptions { onObserverError?: (observer: ScanObserverName, error: unknown) => void; } +/** @internal */ export async function runScanEvents( options: ScanEventRunOptions, ): Promise { diff --git a/sdk/typescript/tsconfig.build.json b/sdk/typescript/tsconfig.build.json index ccf7d2ecb..cf223fc5e 100644 --- a/sdk/typescript/tsconfig.build.json +++ b/sdk/typescript/tsconfig.build.json @@ -7,6 +7,7 @@ "outDir": "dist", "declaration": true, "declarationMap": true, + "stripInternal": true, "sourceMap": true, "inlineSources": true, "noEmit": false,