Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion scripts/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ export interface IsolatedTestEnvironment {
cleanup(): void;
}

/**
* Credentials of the developer's own OpenCodex install that the sandbox must not inherit. A
* Windows install stores its data-plane token as a user environment variable, so every shell on
* that machine carries it; a test that then builds a service definition or starts a proxy reads
* the live token instead of its fixture and fails only on a developer machine.
*/
export const LIVE_INSTALL_CREDENTIAL_ENV = [
"OPENCODEX_API_AUTH_TOKEN",
"OPENCODEX_ADMIN_AUTH_TOKEN",
"OCX_API_TOKEN_FILE",
] as const;

export function createIsolatedTestEnvironment(
baseEnv: Record<string, string | undefined> = process.env,
): IsolatedTestEnvironment {
Expand Down Expand Up @@ -53,11 +65,13 @@ export function createIsolatedTestEnvironment(
mkdirSync(join(root, "AppData", "Roaming"), { recursive: true });
}
writeTestTempOwner(root, baseEnv[TEST_RUN_ID_ENV]);
const inherited = { ...baseEnv };
for (const name of LIVE_INSTALL_CREDENTIAL_ENV) delete inherited[name];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip credential keys case-insensitively on Windows

When a Windows environment contains one of these variables with noncanonical casing, such as opencodex_api_auth_token, spreading baseEnv creates an ordinary case-sensitive object and these exact-name deletions leave the credential in isolated.env; runTestLane then passes it to Bun.spawn, where Windows treats it as the same variable and the sandbox can again consume the developer's live token. Filter the copied keys by an uppercased name and cover a mixed-case input in the regression test.

AGENTS.md reference: scripts/AGENTS.md:L14-L17

Useful? React with 👍 / 👎.


return {
root,
env: {
...baseEnv,
...inherited,
// Captured BEFORE HOME is overwritten: once the child starts with a rewritten
// HOME, `homedir()` returns the sandbox, so this hand-off is the only way the
// real-home write guard can still know which path to protect.
Expand Down
18 changes: 18 additions & 0 deletions tests/ci-workflows/test-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
changedSelectionFailure,
captureTestOutput,
createIsolatedTestEnvironment,
LIVE_INSTALL_CREDENTIAL_ENV,
ensureGuiDependencies,
inspectChangedRun,
resolveBunTestArgs,
Expand Down Expand Up @@ -327,6 +328,23 @@ describe("test runner isolation", () => {
expect(existsSync(isolated.root)).toBe(false);
});

test("drops the developer's live install credentials and keeps the rest of the environment", () => {
const isolated = createIsolatedTestEnvironment({
PATH: "/test/bin",
FIXTURE: "unchanged",
OPENCODEX_API_AUTH_TOKEN: "live-data-token",
OPENCODEX_ADMIN_AUTH_TOKEN: "live-admin-token",
OCX_API_TOKEN_FILE: "/real/home/.opencodex/service-api-token",
});
try {
for (const name of LIVE_INSTALL_CREDENTIAL_ENV) expect(name in isolated.env).toBe(false);
expect(isolated.env.FIXTURE).toBe("unchanged");
expect(isolated.env.PATH).toBe("/test/bin");
} finally {
isolated.cleanup();
}
});

test.if(process.platform === "win32")("gives the Windows sandbox a real profile shape", () => {
const isolated = createIsolatedTestEnvironment({ PATH: "C:\\test\\bin" });
try {
Expand Down
2 changes: 1 addition & 1 deletion tests/clients/client-link-tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test("spawns the client tunnel with the exact local forward argv and writes a pr
argv: fake.children[0]!.child.argv,
ownerPid: process.pid,
});
expect(statSync(pidfile).mode & 0o777).toBe(0o600);
if (process.platform !== "win32") expect(statSync(pidfile).mode & 0o777).toBe(0o600);
fake.children[0]!.resolve(0);
await handle.stop();
expect(existsSync(pidfile)).toBe(false);
Expand Down
3 changes: 2 additions & 1 deletion tests/clients/link-compensation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ test("persists strict compensation markers atomically with private permissions",
const since = "2026-09-25T00:00:00.000Z";
markCompensationFailed(linkId, since, path);
expect(readCompensation(path)).toEqual({ version: 1, entries: { [linkId]: { reason: "compensation_failed", since } } });
expect(statSync(path).mode & 0o777).toBe(0o600);
// Windows has no POSIX mode bits; the file is protected by the NTFS ACL hardening instead.
if (process.platform !== "win32") expect(statSync(path).mode & 0o777).toBe(0o600);
expect(JSON.parse(readFileSync(path, "utf8"))).toEqual(readCompensation(path));
clearCompensationFailed(linkId, path);
expect(readCompensation(path)).toEqual({ version: 1, entries: {} });
Expand Down
4 changes: 3 additions & 1 deletion tests/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*/
import { afterAll } from "bun:test";
import { isTestHomeGuardArmed, protectedHomeForTests } from "../src/lib/test-home-guard";
import { createIsolatedTestEnvironment } from "../scripts/test";
import { createIsolatedTestEnvironment, LIVE_INSTALL_CREDENTIAL_ENV } from "../scripts/test";
import {
acquireTestRunLock,
resolveBareTestRunIdentity,
Expand All @@ -47,6 +47,8 @@ const isolated = createIsolatedTestEnvironment();
for (const [key, value] of Object.entries(isolated.env)) {
if (value !== undefined) process.env[key] = value;
}
// The sandbox drops these from its env, but this process started with them, so remove them here.
for (const name of LIVE_INSTALL_CREDENTIAL_ENV) delete process.env[name];

// Arm the guard once the sandbox is in place, and BEFORE the run lock.
//
Expand Down
Loading