Files
paseo/packages/server/src/utils/string-command-shell.test.ts
T
Mohamed BoudraandGitHub 08a0d0c28d Fix worktree setup scripts losing PATH (#1908)
* fix(worktree): preserve PATH for lifecycle scripts

Lifecycle command strings were running through login shells, which could rewrite PATH before setup commands saw the daemon environment. Use the shared stable script-shell helper without login startup files, and route related project-authored command strings through it.

* fix(worktree): ignore Bash startup env hook

Project command strings should not source shell startup hooks before they see Paseo's supplied environment. Strip BASH_ENV from lifecycle, loop verify, and ACP string-command shells and pin the behavior in setup tests.

* fix(worktree): resolve Bash through PATH

* fix(worktree): preserve Windows cmd command strings

* test(acp): expect Windows cmd terminal strings
2026-07-06 20:34:42 +08:00

105 lines
3.0 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
buildStringCommandShellInvocation,
createStringCommandShellEnv,
} from "./string-command-shell.js";
function hasBashOnPath(): boolean {
const result = spawnSync("bash", ["-c", "true"], { stdio: "ignore" });
return !result.error && result.status === 0;
}
describe("buildStringCommandShellInvocation", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const tempDir of tempDirs) {
rmSync(tempDir, { recursive: true, force: true });
}
tempDirs.length = 0;
});
it("uses bash script semantics on unix platforms", () => {
expect(
buildStringCommandShellInvocation({
command: 'echo "hello"',
platform: "darwin",
}),
).toEqual({
shell: "bash",
args: ["-c", 'echo "hello"'],
});
});
it.skipIf(process.platform === "win32" || !hasBashOnPath())(
"preserves the supplied PATH when login profiles rewrite it",
() => {
const home = mkdtempSync(join(tmpdir(), "paseo-shell-home-"));
tempDirs.push(home);
const binDir = join(home, "bin");
mkdirSync(binDir);
const shimPath = join(binDir, "paseo-shim");
writeFileSync(shimPath, "#!/bin/sh\nprintf 'shim:%s\\n' \"$1\"\n");
chmodSync(shimPath, 0o755);
writeFileSync(join(home, ".bash_profile"), "export PATH=/usr/bin:/bin\n");
const bashEnvPath = join(home, "bash-env");
writeFileSync(bashEnvPath, "export PATH=/usr/bin:/bin\n");
const invocation = buildStringCommandShellInvocation({
command: "command -v paseo-shim >/dev/null && paseo-shim ok",
});
const env: NodeJS.ProcessEnv = {
...process.env,
HOME: home,
PATH: `${binDir}${delimiter}${process.env.PATH ?? "/usr/bin:/bin"}`,
BASH_ENV: bashEnvPath,
};
const stdout = execFileSync(invocation.shell, invocation.args, {
encoding: "utf8",
env: createStringCommandShellEnv(env),
});
expect(stdout.trim()).toBe("shim:ok");
},
);
it("uses powershell command semantics on windows by default", () => {
expect(
buildStringCommandShellInvocation({
command: "Write-Output 'hello'",
platform: "win32",
}),
).toEqual({
shell: "powershell",
args: [
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
"Write-Output 'hello'",
],
});
});
it("can preserve cmd command semantics on windows", () => {
expect(
buildStringCommandShellInvocation({
command: "echo %TEMP% && echo ok",
platform: "win32",
windowsShell: "cmd",
}),
).toEqual({
shell: "cmd.exe",
args: ["/c", "echo %TEMP% && echo ok"],
});
});
});