mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
fix(tui): sweep an attach status bar a killed terminal left behind
restore() runs after spawnSync returns, which covers detaching and the agent exiting inside the pane, but not the terminal dying while attached. Closing the window or dropping the SSH kills the TUI where it stands, and the bar it installed stays pinned on the session: the next attach wears a stale bar naming a different session, and the pane is a row shorter for good. Seen on the beta, where the tester closed the window instead of detaching. One sweep at startup, fire-and-forget so it can neither delay the first frame nor fail a start. Only a bar carrying our own marker is touched, and the marker is now the single source of the bar's own wording so the two cannot drift; a user's hand-written status bar on the same session is left exactly as it is. The session goes back to `status off`, which is how Codeman creates every pane it owns and the only state this bar is ever applied over.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
arrayOptionBase,
|
||||
parseSessionOptions,
|
||||
parseDetachKey,
|
||||
ATTACH_BANNER_MARKER,
|
||||
parseTmuxSessionList,
|
||||
parseWindowSizing,
|
||||
readCodemanCredentials,
|
||||
@@ -600,6 +601,61 @@ describe('attach window sizing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('TuiClient.clearLeakedAttachBanners', () => {
|
||||
const OURS = `#[align=left] press #[bold]Ctrl+B then d#[nobold] to detach, ${ATTACH_BANNER_MARKER} #[default]`;
|
||||
|
||||
function sweeper(formats: Record<string, string>): { client: TuiClient; calls: string[][] } {
|
||||
const calls: string[][] = [];
|
||||
const exec: TuiExecFile = async (_file, args) => {
|
||||
calls.push([...args]);
|
||||
if (args.includes('list-sessions')) return { stdout: Object.keys(formats).join('\n'), stderr: '' };
|
||||
if (args.includes('show-options')) {
|
||||
const name = args[args.indexOf('-t') + 1] ?? '';
|
||||
return { stdout: formats[name] ?? '', stderr: '' };
|
||||
}
|
||||
return { stdout: '', stderr: '' };
|
||||
};
|
||||
return { client: new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }), calls };
|
||||
}
|
||||
|
||||
it('takes down a bar a killed TUI left behind, and puts status back off', async () => {
|
||||
// The case that produced this: the tester closed the terminal window while
|
||||
// attached, so restore() never ran and the bar stayed pinned.
|
||||
const { client, calls } = sweeper({ 'codeman-aaaa1111': OURS });
|
||||
expect(await client.clearLeakedAttachBanners()).toBe(1);
|
||||
const sets = calls.filter((args) => args.includes('set-option'));
|
||||
// The array whole, never one index: unsetting `status-format[0]` alone
|
||||
// leaves an EMPTY array, which renders as a blank bar.
|
||||
expect(sets.some((args) => args.includes('-u') && args.includes('status-format'))).toBe(true);
|
||||
expect(sets.some((args) => args.includes('-u') && args.includes('status-style'))).toBe(true);
|
||||
expect(sets.some((args) => args.join(' ').endsWith('status off'))).toBe(true);
|
||||
expect(sets.every((args) => !args.includes('status-format[0]'))).toBe(true);
|
||||
});
|
||||
|
||||
it('never touches a status bar that is not ours', async () => {
|
||||
const { client, calls } = sweeper({ 'codeman-bbbb2222': '#[align=right] my own bar ' });
|
||||
expect(await client.clearLeakedAttachBanners()).toBe(0);
|
||||
expect(calls.filter((args) => args.includes('set-option'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('sweeps only the sessions that leaked, leaving the rest alone', async () => {
|
||||
const { client } = sweeper({
|
||||
'codeman-aaaa1111': OURS,
|
||||
'codeman-bbbb2222': '',
|
||||
'codeman-cccc3333': OURS,
|
||||
});
|
||||
expect(await client.clearLeakedAttachBanners()).toBe(2);
|
||||
});
|
||||
|
||||
it('reports nothing rather than throwing when there is no tmux server', async () => {
|
||||
const exec: TuiExecFile = async () => {
|
||||
throw new Error('no server running on /tmp/tmux-1000/codeman-beta');
|
||||
};
|
||||
const client = new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec });
|
||||
await expect(client.clearLeakedAttachBanners()).resolves.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDetachKey', () => {
|
||||
// Verbatim from `tmux -L codeman list-keys -T prefix` on tmux 3.4, trimmed to
|
||||
// the two lines that matter. They differ only by case, which is the whole
|
||||
|
||||
Reference in New Issue
Block a user