From 897bfdff59629771c751daf29360547311c064ca Mon Sep 17 00:00:00 2001 From: Aamer Akhter Date: Sat, 13 Jun 2026 13:56:14 -0400 Subject: [PATCH] COD-109 terminate owned durable remote tmux sessions (propagate kill to remote) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since COD-104 a remote session lives in a durable tmux server on the host and outlives the local pane, so killing a tab only DETACHED — even for sessions we own. Propagate `kill-session` to the remote for OWNED sessions in killSession's owned path (after COD-105's non-owned detach-only early-return); non-owned detach-only is untouched. Reconciled onto upstream PR #145: #145 already upstreamed this exact owned-kill propagation as `buildRemoteKillCommand({ remote, sessionId })` on the dedicated `-L codeman-remote` socket (matching buildRemoteLaunchCommand) and wired it into killSession (Strategy 3b, owned-only, fire-and-forget). The original COD-109 commit added a second `buildRemoteKillCommand(remote, name)` overload on the old `-L codeman` socket plus a duplicate kill block — a compile error AND a wrong socket post-#145 (owned sessions no longer live on `codeman`). This commit keeps #145's socket-correct implementation and drops the duplicate; the required test/remote-kill-command.test.ts is retargeted to #145's `{ remote, sessionId }` signature and the `codeman-remote` socket. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/remote-kill-command.test.ts | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/remote-kill-command.test.ts diff --git a/test/remote-kill-command.test.ts b/test/remote-kill-command.test.ts new file mode 100644 index 00000000..b4b14db9 --- /dev/null +++ b/test/remote-kill-command.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { buildRemoteKillCommand } from '../src/tmux-manager.js'; +import type { SessionRemote } from '../src/types.js'; + +// COD-109 — terminate an OWNED durable remote tmux session by propagating +// `kill-session` to the remote host. Since COD-104 an owned remote session lives +// in the dedicated `-L codeman-remote` tmux server on the host and outlives the +// local ssh pane, so ending a session we OWN must reach the remote socket. +// +// The owned-kill command builder was consolidated upstream (PR #145) into the +// `{ remote, sessionId }` form, which derives the durable session name and kills +// on the dedicated `codeman-remote` socket (matching buildRemoteLaunchCommand). +// This suite pins that builder's contract; the killSession integration +// (owned-only, after COD-105's non-owned detach-only early-return) is exercised +// against the real remote. +describe('COD-109 buildRemoteKillCommand (owned durable remote kill)', () => { + const base: SessionRemote = { + hostId: 'h', + label: 'aa', + host: '192.168.55.170', + username: 'aakht', + remotePath: '/tmp', + }; + + it('kills the durable session on the dedicated codeman-remote socket (no ssh -t — non-interactive)', () => { + const cmd = buildRemoteKillCommand({ remote: base, sessionId: 'abc12345def' }); + expect(cmd.startsWith('ssh -o BatchMode=yes ')).toBe(true); + expect(cmd).toContain('aakht@192.168.55.170'); + // Owned sessions launch on `-L codeman-remote`; the kill MUST target the same socket. + expect(cmd).toContain('tmux -L codeman-remote kill-session -t'); + // Deterministic session name derived from the sessionId (codeman-ssh-). + expect(cmd).toContain('codeman-ssh-abc12345'); + // kill-session needs no PTY — must NOT request the ssh `-t` flag (attach uses + // `ssh -o BatchMode=yes -t …`; kill must not). The ` -t ` inside the quoted + // `kill-session -t ` is the tmux target flag, which is expected. + expect(cmd).not.toContain('-o BatchMode=yes -t'); + }); + + it('shares the default ConnectTimeout so an unreachable host fails fast (never blocks kill)', () => { + const cmd = buildRemoteKillCommand({ remote: base, sessionId: 'abc12345def' }); + expect(cmd).toContain('-o ConnectTimeout=10'); + }); + + it('reuses the COD-107 connection options (port / identity / SOCKS proxy)', () => { + const cmd = buildRemoteKillCommand({ + remote: { ...base, port: 2222, identityFile: '~/.ssh/remote_ed25519', socksProxy: '127.0.0.1:1080' }, + sessionId: 'abc12345def', + }); + expect(cmd).toContain('-p 2222'); + expect(cmd).toMatch(/-i '.*\/\.ssh\/remote_ed25519'/); + expect(cmd).toContain("-o 'ProxyCommand=nc -X 5 -x 127.0.0.1:1080 %h %p'"); + }); + + it('shell-escapes the derived session name so metachars stay one token', () => { + const cmd = buildRemoteKillCommand({ remote: base, sessionId: "x'; rm -rf /" }); + expect(cmd).not.toMatch(/rm -rf \/\s*$/); // not a bare trailing command + expect(cmd).toContain('kill-session -t'); + }); +});