mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
The COD-108 reconnect watcher treated any dead local pane as a dropped transport and re-ran the pane command — so a normal ctrl-c/ctrl-d on a remote omp/opencode/claude auto-spawned a FRESH agent (claude only looked correct because its '--session-id || --resume' fallback resumed, with a loud 'already in use' error first). Distinguish a transport drop from an intentional exit: only reconnect when the durable remote tmux session (codeman-ssh-*) is verifiably still alive on the remote host. A clean exit tears that session down; the watcher now probes it via ssh has-session and skips (remote-gone) when it is gone OR unknown (fail closed). The probe is cached per-session and fired async so the 5s tick never blocks on ssh. Also thread ompConfig/resumeSessionId into the remote builders so a dead-pane respawn of an omp session resumes (--resume <id>) or continues (--continue) instead of launching bare omp. Tests: 3 new cases pinning remote-gone / unknown / alive decisions; remote omp resume + --continue fallback. Verified live: all three remote CLIs stay dead after exit.
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { buildRemoteLaunchCommand } from '../src/tmux-manager.js';
|
|
import { parseRemoteSessionList } from '../src/remote-hosts.js';
|
|
import type { SessionRemote } from '../src/types.js';
|
|
|
|
// COD-106 — shared/collaborative remote sessions: window-size policy so concurrent
|
|
// clients don't fight, and a client-count surfaced for the "shared · N" badge.
|
|
describe('COD-106 shared remote sessions', () => {
|
|
const remote: SessionRemote = {
|
|
hostId: 'h',
|
|
label: 'aa',
|
|
host: '192.168.55.170',
|
|
username: 'aakht',
|
|
remotePath: '/tmp',
|
|
commands: { shell: 'exec bash -l' },
|
|
};
|
|
|
|
it('launch command sets window-size latest (so multi-client attach does not clamp to smallest)', () => {
|
|
const cmd = buildRemoteLaunchCommand({ mode: 'shell', remote, sessionId: 'cod106aaa' });
|
|
// Per-session scoped on the dedicated `codeman-remote` socket (PR #145 hardening).
|
|
expect(cmd).toContain('set -t codeman-ssh-cod106aa window-size latest');
|
|
// still has the COD-104 config (no regression)
|
|
expect(cmd).toContain('set -t codeman-ssh-cod106aa status off');
|
|
expect(cmd).toContain('new-session -A -s codeman-ssh-cod106aa');
|
|
});
|
|
|
|
it('remote omp relaunch resumes the pinned conversation instead of starting fresh (2026-08-29)', () => {
|
|
const cmd = buildRemoteLaunchCommand({
|
|
mode: 'omp',
|
|
remote,
|
|
sessionId: 'cod106aaa',
|
|
ompConfig: { model: 'llm-proxy/crof/glm-5.3-flash' },
|
|
resumeSessionId: '01a04eb1-d883-75f0-bdfa-74cc315b09ce',
|
|
});
|
|
// The remote pane command must carry the pinned omp session id so a
|
|
// dead-pane respawn lands back in the same conversation.
|
|
expect(cmd).toContain('omp --model llm-proxy/crof/glm-5.3-flash --resume 01a04eb1-d883-75f0-bdfa-74cc315b09ce');
|
|
// still a durable, idempotent remote tmux session
|
|
expect(cmd).toContain('new-session -A -s codeman-ssh-cod106aa');
|
|
});
|
|
|
|
it('remote omp relaunch falls back to --continue when no id is pinned', () => {
|
|
const cmd = buildRemoteLaunchCommand({
|
|
mode: 'omp',
|
|
remote,
|
|
sessionId: 'cod106aaa',
|
|
ompConfig: { continueSession: true },
|
|
});
|
|
expect(cmd).toContain('omp --continue');
|
|
});
|
|
|
|
it('parses session_attached as a CLIENT COUNT (>1 = shared)', () => {
|
|
const rows = parseRemoteSessionList(
|
|
['codeman-solo\\t1\\t100\\t1', 'codeman-shared\\t2\\t200\\t3', 'codeman-idle\\t0\\t300\\t1'].join('\n')
|
|
);
|
|
const byName = Object.fromEntries(rows.map((r) => [r.name, r]));
|
|
expect(byName['codeman-solo'].attachedClients).toBe(1);
|
|
expect(byName['codeman-solo'].attached).toBe(true);
|
|
expect(byName['codeman-shared'].attachedClients).toBe(2); // shared
|
|
expect(byName['codeman-shared'].attached).toBe(true);
|
|
expect(byName['codeman-idle'].attachedClients).toBe(0);
|
|
expect(byName['codeman-idle'].attached).toBe(false);
|
|
});
|
|
});
|