utils/codexAuth.test.ts
Rama main · réplica de lectura del repositorio original.
import { spawn } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, isAbsolute, join } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveSpawnTarget } from "./codexAuth.ts";
const isWindows = process.platform === "win32";
function shimDir(): string {
return mkdtempSync(join(tmpdir(), "codex-shim-"));
}
/** the exact shape `npm i -g @openai/codex` leaves on Windows: a `.cmd` shim
* around a JS entry point, and no `codex.exe` anywhere on PATH. */
function writeCmdShim(directory: string, name: string): string {
const shim = join(directory, `${name}.cmd`);
writeFileSync(shim, "@echo off\r\necho codex-cli 0.152.0\r\n");
return shim;
}
function writePosixShim(directory: string, name: string): string {
const shim = join(directory, name);
writeFileSync(shim, "#!/bin/sh\necho codex-cli 0.152.0\n");
chmodSync(shim, 0o755);
return shim;
}
function runTarget(params: {
target: { file: string; args: string[]; wrapped: boolean };
env: NodeJS.ProcessEnv;
}): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(params.target.file, params.target.args, {
env: params.env,
windowsVerbatimArguments: params.target.wrapped,
windowsHide: true,
});
let output = "";
child.stdout?.on("data", (chunk: Buffer) => {
output += chunk.toString("utf8");
});
child.on("error", reject);
child.on("close", () => resolve(output.trim()));
});
}
describe("resolveSpawnTarget", () => {
it.skipIf(isWindows)("keeps the bare name on POSIX, where spawn already resolves it", () => {
const target = resolveSpawnTarget({ command: "codex", args: ["login"], env: { PATH: "" } });
expect(target).toEqual({ file: "codex", args: ["login"], wrapped: false });
});
it.runIf(isWindows)("routes a .cmd shim through the command processor", () => {
const directory = shimDir();
const shim = writeCmdShim(directory, "codex");
const target = resolveSpawnTarget({
command: "codex",
args: ["login", "--device-auth"],
env: { PATH: directory, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
});
expect(target?.wrapped).toBe(true);
expect(target?.file.toLowerCase()).toContain("cmd.exe");
// absolute, not the bare name: System32 is on the ambient PATH, so a
// regression to `"cmd.exe"` would spawn fine here and hide itself.
expect(isAbsolute(target?.file ?? "")).toBe(true);
// the outer pair is what `cmd /s` strips; the inner one survives a space.
expect(target?.args.at(-1)).toBe(`""${shim}" login --device-auth"`);
});
it.runIf(isWindows)("spawns a real .exe directly", () => {
const directory = shimDir();
const exe = join(directory, "codex.exe");
writeFileSync(exe, "");
const target = resolveSpawnTarget({
command: "codex",
args: ["login"],
env: { PATH: directory, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
});
expect(target).toEqual({ file: exe, args: ["login"], wrapped: false });
});
it.runIf(isWindows)("prefers .exe over a .cmd shim in the same directory", () => {
const directory = shimDir();
writeCmdShim(directory, "codex");
const exe = join(directory, "codex.exe");
writeFileSync(exe, "");
const target = resolveSpawnTarget({
command: "codex",
args: [],
env: { PATH: directory, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
});
expect(target?.file).toBe(exe);
});
it.runIf(isWindows)("returns null when nothing on PATH matches", () => {
const target = resolveSpawnTarget({
command: "codex",
args: [],
env: { PATH: shimDir(), PATHEXT: ".COM;.EXE;.BAT;.CMD" },
});
expect(target).toBeNull();
});
});
describe("resolved target is actually spawnable", () => {
/** the regression itself: on Windows a bare `spawn("codex")` is ENOENT
* against a `.cmd` shim, so resolving first is what makes the launch work. */
it("runs a shim that a bare spawn cannot find", async () => {
const directory = shimDir();
const name = "pullfrog-codex-probe";
if (isWindows) writeCmdShim(directory, name);
else writePosixShim(directory, name);
// prepended, not replacing — that is where npm's global bin dir sits, and
// a narrowed PATH would hide `cmd.exe` from the wrapper branch.
const env = { ...process.env, PATH: `${directory}${delimiter}${process.env.PATH ?? ""}` };
const target = resolveSpawnTarget({ command: name, args: [], env });
expect(target).not.toBeNull();
if (!target) return;
expect(await runTarget({ target, env })).toBe("codex-cli 0.152.0");
});
/** review claim: the `cmd.exe` invocation breaks when the shim path contains
* a space, which every `C:\Users\First Last` profile does. `/s` strips the
* outer quote pair only when the string both starts and ends with one, so
* this asserts the real quoting rather than reasoning about it. */
it.runIf(isWindows)("runs a shim whose path contains spaces", async () => {
const directory = join(shimDir(), "dir with spaces");
mkdirSync(directory);
const name = "pullfrog codex probe";
writeCmdShim(directory, name);
const env = { ...process.env, PATH: `${directory}${delimiter}${process.env.PATH ?? ""}` };
const target = resolveSpawnTarget({ command: name, args: [], env });
expect(target?.wrapped).toBe(true);
if (!target) return;
expect(await runTarget({ target, env })).toBe("codex-cli 0.152.0");
});
/** review claim: Windows spells the variable `Path`, so reading `env.PATH`
* off a plain object misses it. `process.env` itself is case-insensitive on
* Windows; a spread copy of it is not. */
it.runIf(isWindows)("resolves against a Path-cased environment", () => {
const directory = shimDir();
writeCmdShim(directory, "codex");
const target = resolveSpawnTarget({
command: "codex",
args: [],
env: { Path: directory, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
});
expect(target).not.toBeNull();
});
it.runIf(isWindows)("proves the bare-name spawn this fix replaces fails", async () => {
const directory = shimDir();
const name = "pullfrog-codex-probe-bare";
writeCmdShim(directory, name);
const failure = await new Promise<NodeJS.ErrnoException>((resolve) => {
const child = spawn(name, [], { env: { ...process.env, PATH: directory } });
child.on("error", resolve);
});
expect(failure.code).toBe("ENOENT");
});
});