mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(edit-history): remote lookup finds tmux, a timeout reaches ssh, docs follow
Applies the review of #766. The live lookup ran tmux on the bare PATH of the ssh command. On a host where tmux sits in ~/.local/bin or /opt/homebrew/bin it found no pane, the 2>/dev/null hid that, and the panel stayed on the launch conversation after a /clear. It now goes through withRemoteTmuxPath(), inside the sh script and scoped to the $(...), so the other tools keep the PATH they had. The fake tmux of the test block moved to $HOME/.local/bin for the same reason: that directory is first on the new PATH, so a real tmux is never asked. The two ssh commands get exec. Node signals the local sh -c, and dash stays the parent of a plain command, so ssh survived the timeout. A test with a fake ssh shows the process dies now and survives without exec. The comment at the meta budget claimed a meta past it comes with a later read. A meta comes only with a subagent's first read, so it is never sent. Docs: the store is named as the exception to "never copied to local disk", remoteProbePaths and the remote transcript lookup are described, and the line saying a remote session loads no store is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cce28c24f9
commit
26b63aea2d
7 files changed
+90
-17
No files matched your search
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { exec, execFileSync } from 'node:child_process';
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
@@ -29,6 +29,7 @@ import { tmpdir } from 'node:os';
|
||||
import {
|
||||
RemoteFileAccessError,
|
||||
buildRemoteClaudeEditSourcesCommand,
|
||||
buildRemoteEditHistoryCommand,
|
||||
buildRemoteClaudeTranscriptLocateCommand,
|
||||
isRemoteClaudeTranscriptPath,
|
||||
parseRemoteClaudeEditSources,
|
||||
@@ -153,17 +154,21 @@ describe('the transcript lookup on a real shell', () => {
|
||||
|
||||
describe('after a /clear: the conversation the pane’s claude writes now', () => {
|
||||
// Fake tmux (pane pid 100) and ps (100 → 200 wrapper → 300 claude); claude's own
|
||||
// ~/.claude/sessions/<pid>.json names the current conversation.
|
||||
// ~/.claude/sessions/<pid>.json names the current conversation. tmux sits in the temp
|
||||
// $HOME/.local/bin, which withRemoteTmuxPath() puts ahead of the system dirs, so a real
|
||||
// tmux on this machine is never asked.
|
||||
let bin: string;
|
||||
const pane = { socket: 'codeman-remote', sessionName: 'codeman-ssh-conv1abc' };
|
||||
beforeAll(() => {
|
||||
bin = mkdtempSync(join(tmpdir(), 'codeman-fake-tmux-'));
|
||||
const localBin = join(home, '.local', 'bin');
|
||||
mkdirSync(localBin, { recursive: true });
|
||||
writeFileSync(
|
||||
join(bin, 'tmux'),
|
||||
join(localBin, 'tmux'),
|
||||
'#!/bin/sh\ncase "$*" in *"=codeman-ssh-conv1abc"*) echo 100 ;; *) echo "no such session" >&2; exit 1 ;; esac\n'
|
||||
);
|
||||
writeFileSync(join(bin, 'ps'), '#!/bin/sh\nprintf " 1 0\\n100 1\\n200 100\\n300 200\\n"\n');
|
||||
chmodSync(join(bin, 'tmux'), 0o755);
|
||||
chmodSync(join(localBin, 'tmux'), 0o755);
|
||||
chmodSync(join(bin, 'ps'), 0o755);
|
||||
mkdirSync(join(home, '.claude', 'sessions'), { recursive: true });
|
||||
});
|
||||
@@ -178,6 +183,15 @@ describe('the transcript lookup on a real shell', () => {
|
||||
expect(locateIn(pane)).toBe(join(projects(), 'old', 'solo.jsonl'));
|
||||
});
|
||||
|
||||
it('finds the pane with tmux only under $HOME/.local/bin, on a PATH that does not list it', () => {
|
||||
// A host where tmux is a per-user install: sshd's non-interactive PATH has no such dir.
|
||||
const PATH = `${bin}:/usr/bin:/bin`;
|
||||
expect(PATH).not.toContain('.local/bin');
|
||||
expect(existsSync(join(bin, 'tmux'))).toBe(false);
|
||||
session('{"pid":300,"sessionId":"solo"}');
|
||||
expect(locate(['conv-1'], pane, { PATH })).toBe(join(projects(), 'old', 'solo.jsonl'));
|
||||
});
|
||||
|
||||
it('falls back to the stored ids without a pane, for an unknown session, or a live id with no transcript', () => {
|
||||
session('{"pid":300,"sessionId":"solo"}');
|
||||
expect(locateIn(undefined)).toBe(join(projects(), 'new', 'conv-1.jsonl'));
|
||||
@@ -418,6 +432,43 @@ describe('the transcript read on a real shell', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a timeout ends ssh itself', () => {
|
||||
// Node signals the local `sh -c`. On dash that shell stays the parent of a plain command, so
|
||||
// only `exec` makes the signal reach ssh. The fake ssh records its pid and then just waits.
|
||||
it('kills the ssh process, not only the shell around it', async () => {
|
||||
const bin = mkdtempSync(join(tmpdir(), 'codeman-fake-ssh-'));
|
||||
const pidFile = join(bin, 'pid');
|
||||
try {
|
||||
writeFileSync(
|
||||
join(bin, 'ssh'),
|
||||
`#!/bin/sh\necho $$ > '${pidFile}.tmp'\nmv '${pidFile}.tmp' '${pidFile}'\nexec sleep 30\n`
|
||||
);
|
||||
chmodSync(join(bin, 'ssh'), 0o755);
|
||||
const command = buildRemoteEditHistoryCommand(REMOTE, buildRemoteClaudeTranscriptLocateCommand(['conv-1']));
|
||||
expect(command).toMatch(/^exec ssh /);
|
||||
await new Promise<void>((resolve) => {
|
||||
exec(command, { timeout: 1000, env: { ...process.env, PATH: `${bin}:${process.env.PATH}` } }, () => resolve());
|
||||
});
|
||||
const pid = Number(readFileSync(pidFile, 'utf8'));
|
||||
const alive = (): boolean => {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
try {
|
||||
await expect.poll(alive, { timeout: 3000 }).toBe(false);
|
||||
} finally {
|
||||
if (alive()) process.kill(pid, 'SIGKILL');
|
||||
}
|
||||
} finally {
|
||||
rmSync(bin, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('under vitest', () => {
|
||||
it('never opens a connection: the lookup and the read reject with a clear error', async () => {
|
||||
await expect(remoteLocateClaudeTranscript(REMOTE, ['a'])).rejects.toBeInstanceOf(RemoteFileAccessError);
|
||||
|
||||
Reference in new issue
Block a user