fix: preserve bash cwd without pwd probe

This commit is contained in:
Jeff Scott Ward
2026-06-30 22:16:27 -04:00
parent dff941d5fe
commit 3da25618ea
7 changed files with 137 additions and 54 deletions
@@ -32,6 +32,22 @@ function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}
function configureBashUserShell(homeDir: string): boolean {
if (process.platform === "win32" || !fs.existsSync("/bin/bash")) return false;
Settings.instance.set("shellPath", "/bin/bash");
vi.spyOn(Settings.prototype, "getShellConfig").mockReturnValue({
shell: "/bin/bash",
args: ["-c"],
env: {
PATH: Bun.env.PATH ?? "",
HOME: homeDir,
SHELL: "/bin/bash",
},
prefix: undefined,
});
return true;
}
/** Resolve once `predicate()` holds or `deadlineMs` passes, polling every 2ms. */
async function pollUntil(predicate: () => boolean, deadlineMs: number): Promise<void> {
while (!predicate() && Date.now() < deadlineMs) {
@@ -119,19 +135,18 @@ describe("executeBash", () => {
it("honors cwd", async () => {
const result = await executeBash("pwd", { cwd: tempDir, timeout: 5000 });
expect(result.output.trim()).toBe(fs.realpathSync(tempDir));
expect(result.output.trim()).toBe(tempDir);
});
it("syncs persistent shell directory changes back to the session without clobbering status", async () => {
it("returns and syncs persistent shell directory changes back to the session", async () => {
if (!configureBashUserShell(tempDir)) return;
const childDir = path.join(tempDir, "child");
fs.mkdirSync(childDir);
const realChildDir = fs.realpathSync(childDir);
const sessionKey = `cwd-sync-${Date.now()}`;
const result = await executeBash(`cd ${shellQuote(childDir)}; false`, {
sessionKey,
timeout: 5000,
useUserShell: true,
});
expect(result.exitCode).toBe(1);
const result = await executeBash(`cd ${shellQuote(childDir)}`, { sessionKey, timeout: 5000, useUserShell: true });
expect(result.workingDir ? fs.realpathSync(result.workingDir) : undefined).toBe(realChildDir);
const applied: string[] = [];
const synced = await syncBashSessionCwd({
@@ -142,24 +157,79 @@ describe("executeBash", () => {
},
});
expect(synced).toBe(childDir);
expect(applied).toEqual([childDir]);
const status = await executeBash("echo $?", { sessionKey, timeout: 5000, useUserShell: true });
expect(synced ? fs.realpathSync(synced) : undefined).toBe(realChildDir);
expect(applied.map(cwd => fs.realpathSync(cwd))).toEqual([realChildDir]);
});
it("does not clobber the persistent shell status while syncing cwd", async () => {
if (!configureBashUserShell(tempDir)) return;
const childDir = path.join(tempDir, "child-status");
fs.mkdirSync(childDir);
const sessionKey = `cwd-status-${Date.now()}`;
const result = await executeBash(`cd ${shellQuote(childDir)}; false`, {
sessionKey,
cwd: tempDir,
timeout: 5000,
useUserShell: true,
});
expect(result.exitCode).toBe(1);
await syncBashSessionCwd({
result,
currentCwd: tempDir,
applyCwd: async () => {},
});
const status = await executeBash(`printf '%s\n' "$?"`, { sessionKey, timeout: 5000, useUserShell: true });
expect(status.output.trim()).toBe("1");
});
it("canonicalizes symlinked cwd before execution", async () => {
it("does not reset OLDPWD when synchronized cwd is passed to the next command", async () => {
if (!configureBashUserShell(tempDir)) return;
const childDir = path.join(tempDir, "child-oldpwd");
fs.mkdirSync(childDir);
const realTempDir = fs.realpathSync(tempDir);
const realChildDir = fs.realpathSync(childDir);
const sessionKey = `cwd-oldpwd-${Date.now()}`;
const result = await executeBash(`cd ${shellQuote(childDir)}`, {
sessionKey,
cwd: tempDir,
timeout: 5000,
useUserShell: true,
});
const synced = await syncBashSessionCwd({
result,
currentCwd: tempDir,
applyCwd: async () => {},
});
expect(synced ? fs.realpathSync(synced) : undefined).toBe(realChildDir);
const back = await executeBash("cd - >/dev/null; pwd", {
sessionKey,
cwd: synced ?? childDir,
timeout: 5000,
useUserShell: true,
});
expect(back.output.trim() ? fs.realpathSync(back.output.trim()) : undefined).toBe(realTempDir);
});
it("honors symlinked cwd requests in persistent shells", async () => {
if (process.platform === "win32") {
return;
}
if (!configureBashUserShell(tempDir)) return;
const realDir = path.join(tempDir, "real");
const linkDir = path.join(tempDir, "link");
fs.mkdirSync(realDir);
fs.symlinkSync(realDir, linkDir, "dir");
const sessionKey = `cwd-symlink-${Date.now()}`;
const result = await executeBash("pwd", { cwd: linkDir, timeout: 5000 });
expect(result.output.trim()).toBe(fs.realpathSync(linkDir));
await executeBash("pwd", { sessionKey, cwd: realDir, timeout: 5000, useUserShell: true });
const result = await executeBash("pwd", { sessionKey, cwd: linkDir, timeout: 5000, useUserShell: true });
expect(result.output.trim()).toBe(linkDir);
expect(result.workingDir).toBe(linkDir);
});
it("passes env vars", async () => {