126 lines
4.7 KiB
TypeScript
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);
|
|
});
|