event-driven test waits, fail-loud on timeout (T-108)

Replaces the fixed Future.delayed sleeps the consultant flagged
with stream-based waits that complete when the awaited event
arrives. Timeout callbacks call fail() with a diagnostic instead
of `onTimeout: () {}` swallowing the signal — a never-producing
pty now reports "pty did not produce X within 5s" instead of an
unhelpful "Actual: ''".

session_test.dart:
  - _readUntil helper subscribes to s.output, completes when a
    marker substring appears (or onDone), fails on timeout.
  - _waitForBuffer polls a buffer the listener is already filling
    after a write; 25ms tick, 5s ceiling, fail-loud on miss.
  - Drops the 500ms settle + 50×100ms polling pattern in the write
    test; uses a "first-byte" completer for prompt-readiness.
  - retry: 2 restored on the four read-dependent forkpty tests
    (the underlying flutter-test-runner pty-output flake hasn't
    fully gone away; recovers cleanly on a fresh spawn).

watcher_test.dart:
  - "emits a created event" awaits stream.firstWhere instead of two
    fixed sleeps.
  - "filters ignored paths" uses pre + post sentinel markers to
    bracket the inotify-delivery window event-driven; the negative
    assertion only runs after the post marker is observed.

event_sink.dart:
  - RecordingEventSink gains a broadcast `stream` for the same
    event-await pattern. PaneRegistry's output test subscribes
    BEFORE spawn so first bytes aren't lost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 22:05:49 +02:00
co-authored by Claude Opus 4.7
parent 7937da1734
commit b66e8f6cc0
7 changed files with 160 additions and 54 deletions
+19 -8
View File
@@ -41,7 +41,20 @@ void main() {
expect(evt.data['id'], pane.id);
});
test('output events base64-encode the child bytes', tags: ['forkpty'], () async {
test('output events base64-encode the child bytes', tags: ['forkpty'], retry: 2, () async {
// Subscribe to the sink stream BEFORE spawn so we don't miss
// any pane.output events that arrive between spawn and listen.
final buf = StringBuffer();
final got = Completer<String>();
final sub = sink.stream.listen((e) {
if (e.kind != 'pane.output') return;
buf.write(utf8.decode(base64Decode(e.data['bytes_b64']! as String)));
if (buf.toString().contains('hello-panes') && !got.isCompleted) {
got.complete(buf.toString());
}
});
addTearDown(sub.cancel);
await registry.spawn(
kind: PaneKind.terminal,
// Child writes then lingers so the reader's poll has a wide
@@ -49,14 +62,12 @@ void main() {
argv: const ['/bin/sh', '-c', 'printf hello-panes; sleep 0.25'],
);
final deadline = DateTime.now().add(const Duration(seconds: 2));
String decoded() => sink.ofKind('pane.output').map((e) => utf8.decode(base64Decode(e.data['bytes_b64']! as String))).join();
while (!decoded().contains('hello-panes') && DateTime.now().isBefore(deadline)) {
await Future<void>.delayed(const Duration(milliseconds: 25));
}
final decoded = await got.future.timeout(
const Duration(seconds: 5),
onTimeout: () => fail('pane.output never carried "hello-panes" within 5s'),
);
expect(decoded, contains('hello-panes'));
expect(sink.ofKind('pane.output'), isNotEmpty);
expect(decoded(), contains('hello-panes'));
});
test('write + resize emit no spurious events, update state', () async {