diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index dcc7dafffde..1133148f2be 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1012,6 +1012,7 @@ "native-profile-route-security.test.ts": "codex-integration", "native-profile-stage-lifecycle.test.ts": "codex-integration", "native-profile-startup.test.ts": "codex-integration", + "native-profile-startup-publication.test.ts": "codex-integration", "native-profile-store.test.ts": "codex-integration", "new-model-policy.test.ts": "providers", "nous-oauth-live.test.ts": "providers", diff --git a/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts b/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts index 41a61c21023..075e3c33186 100644 --- a/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts +++ b/tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts @@ -12,6 +12,10 @@ const provider: OcxProviderConfig = { baseUrl: "https://example.test/v1", apiKey: "sk-test", authMode: "key", + // The wire role folds to `system` unless a destination is recorded as accepting + // `developer`; this suite is about tool-result repair ordering, so it declares the + // destination rather than asserting the default. + foldDeveloperRoleToSystem: false, }; interface ChatMsg { diff --git a/tests/codex-integration/native-profile-startup-publication.test.ts b/tests/codex-integration/native-profile-startup-publication.test.ts new file mode 100644 index 00000000000..7c885e24aa7 --- /dev/null +++ b/tests/codex-integration/native-profile-startup-publication.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { publishFixtureFile } from "../helpers/fixture-file-publisher"; + +test("fixture publication refuses a pre-positioned temporary symlink", () => { + const root = mkdtempSync(join(tmpdir(), "ocx-fixture-publish-")); + try { + const marker = join(root, "settled"); + const victim = join(root, "victim"); + writeFileSync(victim, "original", "utf8"); + symlinkSync(victim, `${marker}.${process.pid}.tmp`); + + expect(() => publishFixtureFile(marker, "replacement")).toThrow(); + expect(readFileSync(victim, "utf8")).toBe("original"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 17dadddff96..cf6be0d98cd 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -838,6 +838,7 @@ "native-profile-route-security.test.ts": "codex-integration", "native-profile-stage-lifecycle.test.ts": "codex-integration", "native-profile-startup.test.ts": "codex-integration", + "native-profile-startup-publication.test.ts": "codex-integration", "native-profile-store.test.ts": "codex-integration", "new-model-policy.test.ts": "providers", "nous-oauth-live.test.ts": "providers", diff --git a/tests/helpers/fixture-file-publisher.ts b/tests/helpers/fixture-file-publisher.ts new file mode 100644 index 00000000000..c4c1e270a0f --- /dev/null +++ b/tests/helpers/fixture-file-publisher.ts @@ -0,0 +1,35 @@ +import { + closeSync, + fstatSync, + lstatSync, + openSync, + renameSync, + writeFileSync, +} from "node:fs"; + +/** + * Publish a non-secret test marker without invoking the production secret-path hardening. + * Exclusive creation and descriptor/entry identity checks prevent a predictable temporary + * path from becoming a symlink-following write, while rename keeps the reader view atomic. + */ +export function publishFixtureFile(path: string, content: string): void { + const temporaryPath = `${path}.${process.pid}.tmp`; + const descriptor = openSync(temporaryPath, "wx", 0o600); + try { + const opened = fstatSync(descriptor); + const linked = lstatSync(temporaryPath); + if (!opened.isFile() || !linked.isFile() + || opened.dev !== linked.dev || opened.ino !== linked.ino) { + throw new Error("fixture temporary file identity changed before write"); + } + writeFileSync(descriptor, content, "utf8"); + const written = fstatSync(descriptor); + const published = lstatSync(temporaryPath); + if (!published.isFile() || written.dev !== published.dev || written.ino !== published.ino) { + throw new Error("fixture temporary file identity changed after write"); + } + } finally { + closeSync(descriptor); + } + renameSync(temporaryPath, path); +} diff --git a/tests/helpers/native-profile-startup-child.ts b/tests/helpers/native-profile-startup-child.ts index 90c3dfd9113..52aae469187 100644 --- a/tests/helpers/native-profile-startup-child.ts +++ b/tests/helpers/native-profile-startup-child.ts @@ -1,4 +1,4 @@ -import { appendFileSync, existsSync, renameSync, writeFileSync } from "node:fs"; +import { appendFileSync, existsSync } from "node:fs"; import { NativeProfileManager } from "../../src/codex/native-profile-manager"; import { isCodexAccountUsable } from "../../src/codex/account-usability"; @@ -10,6 +10,7 @@ import { } from "../../src/codex/native-profile-startup"; import type { NativeProfileKey, NativeProfileKeyProvider } from "../../src/codex/native-profile-types"; import { startServer } from "../../src/server"; +import { publishFixtureFile } from "./fixture-file-publisher"; const launchedAt = Number(process.env.NATIVE_STARTUP_LAUNCHED_AT ?? Date.now()); @@ -25,23 +26,6 @@ const phase = (name: string): void => { phase("child-entry"); -/** - * A disposable port number is not a secret, so it must not travel through the production - * secret writer. On Windows `atomicWriteFile` runs `hardenSecretPath(..., required: true)` - * twice (`src/config/atomic-write.ts`), each of which can spawn PowerShell for SID resolution - * and several `icacls` passes budgeted at 30s apiece — an ACL ceremony performed inside the - * window the parent measures as "time to reach a port". - * - * The parent's actual contract is narrower (#1061): it treats existence as readiness and parses - * immediately, so it must never observe the file between create and write. A rename within the - * same directory gives exactly that — a reader sees either nothing or the whole document. - */ -function publishFixtureFile(path: string, content: string): void { - const tmp = `${path}.${process.pid}.tmp`; - writeFileSync(tmp, content, "utf8"); - renameSync(tmp, path); -} - const required = (name: string): string => { const value = process.env[name]; if (!value) throw new Error(`missing ${name}`); diff --git a/tests/responses/chat-inline-document-bytes.test.ts b/tests/responses/chat-inline-document-bytes.test.ts index dd88fa05788..a718c1c376a 100644 --- a/tests/responses/chat-inline-document-bytes.test.ts +++ b/tests/responses/chat-inline-document-bytes.test.ts @@ -28,6 +28,10 @@ const chatProvider: OcxProviderConfig = { adapter: "openai-chat", baseUrl: "https://gateway.example.internal/v1", apiKey: "k", + // The wire role folds to `system` unless a destination is recorded as accepting + // `developer`; the document test asserts the role a turn keeps, so it declares the + // destination rather than asserting the default. + foldDeveloperRoleToSystem: false, }; const anthropicProvider = { adapter: "anthropic",