Files
clide/test/builtin/claude/tmux_session_test.dart
T
jpmschweitzerandClaude 6601056972 finish T-87: cold-start reap, kill-all-sessions, helper tests
Three remaining acceptance criteria for T-87:

1. Cold-start reap. The Claude extension's activate() now kills
   every leftover secondary tmux session for the current repo
   before any new spawn. activate runs before any UI mounts, so
   _nextSecondary's starting value of 1 is correct even when a
   previous run died abruptly (kill -9, OOM, force-quit). The
   deactivate() hook also calls reapSecondaries as a courtesy on
   explicit extension teardown — but Flutter's deactivate doesn't
   fire on app quit, so activate is the load-bearing path.

2. claude.kill-all-sessions actually kills server-side. The
   command previously called pane.close on every claude pane,
   which only kills the tmux client. It now also calls
   tmux.killAllForRepo to kill the sessions on the clide socket.

3. Tests. test/builtin/claude/tmux_session_test.dart covers
   killSession, listClideSessions, reapSecondaries, and
   killAllForRepo via the TmuxRunner override — no real shell-out
   in tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 16:48:06 +02:00

107 lines
3.5 KiB
Dart

import 'dart:io';
import 'package:clide/builtin/claude/src/tmux_session.dart' as tmux;
import 'package:test/test.dart';
class _RecordingRunner {
final List<List<String>> calls = [];
String stdout = '';
int exitCode = 0;
Future<ProcessResult> call(List<String> args) async {
calls.add(List.of(args));
return ProcessResult(0, exitCode, stdout, '');
}
}
void main() {
late _RecordingRunner runner;
setUp(() {
runner = _RecordingRunner();
tmux.tmuxRunner = runner.call;
});
tearDown(() {
// Restore the default runner so other tests aren't affected.
tmux.tmuxRunner = (args) => Process.run('tmux', args);
});
group('killSession', () {
test('invokes tmux kill-session on the clide socket', () async {
await tmux.killSession('clide-claude-foo');
expect(runner.calls, [
['-L', 'clide', 'kill-session', '-t', 'clide-claude-foo'],
]);
});
test('does not surface non-zero exit (session already gone)', () async {
runner.exitCode = 1;
await tmux.killSession('clide-claude-foo');
expect(runner.calls, hasLength(1));
});
});
group('listClideSessions', () {
test('parses session names from tmux output', () async {
runner.stdout = 'clide-claude-foo\nclide-claude-foo-1\nclide-claude-foo-2\n';
final names = await tmux.listClideSessions();
expect(names, ['clide-claude-foo', 'clide-claude-foo-1', 'clide-claude-foo-2']);
});
test('returns empty list when server is not running', () async {
runner.exitCode = 1;
final names = await tmux.listClideSessions();
expect(names, isEmpty);
});
test('strips blank lines and whitespace', () async {
runner.stdout = '\nclide-claude-foo\n\n clide-claude-foo-1 \n';
final names = await tmux.listClideSessions();
expect(names, ['clide-claude-foo', 'clide-claude-foo-1']);
});
});
group('reapSecondaries', () {
test('kills only -<digits>-suffixed sessions, leaves primary alive', () async {
runner.stdout = 'clide-claude-foo\nclide-claude-foo-1\nclide-claude-foo-2\n';
await tmux.reapSecondaries('clide-claude-foo');
// First call lists, then one kill per secondary.
expect(runner.calls.first, ['-L', 'clide', 'list-sessions', '-F', '#{session_name}']);
final kills = runner.calls.skip(1).toList();
expect(kills, [
['-L', 'clide', 'kill-session', '-t', 'clide-claude-foo-1'],
['-L', 'clide', 'kill-session', '-t', 'clide-claude-foo-2'],
]);
});
test('ignores sessions for other repos', () async {
runner.stdout = 'clide-claude-foo\nclide-claude-bar-1\nclide-claude-foo-1\n';
await tmux.reapSecondaries('clide-claude-foo');
final kills = runner.calls.skip(1).toList();
expect(kills, [
['-L', 'clide', 'kill-session', '-t', 'clide-claude-foo-1'],
]);
});
test('no-op when there are no secondaries', () async {
runner.stdout = 'clide-claude-foo\n';
await tmux.reapSecondaries('clide-claude-foo');
expect(runner.calls, hasLength(1)); // just the list call
});
});
group('killAllForRepo', () {
test('kills primary and every secondary for the repo', () async {
runner.stdout = 'clide-claude-foo\nclide-claude-foo-1\nclide-claude-bar\n';
await tmux.killAllForRepo('clide-claude-foo');
final kills = runner.calls.skip(1).toList();
expect(kills, [
['-L', 'clide', 'kill-session', '-t', 'clide-claude-foo'],
['-L', 'clide', 'kill-session', '-t', 'clide-claude-foo-1'],
]);
});
});
}