Files
clide/test/builtin/claude/tmux_session_test.dart
T
jpmschweitzerandClaude Opus 4.7 5fad8dc430 deliver slash commands typed so the Claude TUI runs them
The composer routed everything through tmux paste-buffer -p (bracketed
paste), and Claude's TUI deliberately doesn't parse a leading slash on
pasted input — so /command and /skill arrived as literal text instead of
running. Now a recognised command (single-line, leading slash, token in
ClaudeConfig's slash list) is delivered via send-keys -l (typed) so the
TUI fires it; everything else keeps the bracketed-paste path, which also
leaves a stray leading slash (e.g. a /tmp path) as literal text rather
than mis-parsing it. The slash list is warmed lazily when a Claude pane
opens so custom commands are recognised.

T-153.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:35:53 +02:00

138 lines
4.7 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'],
]);
});
});
group('hasSession', () {
test('true on exit 0, false otherwise', () async {
runner.exitCode = 0;
expect(await tmux.hasSession('clide-claude-foo'), isTrue);
expect(runner.calls.last, ['-L', 'clide', 'has-session', '-t', 'clide-claude-foo']);
runner.exitCode = 1;
expect(await tmux.hasSession('clide-claude-foo'), isFalse);
});
});
group('sendMessage', () {
test('loads a bracketed paste buffer then submits with Enter', () async {
await tmux.sendMessage('clide-claude-foo', 'hello\nworld');
expect(runner.calls, [
['-L', 'clide', 'set-buffer', '-b', 'clide-compose', '--', 'hello\nworld'],
['-L', 'clide', 'paste-buffer', '-p', '-d', '-b', 'clide-compose', '-t', 'clide-claude-foo'],
['-L', 'clide', 'send-keys', '-t', 'clide-claude-foo', 'Enter'],
]);
});
});
group('sendCommand', () {
test('types the text literally (no bracketed paste) then submits Enter', () async {
await tmux.sendCommand('clide-claude-foo', '/whats-next');
expect(runner.calls, [
['-L', 'clide', 'send-keys', '-t', 'clide-claude-foo', '-l', '--', '/whats-next'],
['-L', 'clide', 'send-keys', '-t', 'clide-claude-foo', 'Enter'],
]);
});
});
}