mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
fix(remote): hand remote/docker launch commands to bash verbatim
respawn-pane runs `bash -c "<ssh …>"` through the local sh, so every `$` in
the remote command is expanded on the Codeman host. withRemoteTmuxPath
(`export PATH="$HOME/.local/bin:…:$PATH"; tmux …`, 5b9445de) therefore sends
the Codeman host's home and PATH to the remote host: a remote user with a
different home (tmux in ~/.nix-profile/bin) gets "tmux: command not found" and
every new remote pane exits 127. `${SHELL:-/bin/sh}` in the remote launch was
the local shell for the same reason.
Remote and docker commands are already quoted for the far end, so they now go
to `bash -c` single-quoted (bashCArg); local commands keep the double-quoted
form. Two tests run the captured respawn-pane line (create and respawn)
through a real sh with a foreign HOME/PATH and check that ssh gets `$HOME` and
`$PATH` unexpanded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2818e9455489b9acd65841258df4cd035609954c)
This commit is contained in:
1 parent
46b85fa6d0
commit
21256202ba
2 files changed
+90
-2
No files matched your search
@@ -28,6 +28,20 @@ import type { MuxSession } from '../src/mux-interface.js';
|
||||
// ============================================================================
|
||||
|
||||
// Mock child_process
|
||||
/**
|
||||
* Runs a captured `… respawn-pane … bash -c <arg>` line through a real local sh (with a
|
||||
* foreign HOME/PATH) and returns the string bash would get, i.e. what ssh sends on.
|
||||
*/
|
||||
async function bashArgAfterLocalShell(respawnCmd: string): Promise<string> {
|
||||
const real = await vi.importActual<typeof import('node:child_process')>('node:child_process');
|
||||
const at = respawnCmd.indexOf(' bash -c ');
|
||||
expect(at).toBeGreaterThan(0);
|
||||
return real.execFileSync('/bin/sh', ['-c', `printf '%s' ${respawnCmd.slice(at + ' bash -c '.length)}`], {
|
||||
env: { HOME: '/local-home-xyz', PATH: '/local-path-xyz:/usr/bin:/bin' },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock('node:child_process', async () => {
|
||||
const actual = await vi.importActual('node:child_process');
|
||||
return {
|
||||
@@ -865,6 +879,66 @@ describe('TmuxManager (unit)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('hands a remote launch to ssh with $HOME and $PATH unexpanded', async () => {
|
||||
// respawn-pane runs `bash -c <arg>` through the local sh. In double quotes that sh
|
||||
// expanded withRemoteTmuxPath's `$HOME/…:$PATH` to the Codeman host's values, so a
|
||||
// remote user with another home found no tmux and the pane exited 127.
|
||||
const NonTestTmuxManager = await importWithTmuxCommandsEnabled();
|
||||
const nonTestManager = new NonTestTmuxManager();
|
||||
|
||||
try {
|
||||
await nonTestManager.createSession({
|
||||
sessionId: 'abc12345-1234-5678-90ab-cdef12345679',
|
||||
workingDir: '/w',
|
||||
mode: 'shell',
|
||||
remote: { hostId: 'gpu-box', label: 'GPU Box', host: '10.0.0.42', username: 'ubuntu', remotePath: '/w' },
|
||||
});
|
||||
const launch = [...mockedExecSync.mock.calls, ...vi.mocked(exec).mock.calls]
|
||||
.map(([cmd]) => cmd)
|
||||
.find((cmd) => typeof cmd === 'string' && cmd.includes(' respawn-pane '));
|
||||
const sent = await bashArgAfterLocalShell(launch as string);
|
||||
expect(sent).toContain('ssh ');
|
||||
expect(sent).toContain('export PATH="$HOME/');
|
||||
expect(sent).toContain(':$PATH"; tmux ');
|
||||
expect(sent).not.toContain('local-home-xyz');
|
||||
expect(sent).not.toContain('local-path-xyz');
|
||||
} finally {
|
||||
nonTestManager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('respawns a remote pane with $HOME unexpanded as well', async () => {
|
||||
const NonTestTmuxManager = await importWithTmuxCommandsEnabled();
|
||||
const nonTestManager = new NonTestTmuxManager();
|
||||
nonTestManager.registerSession({
|
||||
sessionId: 'respawnremote2',
|
||||
muxName: 'codeman-abcd8888',
|
||||
pid: 1000,
|
||||
createdAt: Date.now(),
|
||||
workingDir: '/w',
|
||||
mode: 'shell',
|
||||
attached: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await nonTestManager.respawnPane({
|
||||
sessionId: 'respawnremote2',
|
||||
workingDir: '/w',
|
||||
mode: 'shell',
|
||||
remote: { hostId: 'gpu-box', label: 'GPU Box', host: '10.0.0.42', username: 'ubuntu', remotePath: '/w' },
|
||||
});
|
||||
const { exec: currentExec } = await import('node:child_process');
|
||||
const respawnCall = vi
|
||||
.mocked(currentExec)
|
||||
.mock.calls.find(([cmd]) => typeof cmd === 'string' && cmd.includes(' respawn-pane '));
|
||||
const sent = await bashArgAfterLocalShell(respawnCall?.[0] as string);
|
||||
expect(sent).toContain('export PATH="$HOME/');
|
||||
expect(sent).not.toContain('local-home-xyz');
|
||||
} finally {
|
||||
nonTestManager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('changes the global history default on tmux versions that cannot resize panes', async () => {
|
||||
const NonTestTmuxManager = await importWithTmuxCommandsEnabled();
|
||||
const nonTestManager = new NonTestTmuxManager();
|
||||
|
||||
Reference in new issue
Block a user