Files
clide/test/daemon/ui_command_test.dart
T
jpmschweitzerandClaude Opus 4.8 09d1a6370a add clide ui open to drive GUI readers from the CLI (T-231)
The drive-half complement to clide status (observe): an agent can now
open a doc in a GUI reader from the CLI so it can show the user what
it's looking at. 'clide ui open tickets T-48' (decisions by id,
markdown by path) publishes a 'selection' to the kernel MessageBus that
the reader's ReaderNav picks up.

The verb (ui_command.dart) is decoupled from the kernel via a publish
callback, wired in main.dart to the post-boot-captured MessageBus, so it
stays Flutter-free under dart test. Reads CLI positionals or named args.

Also files T-232 (CLI argv args don't reach typed handlers -- the gating
drive-the-IDE bug) and T-233 (diff-panel ui.open follow-up).

Closes T-231 (under T-208).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:30:36 +02:00

79 lines
2.7 KiB
Dart

/// Tests for `ui.open` — the drive-half of D-6 parity (T-231). Verifies it
/// publishes the right MessageBus 'selection' per reader, validates input,
/// and fails honestly when there is no live UI.
library;
import 'package:clide/clide.dart';
import 'package:clide/src/daemon/ui_command.dart';
import 'package:test/test.dart';
void main() {
// Records what would be published to the kernel MessageBus.
late List<({String publisher, String channel, Map<String, Object?> data})> published;
late DaemonDispatcher d;
void wire({bool liveUi = true}) {
published = [];
d = DaemonDispatcher();
registerUiCommands(d, () {
if (!liveUi) return null;
return (publisher, channel, data) => published.add((publisher: publisher, channel: channel, data: data));
});
}
Future<IpcResponse> open(List<String> positional) => d.dispatch(
IpcRequest(id: '1', cmd: 'ui.open', args: {'positional': positional}),
);
test('ui open tickets T-48 publishes a tickets selection', () async {
wire();
final r = await open(['tickets', 'T-48']);
expect(r.ok, isTrue, reason: r.error?.message);
expect(r.data['opened'], isTrue);
expect(published, hasLength(1));
expect(published.single.publisher, 'builtin.tickets');
expect(published.single.channel, 'selection');
expect(published.single.data, {'id': 'T-48'});
});
test('decisions keys on id; markdown keys on path', () async {
wire();
await open(['decisions', 'D-17']);
await open(['markdown', 'docs/initial-plan.md']);
expect(published[0].publisher, 'builtin.decisions');
expect(published[0].data, {'id': 'D-17'});
expect(published[1].publisher, 'builtin.markdown');
expect(published[1].data, {'path': 'docs/initial-plan.md'});
});
test('named args work too (reader/ref)', () async {
wire();
final r = await d.dispatch(IpcRequest(id: '1', cmd: 'ui.open', args: {'reader': 'tickets', 'ref': 'T-7'}));
expect(r.ok, isTrue);
expect(published.single.data, {'id': 'T-7'});
});
test('unknown reader → userError, nothing published', () async {
wire();
final r = await open(['canvas', 'foo']);
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.userError);
expect(published, isEmpty);
});
test('missing ref → userError', () async {
wire();
final r = await open(['tickets']);
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.userError);
expect(published, isEmpty);
});
test('no live UI (null publisher) → toolError, not a hang', () async {
wire(liveUi: false);
final r = await open(['tickets', 'T-48']);
expect(r.ok, isFalse);
expect(r.error?.kind, IpcErrorKind.toolError);
});
}