fix(coding-agent): run fish user shell interactively instead of as a login shell
The interactive !/!! shortcut wrapped commands as `fish -l -c '…'`: resolveUserShellConfig swaps in $SHELL but inherits the bash-oriented ["-l", "-c"] args, and ensureInteractiveShellArgs injected -i only for zsh. A login fish fires `status is-login` blocks in user config (agent/keychain setup, PATH mutation) on every command. fish sources the same config.fish/conf.d files for interactive shells as for login shells, so give fish -i and strip the inherited -l: user aliases and functions (#1816) keep working without login-shell side effects. zsh keeps -l -i since .zprofile is login-only.
This commit is contained in:
@@ -276,8 +276,10 @@ exit 64
|
||||
expect(result.cancelled).toBe(false);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output.trim()).toBe("env-shell-ok");
|
||||
expect(fs.readFileSync(marker, "utf8")).toContain("-l -c");
|
||||
expect(fs.readFileSync(marker, "utf8")).not.toContain("-i");
|
||||
// fish gets `-i` (interactive loads config.fish too) instead of `-l`,
|
||||
// so `status is-login` blocks in user config don't fire on `!` commands.
|
||||
expect(fs.readFileSync(marker, "utf8")).toContain("-i -c");
|
||||
expect(fs.readFileSync(marker, "utf8")).not.toContain("-l");
|
||||
} finally {
|
||||
if (originalShell === undefined) {
|
||||
delete Bun.env.SHELL;
|
||||
@@ -330,6 +332,58 @@ exit 64
|
||||
}
|
||||
});
|
||||
|
||||
it("runs fish user-shell commands without login-shell side effects", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const fishPath = ["/usr/bin/fish", "/bin/fish", "/usr/local/bin/fish", "/opt/homebrew/bin/fish"].find(
|
||||
candidate => fs.existsSync(candidate),
|
||||
);
|
||||
if (!fishPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shellDir = fs.mkdtempSync(path.join(os.tmpdir(), "omp-fish-shellpath-"));
|
||||
const configDir = path.join(shellDir, ".config", "fish");
|
||||
fs.mkdirSync(path.join(configDir, "conf.d"), { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, "config.fish"), "function pi_fish_fn; echo fish-fn-ok; end\n");
|
||||
// Login-gated snippet: fires only when the spawned fish is a login shell.
|
||||
fs.writeFileSync(
|
||||
path.join(configDir, "conf.d", "pi-login.fish"),
|
||||
"if status is-login; echo fish-login-side-effect; end\n",
|
||||
);
|
||||
Settings.instance.set("shellPath", fishPath);
|
||||
|
||||
vi.spyOn(Settings.prototype, "getShellConfig").mockReturnValue({
|
||||
shell: fishPath,
|
||||
args: ["-l", "-c"],
|
||||
env: {
|
||||
PATH: Bun.env.PATH ?? "",
|
||||
HOME: shellDir,
|
||||
},
|
||||
prefix: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executeBash("pi_fish_fn", {
|
||||
cwd: tempDir,
|
||||
timeout: 5000,
|
||||
sessionKey: "fish-shell-path",
|
||||
useUserShell: true,
|
||||
});
|
||||
|
||||
expect(result.cancelled).toBe(false);
|
||||
expect(result.exitCode).toBe(0);
|
||||
// config.fish must still load (#1816 contract)…
|
||||
expect(result.output).toContain("fish-fn-ok");
|
||||
// …but the shell must not be a login shell.
|
||||
expect(result.output).not.toContain("fish-login-side-effect");
|
||||
} finally {
|
||||
removeSyncWithRetries(shellDir);
|
||||
}
|
||||
});
|
||||
|
||||
it("invokes onChunk with command output", async () => {
|
||||
let seenChunk: string | null = null;
|
||||
const result = await executeBash("echo hello", {
|
||||
|
||||
Reference in New Issue
Block a user