Files
diegosouzapw--omniroute/tests/unit/qodercli-windows-resolve-6263.test.ts
T
2026-07-13 13:39:12 +08:00

126 lines
4.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
// #6263 — On Windows, Qoder PAT auth failed with `spawn qodercli ENOENT` even
// though `qodercli.cmd` was installed under `%APPDATA%\npm` and worked from a
// shell. Root cause: `spawnQoderCli` spawned the bare `"qodercli"` name with
// `shell:false` and an unenriched env, so the npm `.cmd` wrapper on the user PATH
// was never resolved. The fix routes command resolution through the already
// Windows-aware `src/shared/services/cliRuntime.ts` (which enumerates
// `qodercli.cmd`/`.exe` under npm-global + `%APPDATA%\npm`, resolves an absolute
// `commandPath`, and flags `.cmd`/`.bat` as needing a shell).
//
// This suite exercises the *pure* resolution logic with mocks; the end-to-end
// spawn of a real `qodercli.cmd` can only be validated on a real Windows host
// (fs realpath/stat security checks + the frozen expected-parent list).
const qoderResolve = await import("../../open-sse/services/qoderCliResolve.ts");
const cliRuntime = await import("../../src/shared/services/cliRuntime.ts");
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
const originalAppData = process.env.APPDATA;
const originalQoderBin = process.env.CLI_QODER_BIN;
function setPlatform(value: string) {
Object.defineProperty(process, "platform", { configurable: true, value });
}
test.afterEach(() => {
if (originalPlatformDescriptor) {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
if (originalAppData === undefined) delete process.env.APPDATA;
else process.env.APPDATA = originalAppData;
if (originalQoderBin === undefined) delete process.env.CLI_QODER_BIN;
else process.env.CLI_QODER_BIN = originalQoderBin;
qoderResolve.__clearQoderCliInvocationCache();
});
test("cliRuntime enumerates qodercli.cmd under %APPDATA%\\npm on Windows", () => {
setPlatform("win32");
// APPDATA must live inside the home dir to pass cliRuntime's env-path validation.
const appData = path.join(os.homedir(), "AppData", "Roaming");
process.env.APPDATA = appData;
const candidates = cliRuntime.getKnownToolPaths("qoder");
const expected = path.join(appData, "npm", "qodercli.cmd");
assert.ok(
candidates.includes(expected),
`expected getKnownToolPaths("qoder") to include ${expected}, got: ${candidates.join(", ")}`
);
});
test("shouldUseShellForCommand: true for a .cmd wrapper, false for the bare name (Windows)", () => {
setPlatform("win32");
const cmdPath = path.join(os.homedir(), "AppData", "Roaming", "npm", "qodercli.cmd");
assert.equal(cliRuntime.shouldUseShellForCommand(cmdPath), true);
assert.equal(cliRuntime.shouldUseShellForCommand("qodercli"), false);
});
test("resolveQoderCliInvocation picks the absolute .cmd path + shell when cliRuntime finds it", async () => {
setPlatform("win32");
const cmdPath = path.join(os.homedir(), "AppData", "Roaming", "npm", "qodercli.cmd");
const invocation = await qoderResolve.resolveQoderCliInvocation(null, {
getStatus: async () => ({
installed: true,
runnable: true,
command: "qodercli",
commandPath: cmdPath,
reason: null,
runtimeMode: "auto",
requiresBinary: true,
}),
});
// The bug was spawning the bare "qodercli"; the fix must select the resolved
// absolute .cmd path and mark it as needing a shell (cmd.exe).
assert.equal(invocation.command, cmdPath);
assert.equal(invocation.useShell, true);
assert.notEqual(invocation.command, "qodercli");
});
test("resolveQoderCliInvocation falls back to the bare command when cliRuntime finds nothing", async () => {
setPlatform("win32");
delete process.env.CLI_QODER_BIN;
const invocation = await qoderResolve.resolveQoderCliInvocation(null, {
getStatus: async () => ({
installed: false,
runnable: false,
command: "qodercli",
commandPath: null,
reason: "not_found",
runtimeMode: "auto",
requiresBinary: true,
}),
});
assert.equal(invocation.command, "qodercli");
// A bare name is not a .cmd/.bat, so no shell is requested.
assert.equal(invocation.useShell, false);
});
test("resolveQoderCliInvocation is inert on non-Windows (no shell, resolved path honored)", async () => {
setPlatform("linux");
const posixPath = "/usr/local/bin/qodercli";
const invocation = await qoderResolve.resolveQoderCliInvocation(null, {
getStatus: async () => ({
installed: true,
runnable: true,
command: "qodercli",
commandPath: posixPath,
reason: null,
runtimeMode: "auto",
requiresBinary: true,
}),
});
assert.equal(invocation.command, posixPath);
assert.equal(invocation.useShell, false);
});