test sweep: cover kernel/src/{events,ipc} (T-91)
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m2s
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m2s
Three new test files + a small DaemonClient dispose-safety fix: - test/kernel/src/events/types_test.dart (7 tests): every ClideEvent subclass's subsystem / kind / payload contract + the ClideEventEnvelope v1 JSON shape. - test/kernel/src/events/message_bus_test.dart (6 tests): Message shape, MessageBus publish/subscribe/dispose, filter-by-publisher, filter-by-channel, intersection. - test/kernel/src/ipc/client_test.dart (9 tests): real Unix-socket roundtrip via a _TestDaemon helper — connect + correlate request/ response, event forwarding to the DaemonBus, malformed-line skip, daemon-disconnect failing pending requests, stop cleanup, dispose, connect-failure-then-reconnect, daemon-sent-Request warn-and-skip, DaemonConnectionChanged emission. Fix in lib/kernel/src/ipc/client.dart: _setConnected now skips notifyListeners / event emit when _disposed. The socket stream's onDone can fire after dispose runs, which previously hit ChangeNotifier's "used after disposed" assertion. State flip stays unconditional so stop()'s explicit transition still works. Coverage: ipc/client.dart 14% -> 92% (79/86; remaining 7 lines are the socket onError callback + 1 const ctor phantom); events/types .dart 95% (37/39 — 2 const-ctor phantoms); events/message_bus.dart 100%; events/bus.dart stays 100%. Total coverage 71.93% -> 73.34%; floor bumped to 73. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/// Unit tests for `MessageBus` in `lib/kernel/src/events/message_bus.dart`.
|
||||
library;
|
||||
|
||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('Message', () {
|
||||
test('address joins publisher and channel with a slash', () {
|
||||
final m = Message(publisher: 'pty', channel: 'output', data: const {});
|
||||
expect(m.address, 'pty/output');
|
||||
});
|
||||
|
||||
test('timestamp is set at construction', () {
|
||||
final before = DateTime.now();
|
||||
final m = Message(publisher: 'p', channel: 'c', data: const {});
|
||||
final after = DateTime.now();
|
||||
expect(m.timestamp.isBefore(before), isFalse);
|
||||
expect(m.timestamp.isAfter(after), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('MessageBus', () {
|
||||
late MessageBus bus;
|
||||
|
||||
setUp(() => bus = MessageBus());
|
||||
tearDown(() => bus.dispose());
|
||||
|
||||
test('publish delivers to every subscriber', () async {
|
||||
final received = <Message>[];
|
||||
final sub = bus.subscribe().listen(received.add);
|
||||
bus.publish('git', 'status-changed', {'dirty': true});
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(received, hasLength(1));
|
||||
expect(received.first.publisher, 'git');
|
||||
expect(received.first.channel, 'status-changed');
|
||||
expect(received.first.data, {'dirty': true});
|
||||
await sub.cancel();
|
||||
});
|
||||
|
||||
test('subscribe filter by publisher narrows the stream', () async {
|
||||
final got = <Message>[];
|
||||
final sub = bus.subscribe(publisher: 'git').listen(got.add);
|
||||
bus.publish('git', 'a', const {});
|
||||
bus.publish('pty', 'a', const {});
|
||||
bus.publish('git', 'b', const {});
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(got.map((m) => m.channel), ['a', 'b']);
|
||||
await sub.cancel();
|
||||
});
|
||||
|
||||
test('subscribe filter by channel narrows the stream', () async {
|
||||
final got = <Message>[];
|
||||
final sub = bus.subscribe(channel: 'output').listen(got.add);
|
||||
bus.publish('pty', 'output', const {});
|
||||
bus.publish('pty', 'exit', const {});
|
||||
bus.publish('git', 'output', const {});
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(got.map((m) => m.publisher), ['pty', 'git']);
|
||||
await sub.cancel();
|
||||
});
|
||||
|
||||
test('subscribe with both filters intersects them', () async {
|
||||
final got = <Message>[];
|
||||
final sub = bus.subscribe(publisher: 'git', channel: 'status').listen(got.add);
|
||||
bus.publish('git', 'status', const {});
|
||||
bus.publish('git', 'other', const {});
|
||||
bus.publish('pty', 'status', const {});
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(got, hasLength(1));
|
||||
await sub.cancel();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/// Unit tests for the `ClideEvent` subclasses and `ClideEventEnvelope`
|
||||
/// in `lib/kernel/src/events/types.dart`.
|
||||
library;
|
||||
|
||||
import 'package:clide/kernel/src/events/types.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ClideEvent subclasses — subsystem / kind / payload', () {
|
||||
test('DaemonConnectionChanged', () {
|
||||
const e = DaemonConnectionChanged(connected: true);
|
||||
expect(e.subsystem, 'ipc');
|
||||
expect(e.kind, 'connection-changed');
|
||||
expect(e.payload(), {'connected': true});
|
||||
});
|
||||
|
||||
test('ThemeChanged', () {
|
||||
const e = ThemeChanged(themeName: 'midnight');
|
||||
expect(e.subsystem, 'theme');
|
||||
expect(e.kind, 'changed');
|
||||
expect(e.payload(), {'theme': 'midnight'});
|
||||
});
|
||||
|
||||
test('ProjectOpened', () {
|
||||
const e = ProjectOpened(path: '/tmp/x');
|
||||
expect(e.subsystem, 'project');
|
||||
expect(e.kind, 'opened');
|
||||
expect(e.payload(), {'path': '/tmp/x'});
|
||||
});
|
||||
|
||||
test('ProjectClosed — empty payload default applies', () {
|
||||
const e = ProjectClosed();
|
||||
expect(e.subsystem, 'project');
|
||||
expect(e.kind, 'closed');
|
||||
expect(e.payload(), isEmpty);
|
||||
});
|
||||
|
||||
test('ExtensionActivated / ExtensionDeactivated', () {
|
||||
const a = ExtensionActivated(id: 'builtin.git');
|
||||
expect(a.subsystem, 'extensions');
|
||||
expect(a.kind, 'activated');
|
||||
expect(a.payload(), {'id': 'builtin.git'});
|
||||
const d = ExtensionDeactivated(id: 'builtin.git');
|
||||
expect(d.subsystem, 'extensions');
|
||||
expect(d.kind, 'deactivated');
|
||||
expect(d.payload(), {'id': 'builtin.git'});
|
||||
});
|
||||
|
||||
test('DaemonEvent merges ts into payload', () {
|
||||
final ts = DateTime.utc(2026, 5, 11, 12, 0, 0);
|
||||
final e = DaemonEvent(
|
||||
subsystem: 'pty',
|
||||
kind: 'output',
|
||||
data: {'bytes': 'aGVsbG8='},
|
||||
ts: ts,
|
||||
);
|
||||
expect(e.subsystem, 'pty');
|
||||
expect(e.kind, 'output');
|
||||
expect(e.payload()['ts'], ts.toIso8601String());
|
||||
expect(e.payload()['bytes'], 'aGVsbG8=');
|
||||
});
|
||||
});
|
||||
|
||||
group('ClideEventEnvelope', () {
|
||||
test('toJson builds a v1 envelope around the event', () {
|
||||
final ts = DateTime.utc(2026, 5, 11, 9, 30);
|
||||
const event = ThemeChanged(themeName: 'paper');
|
||||
final envelope = ClideEventEnvelope(event, ts);
|
||||
final json = envelope.toJson();
|
||||
expect(json['v'], 1);
|
||||
expect(json['subsystem'], 'theme');
|
||||
expect(json['kind'], 'changed');
|
||||
expect(json['ts'], ts.toIso8601String());
|
||||
expect(json['data'], {'theme': 'paper'});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user