test: deflake the suite — pumpEventQueue instead of Future.delayed(Duration.zero)

~173 `await Future<void>.delayed(Duration.zero)` async-settle waits across 25
test files yield the microtask queue exactly once; when an event→handler chain
needs more than one hop they lose the race under CI's parallel load, so the
failing set varied run to run. Replace with `await pumpEventQueue()` (the
deterministic drain already used elsewhere in the suite); rewired the shared
settle()/tick() helpers in one shot. menu_bar's toggle-close test gets a bounded
extra pump. Verified green under CI=true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:13:13 +02:00
co-authored by Claude Opus 4.8
parent 3dce5c614d
commit e55b4a9173
26 changed files with 175 additions and 174 deletions
@@ -122,18 +122,18 @@ void main() {
test('an image-show message with no live session is dropped silently (T-249)', () async {
f.services.messages.publish('test', imageShowChannel, {'path': '/tmp/x.png'});
f.services.messages.publish('test', imageShowChannel, {'path': ''});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Nothing to assert beyond "no throw" — there is no conversation to
// receive the card and the CLI already acked at publish time.
});
test('a project switch closes sessions that belong to the old root (T-269)', () async {
f.services.events.emit(const ProjectOpened(path: '/repo-one'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
f.services.events.emit(const ProjectOpened(path: '/repo-one'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
f.services.events.emit(const ProjectOpened(path: '/repo-two'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// No live sessions in this fixture — the sweep runs over an empty set.
expect(activeSessionOrchestrator!.sessions, isEmpty);
});
@@ -230,7 +230,7 @@ void main() {
}
expect(orch.sessions, isEmpty);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(created.single.killed, isTrue);
});
@@ -247,7 +247,7 @@ void main() {
}
expect(orch.sessions, isEmpty);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(created.every((p) => p.killed), isTrue);
});
@@ -65,7 +65,7 @@ void main() {
final m = await orch.spawn(SpawnSpec(id: 'fork-x', role: 'teammate', sessionId: 'placeholder-uuid', cwd: '/repo', forkSourceSessionId: 'source-uuid'));
expect(m.sessionId, 'placeholder-uuid'); // starts as the placeholder
created.last.emit(jsonEncode({'type': 'system', 'subtype': 'init', 'session_id': 'real-fork-id', 'model': 'claude-opus-4-8', 'permissionMode': 'default'}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(m.sessionId, 'real-fork-id'); // updated to the branch's real id
});
@@ -120,7 +120,7 @@ void main() {
await orch.close('primary');
expect(orch.byId('primary'), isNull);
expect(orch.sessions, isEmpty);
await Future<void>.delayed(Duration.zero); // session.dispose is async
await pumpEventQueue(); // session.dispose is async
expect(created.single.killed, isTrue);
});
@@ -143,7 +143,7 @@ void main() {
await orch.spawn(spec('a'));
await orch.spawn(spec('b'));
orch.dispose();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(created.every((p) => p.killed), isTrue);
});
@@ -164,7 +164,7 @@ void main() {
await orch.spawn(teamSpec('primary', 'lead', 'lead'));
await orch.spawn(teamSpec('teammate:tyre', 'tyre', 'teammate'));
orch.broker.sendMessage('primary', 'tyre', 'pick up T-9');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final tyreProc = created[1];
expect(tyreProc.writes.any((w) => w.contains('[team] lead: pick up T-9')), isTrue);
});
@@ -210,7 +210,7 @@ void main() {
},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(s.availableModels, hasLength(2));
expect(s.availableModels[0].value, 'default');
expect(s.availableModels[0].description, 'recommended');
@@ -226,13 +226,13 @@ void main() {
expect(sent['type'], 'control_request');
expect((sent['request'] as Map)['subtype'], 'set_model');
expect((sent['request'] as Map)['model'], 'sonnet');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.model, 'sonnet');
});
test('setModel(default) does not guess the resolved model', () async {
session.setModel('default');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses, isEmpty, reason: 'only the CLI knows what default resolves to');
});
@@ -240,10 +240,10 @@ void main() {
final errors = <String>[];
session.modelErrors.listen(errors.add);
proc.emit(initEvent()); // model: claude-opus-4-7
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.setModel('bogus-model');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.model, 'bogus-model'); // optimistic
final rid = (jsonDecode(proc.writes.single) as Map)['request_id'];
@@ -253,7 +253,7 @@ void main() {
'response': {'subtype': 'error', 'request_id': rid, 'error': 'Unknown model: bogus-model'},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.model, 'claude-opus-4-7', reason: 'rolled back');
expect(errors, ['Unknown model: bogus-model']);
});
@@ -267,7 +267,7 @@ void main() {
'response': {'subtype': 'success', 'request_id': rid},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.model, 'opus');
});
});
@@ -275,7 +275,7 @@ void main() {
test('parses assistant text + tool_use events into items', () async {
proc.emit(assistantText('hello there'));
proc.emit(assistantToolUse());
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(items, hasLength(2));
expect(items[0], isA<AssistantTextMessage>());
@@ -290,7 +290,7 @@ void main() {
test('derives status: model + tokens from assistant, permission-mode from init', () async {
proc.emit(initEvent());
proc.emit(assistantText('hi'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.model, 'claude-opus-4-7');
expect(statuses.last.permissionMode, 'default');
@@ -300,7 +300,7 @@ void main() {
test('only emits status on change', () async {
proc.emit(initEvent());
proc.emit(initEvent()); // identical → no second emit
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses, hasLength(1));
});
@@ -308,11 +308,11 @@ void main() {
// the plain broadcast stream dropped it — the status bar stayed blank.
test('subscribing AFTER the init event still yields the status (T-274/T-386)', () async {
proc.emit(initEvent());
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final late = <SessionStatus>[];
session.statusStream.listen(late.add);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(late, hasLength(1), reason: 'replay-latest delivers the current status to late binders');
expect(late.single.model, 'claude-opus-4-7');
@@ -323,7 +323,7 @@ void main() {
final ids = <String>[];
session.sessionIdResolved.listen(ids.add);
proc.emit(jsonEncode({'type': 'system', 'subtype': 'init', 'session_id': 'sess-abc', 'model': 'claude-opus-4-7', 'permissionMode': 'default'}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.claudeSessionId, 'sess-abc');
expect(ids, ['sess-abc']);
});
@@ -331,7 +331,7 @@ void main() {
group('live cost/context from result events (T-168)', () {
test('result event with total_cost_usd populates cost field', () async {
proc.emit(resultEvent(cost: 0.042));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.cost, closeTo(0.042, 1e-9));
});
@@ -344,7 +344,7 @@ void main() {
},
),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.contextWindow, 1000000);
});
@@ -352,7 +352,7 @@ void main() {
proc.emit(initEvent());
proc.emit(assistantText('hi'));
proc.emit(resultEvent(cost: 0.05));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.model, 'claude-opus-4-7');
expect(statuses.last.permissionMode, 'default');
expect(statuses.last.cost, closeTo(0.05, 1e-9));
@@ -361,7 +361,7 @@ void main() {
test('result event without cost or modelUsage emits nothing', () async {
final before = statuses.length;
proc.emit(jsonEncode({'type': 'result', 'result': '', 'usage': <String, dynamic>{}}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.length, before); // no change → no emit
});
});
@@ -369,14 +369,14 @@ void main() {
group('rate_limit_event status (T-168)', () {
test('rate_limit_event with status populates rateLimitInfo', () async {
proc.emit(rateLimitEvent(status: 'rate_limited'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.rateLimitInfo, contains('rate limited'));
});
test('rate_limit_event with an ISO resetsAt includes the time', () async {
// 2026-05-30T14:32:00Z → shows 14:32 (UTC, local may differ but contains digits)
proc.emit(rateLimitEvent(status: 'rate_limited', resetsAt: '2026-05-30T14:32:00Z'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.rateLimitInfo, contains('rate limited'));
expect(statuses.last.rateLimitInfo, contains('resets'));
});
@@ -390,7 +390,7 @@ void main() {
'rate_limit_info': {'status': 'rate_limited', 'resetsAt': 1780000000},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.rateLimitInfo, contains('rate limited'));
expect(statuses.last.rateLimitInfo, contains('resets'));
});
@@ -401,7 +401,7 @@ void main() {
proc.emit(streamMessageStart('msg-1'));
proc.emit(streamTextDelta('one '));
proc.emit(streamTextDelta('two three'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final parts = items.whereType<AssistantTextMessage>().toList();
// Each delta emits an upserting placeholder; all share the stable uuid and
// the latest carries the accumulated text.
@@ -414,7 +414,7 @@ void main() {
proc.emit(streamMessageStart('msg-2'));
proc.emit(streamTextDelta('hel'));
proc.emit(assistantTextWithId('msg-2', 'hello there'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final parts = items.whereType<AssistantTextMessage>().toList();
// The final, complete text reuses the placeholder uuid so the controller
// replaces rather than appends — no duplicate.
@@ -427,7 +427,7 @@ void main() {
proc.emit(streamTextDelta('working'));
proc.emit(assistantTextWithId('msg-3', 'working on it')); // finalises partial-msg-3
proc.emit(assistantToolUse()); // separate block, own uuid
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final tool = items.whereType<AssistantToolUse>().single;
expect(tool.uuid, isNot('partial-msg-3'));
expect(items.last, isA<AssistantToolUse>());
@@ -438,11 +438,11 @@ void main() {
proc.emit(streamTextDelta('first'));
proc.emit(streamMessageStop());
proc.emit(jsonEncode({'type': 'result', 'result': '', 'usage': <String, dynamic>{}}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// A new turn reusing the same id still streams (no leftover finalised flag).
proc.emit(streamMessageStart('msg-4'));
proc.emit(streamTextDelta('second'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final parts = items.whereType<AssistantTextMessage>().toList();
expect(parts.last.text, 'second');
});
@@ -452,14 +452,14 @@ void main() {
proc.emit('');
proc.emit('not json');
proc.emit(' ');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(items, isEmpty);
expect(statuses, isEmpty);
});
test('send writes a stream-json user message and echoes it locally', () async {
session.send('do the thing');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(proc.writes, hasLength(1));
final sent = jsonDecode(proc.writes.single) as Map<String, Object?>;
@@ -484,7 +484,7 @@ void main() {
},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final u = items.whereType<UserMessage>().single;
expect(u.injected, isTrue);
});
@@ -501,7 +501,7 @@ void main() {
},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(items.whereType<UserMessage>().single.injected, isFalse);
});
@@ -509,7 +509,7 @@ void main() {
final emitted = <ToolPrompt?>[];
session.pendingPromptStream.listen(emitted.add);
proc.emit(canUseTool('req-1'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final p = session.pendingPrompt;
expect(p, isNotNull);
@@ -526,7 +526,7 @@ void main() {
test('resolvePrompt(allow) writes success+updatedInput and clears the pending prompt', () async {
proc.emit(canUseTool('req-2'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final p = session.pendingPrompt!;
session.resolvePrompt(p.promptId, AllowTool(p.input));
@@ -557,13 +557,13 @@ void main() {
},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.pendingPrompt!.permissionSuggestions, hasLength(1));
});
test('resolvePrompt(allow with updatedPermissions) echoes them in the response', () async {
proc.emit(canUseTool('rp'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt(
'rp',
AllowTool(
@@ -580,7 +580,7 @@ void main() {
test('resolvePrompt(allow with a follow-up note) sends the note as a user message', () async {
proc.emit(canUseTool('rn'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt('rn', AllowTool(const {'x': 1}, followUpNote: 'use docs/ instead'));
// first write = control_response (allow), second = the follow-up message
@@ -592,14 +592,14 @@ void main() {
test('resolvePrompt records the tool outcome — allow', () async {
proc.emit(canUseTool('o1'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt('o1', AllowTool(const {}));
expect(session.toolUseOutcomes['toolu_1'], isTrue);
});
test('resolvePrompt records the tool outcome — deny', () async {
proc.emit(canUseTool('o2'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt('o2', const DenyTool('no'));
expect(session.toolUseOutcomes['toolu_1'], isFalse);
});
@@ -608,44 +608,44 @@ void main() {
test('approving ExitPlanMode leaves plan mode (T-337)', () async {
proc.emit(planInit());
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.permissionMode, 'plan');
proc.emit(canUseTool('exit-1', tool: 'ExitPlanMode', input: {'plan': 'do the thing'}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final p = session.pendingPrompt!;
expect(p.toolName, 'ExitPlanMode');
session.resolvePrompt(p.promptId, AllowTool(p.input));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.permissionMode, 'default', reason: 'approving ExitPlanMode must exit plan mode');
});
test('denying ExitPlanMode stays in plan mode (T-337)', () async {
proc.emit(planInit());
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
proc.emit(canUseTool('exit-2', tool: 'ExitPlanMode', input: {'plan': 'x'}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt(session.pendingPrompt!.promptId, const DenyTool('keep planning'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.permissionMode, 'plan', reason: 'a denied plan-exit keeps plan mode');
});
test('approving a non-ExitPlanMode tool does not change plan mode (T-337)', () async {
proc.emit(planInit());
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
proc.emit(canUseTool('w1')); // a Write
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt(session.pendingPrompt!.promptId, AllowTool(const {}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.permissionMode, 'plan', reason: 'only ExitPlanMode exits plan mode');
});
test('noteEffort merges the effort level into the status (T-412)', () async {
proc.emit(initEvent());
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.noteEffort('xhigh');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.effort, 'xhigh');
expect(statuses.last.model, 'claude-opus-4-7'); // merge, not replace
});
@@ -653,7 +653,7 @@ void main() {
test('addLocalNotice emits a synthetic clide item and sends nothing (T-411)', () async {
final before = proc.writes.length;
session.addLocalNotice('/status is a Claude Code TUI command');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final notice = items.whereType<AssistantTextMessage>().single;
expect(notice.synthetic, isTrue);
expect(notice.text, contains('/status'));
@@ -662,7 +662,7 @@ void main() {
test('resolvePrompt(deny) writes a deny decision with a message', () async {
proc.emit(canUseTool('req-3'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt('req-3', const DenyTool('nope'));
final decision = ((jsonDecode(proc.writes.single) as Map)['response'] as Map)['response'] as Map;
@@ -682,14 +682,14 @@ void main() {
},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt(
'aq',
AllowTool(const {
'answers': {'Pet': 'Dogs'},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final echo = items.whereType<UserMessage>().toList();
expect(echo, hasLength(1));
@@ -699,7 +699,7 @@ void main() {
test('prompts queue: resolving the head surfaces the next', () async {
proc.emit(canUseTool('q1'));
proc.emit(canUseTool('q2'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.pendingPrompt!.promptId, 'q1');
session.resolvePrompt('q1', AllowTool(const {}));
@@ -710,7 +710,7 @@ void main() {
test('resolvePrompt is a no-op for an unknown / already-resolved id', () async {
proc.emit(canUseTool('req-4'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
session.resolvePrompt('req-4', AllowTool(const {})); // resolves
session.resolvePrompt('req-4', AllowTool(const {})); // already gone
@@ -726,7 +726,7 @@ void main() {
'request': {'subtype': 'mystery_subtype'},
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(items, isEmpty);
final resp = (jsonDecode(proc.writes.single) as Map)['response'] as Map;
@@ -767,11 +767,11 @@ void main() {
// The control_request itself emits no status event; without an optimistic
// update the badge stayed stale. Each call must surface the new mode.
session.setPermissionMode('plan');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.permissionMode, 'plan');
session.setPermissionMode('acceptEdits');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(statuses.last.permissionMode, 'acceptEdits');
});
@@ -782,7 +782,7 @@ void main() {
expect(session.busy, isTrue);
proc.emit(jsonEncode({'type': 'result', 'subtype': 'success'}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.busy, isFalse);
// Leading false is the replayed seed — busyStream tells a new
// subscriber the CURRENT state before the live updates (T-386).
@@ -796,14 +796,14 @@ void main() {
test('promptedToolUseIds contains the tool_use_id after a can_use_tool arrives', () async {
proc.emit(canUseTool('p1'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// promptedToolUseIds exposes the set of prompted tool use ids.
expect(session.promptedToolUseIds, contains('toolu_1'));
});
test('rate_limit_event with a non-ISO resetsAt shows the raw string', () async {
proc.emit(rateLimitEvent(status: 'rate_limited', resetsAt: 'soon'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Non-ISO resetsAt → DateTime.tryParse returns null → raw string is used.
expect(statuses.last.rateLimitInfo, 'rate limited — resets soon');
});
@@ -841,14 +841,14 @@ void main() {
'id': 0,
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final r = mcpResponseOf(mproc.writes.last);
expect((r['result'] as Map)['serverInfo'], {'name': 'clide-team', 'version': '9.9.9'});
});
test('answers tools/list with the server tools', () async {
mproc.emit(mcpMessage('m2', {'method': 'tools/list', 'jsonrpc': '2.0', 'id': 1}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final r = mcpResponseOf(mproc.writes.last);
final tools = (r['result'] as Map)['tools'] as List;
expect(tools.single['name'], 'ping');
@@ -863,7 +863,7 @@ void main() {
'id': 2,
}),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(server.calls, ['ping']);
final r = mcpResponseOf(mproc.writes.last);
final content = (r['result'] as Map)['content'] as List;
@@ -872,21 +872,21 @@ void main() {
test('an mcp_message for an unknown server is answered with an error', () async {
mproc.emit(mcpMessage('m4', {'method': 'tools/list', 'jsonrpc': '2.0', 'id': 3}, server: 'nope'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final r = mcpResponseOf(mproc.writes.last);
expect(r['error'], isNotNull);
});
test('answers notifications/initialized with an empty result', () async {
mproc.emit(mcpMessage('m5', {'method': 'notifications/initialized', 'jsonrpc': '2.0', 'id': 4}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final r = mcpResponseOf(mproc.writes.last);
expect(r['result'], isA<Map>());
});
test('answers unknown MCP method with a JSON-RPC error -32601', () async {
mproc.emit(mcpMessage('m6', {'method': 'resources/list', 'jsonrpc': '2.0', 'id': 5}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final r = mcpResponseOf(mproc.writes.last);
expect((r['error'] as Map)['code'], -32601);
expect((r['error'] as Map)['message'], contains('resources/list'));
@@ -900,12 +900,12 @@ void main() {
final ends = <SessionEnd>[];
session.endedStream.listen(ends.add);
session.send('do something');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.busy, isTrue, reason: 'a send marks the turn in flight');
proc.stderr.addAll(['boom: stack', 'fatal: died']);
proc.exit.complete(70);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.busy, isFalse, reason: 'a dead process is not thinking');
expect(ends, hasLength(1));
@@ -918,11 +918,11 @@ void main() {
final pendings = <ToolPrompt?>[];
session.pendingPromptStream.listen(pendings.add);
proc.emit(canUseTool('p1'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.pendingPrompt, isNotNull);
proc.exit.complete(1);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(session.pendingPrompt, isNull);
expect(pendings.last, isNull, reason: 'the composer swaps back from the prompt UI');
});
@@ -932,7 +932,7 @@ void main() {
final s = StreamJsonSession(p)..start();
await s.dispose();
p.exit.complete(9); // the kill's exit must not surface as a crash
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(s.end, isNull);
});
});
@@ -976,7 +976,7 @@ void main() {
}),
);
p.emit(jsonEncode({'type': 'system', 'subtype': 'task_notification', 'tool_use_id': 'toolu_wf', 'status': 'completed', 'summary': 'done'}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final run = session.workflows['toolu_wf'];
expect(run, isNotNull);
@@ -994,7 +994,7 @@ void main() {
final items = <ConversationItem>[];
session.items.listen(items.add);
p.emit(jsonEncode({'type': 'system', 'subtype': 'task_progress', 'tool_use_id': 'toolu_wf', 'workflow_progress': const []}));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(items, isEmpty);
expect(session.workflows.containsKey('toolu_wf'), isTrue);
});
+3 -3
View File
@@ -168,7 +168,7 @@ void main() {
final events = <void>[];
final sub = broker.changes.listen((_) => events.add(null));
broker.removeMember('teammate:tyre');
await Future<void>.delayed(Duration.zero); // let the broadcast event deliver
await pumpEventQueue(); // let the broadcast event deliver
await sub.cancel();
expect(events, hasLength(1));
});
@@ -177,7 +177,7 @@ void main() {
var done = false;
broker.changes.listen(null, onDone: () => done = true);
broker.dispose();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(done, isTrue);
});
});
@@ -223,7 +223,7 @@ void main() {
final events = <void>[];
final sub = broker.changes.listen((_) => events.add(null));
broker.reassignTask(id, 'primary');
await Future<void>.delayed(Duration.zero); // let the broadcast event deliver
await pumpEventQueue(); // let the broadcast event deliver
await sub.cancel();
expect(events, hasLength(1));
});
+12 -12
View File
@@ -40,7 +40,7 @@ void main() {
final events = <void>[];
final sub = model.changes.listen((_) => events.add(null));
broker.sendMessage('primary', 'tyre', 'hello tyre');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
await sub.cancel();
expect(model.messages, hasLength(1));
expect(model.messages.single.from, 'lead');
@@ -51,7 +51,7 @@ void main() {
test('broadcast messages are appended for each recipient', () async {
broker.broadcast('primary', 'standup');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// One message for 'tyre', one for 'user' (both are non-sender members).
expect(model.messages.length, greaterThanOrEqualTo(1));
expect(model.messages.every((m) => m.text == 'standup'), isTrue);
@@ -59,7 +59,7 @@ void main() {
test('direct send_message to user lands in the timeline', () async {
broker.sendMessage('primary', 'user', 'attention user');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(model.messages.single.text, 'attention user');
expect(model.messages.single.to, 'user');
// User has no stdin delivery.
@@ -71,7 +71,7 @@ void main() {
final sub = model.changes.listen((_) => events.add(null));
broker.sendMessage('primary', 'tyre', 'one');
broker.sendMessage('primary', 'tyre', 'two');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
await sub.cancel();
expect(events, hasLength(2));
});
@@ -79,7 +79,7 @@ void main() {
test('messages list is append-only (oldest first)', () async {
broker.sendMessage('primary', 'tyre', 'first');
broker.sendMessage('primary', 'tyre', 'second');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(model.messages[0].text, 'first');
expect(model.messages[1].text, 'second');
});
@@ -92,20 +92,20 @@ void main() {
group('postAsUser routing', () {
test('postAsUser with no toName broadcasts to all agents', () async {
model.postAsUser('hello team');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Delivered to tyre (lead is the sender-equivalent; user has no delivery).
expect(delivered.any((d) => d.$1 == 'teammate:tyre'), isTrue);
});
test('postAsUser with toName=team broadcasts', () async {
model.postAsUser('standup', toName: 'team');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(delivered.any((d) => d.$1 == 'teammate:tyre'), isTrue);
});
test('postAsUser with a member name delivers to that member only', () async {
model.postAsUser('hey tyre', toName: 'tyre');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(delivered.length, 1);
expect(delivered.single.$1, 'teammate:tyre');
});
@@ -148,7 +148,7 @@ void main() {
},
);
interruptModel.postAsUser('cancel that', toName: 'tyre', interrupt: true);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(resolvedName, 'tyre');
interruptModel.dispose();
});
@@ -163,7 +163,7 @@ void main() {
},
);
interruptModel.postAsUser('abort all', interrupt: true);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Broadcast → resolver not called (no single target to interrupt).
expect(resolvedName, isNull);
interruptModel.dispose();
@@ -179,7 +179,7 @@ void main() {
},
);
interruptModel.postAsUser('no interrupt', toName: 'tyre', interrupt: false);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(resolvedName, isNull);
interruptModel.dispose();
});
@@ -193,7 +193,7 @@ void main() {
var done = false;
model.changes.listen(null, onDone: () => done = true);
model.dispose();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(done, isTrue);
});
}
+3 -3
View File
@@ -59,7 +59,7 @@ void main() {
await orch.spawn(SpawnSpec(id: 'primary', role: 'primary', sessionId: 'p-uuid', cwd: '/repo'));
final accepted = await applyTicketPickUp(payload(), orchestrator: orch, ipc: ipc, messages: messages);
await Future<void>.delayed(Duration.zero); // let the bus deliver 'changed'
await pumpEventQueue(); // let the bus deliver 'changed'
expect(accepted, isTrue);
expect(statusCalls, hasLength(1));
@@ -71,7 +71,7 @@ void main() {
test('no live session: nothing injected, ticket untouched (T-339)', () async {
// Orchestrator has no sessions → quiet no-op.
final accepted = await applyTicketPickUp(payload(), orchestrator: orch, ipc: ipc, messages: messages);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(accepted, isFalse);
expect(statusCalls, isEmpty);
@@ -87,7 +87,7 @@ void main() {
ipc: ipc,
messages: messages,
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(accepted, isTrue); // prompt still delivered
expect(statusCalls, isEmpty); // but no transition
@@ -97,9 +97,9 @@ void main() {
test('decisions.detail tab count stays at 1 after multiple selections', () async {
_select(f, 'D-1');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
_select(f, 'D-2');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final tabs = f.services.panels.tabsFor(Slots.contextPanel);
expect(tabs.where((t) => t.id == 'decisions.detail').length, 1, reason: 'no per-click re-contribution — exactly one decisions.detail tab');
@@ -107,25 +107,25 @@ void main() {
test('selection activates decisions.detail tab', () async {
_select(f, 'D-1');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'decisions.detail');
});
test('second selection switches to decisions.detail (already active, stays)', () async {
_select(f, 'D-1');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
_select(f, 'D-2');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'decisions.detail');
});
test('clicking the same decision twice leaves decisions.detail active', () async {
_select(f, 'D-5');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
_select(f, 'D-5');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'decisions.detail');
expect(f.services.panels.tabsFor(Slots.contextPanel).where((t) => t.id == 'decisions.detail').length, 1);
@@ -139,7 +139,7 @@ void main() {
f.services.arrangement.setCollapsed(Slots.contextPanel, true);
_select(f, 'D-3');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.isVisible(Slots.contextPanel), isTrue, reason: 'panel must be made visible on selection');
expect(f.services.arrangement.isCollapsed(Slots.contextPanel), isFalse, reason: 'panel must be un-collapsed on selection');
@@ -149,7 +149,7 @@ void main() {
for (var i = 1; i <= 10; i++) {
_select(f, 'D-$i');
}
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'decisions.detail');
expect(f.services.panels.tabsFor(Slots.contextPanel).where((t) => t.id == 'decisions.detail').length, 1);
@@ -158,11 +158,11 @@ void main() {
test('null id in selection message is ignored', () async {
// Seed a valid tab selection first.
_select(f, 'D-1');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Then send a bad message.
f.services.messages.publish('builtin.decisions', 'selection', {'id': null});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Tab still active, still only one.
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'decisions.detail');
@@ -176,7 +176,7 @@ void main() {
// tab at all — but the panel activation path must not fire either.
f.services.panels.registerSlot(const SlotDefinition(id: Slots.contextPanel, position: SlotPosition.right));
f.services.messages.publish('builtin.decisions', 'selection', {'id': 'D-99'});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(
f.services.panels.activeTabIn(Slots.contextPanel),
+4 -4
View File
@@ -54,7 +54,7 @@ void main() {
expect(c.focusPath, 'lib/b.dart');
expect(notified, greaterThan(0));
// focus() reloads so the latest edits to that file are present.
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(diffCalls, greaterThan(before));
});
@@ -62,13 +62,13 @@ void main() {
c.focus('lib/gone.dart');
expect(c.focusPath, 'lib/gone.dart');
// The reload triggered by focus() returns a list without that file.
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.focusPath, isNull);
});
test('a focus that stays in the diff survives reload', () async {
c.focus('lib/a.dart');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.focusPath, 'lib/a.dart');
});
@@ -76,7 +76,7 @@ void main() {
await c.load();
final before = diffCalls;
bus.emit(DaemonEvent(subsystem: 'git', kind: 'git.changed', data: const {}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(diffCalls, greaterThan(before));
});
@@ -34,36 +34,36 @@ void main() {
test('editor.opened opens the editor split', () async {
expect(f.services.arrangement.editorOpen, isFalse);
emitEditor('editor.opened', id: 'b_1');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.editorOpen, isTrue);
});
test('editor.active-changed with a buffer keeps the split open', () async {
emitEditor('editor.active-changed', id: 'b_2');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.editorOpen, isTrue);
});
test('editor.active-changed with a null id collapses the split', () async {
emitEditor('editor.opened', id: 'b_1');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.editorOpen, isTrue);
emitEditor('editor.active-changed', id: null);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.editorOpen, isFalse);
});
test('a non-editor event does not open the split', () async {
f.services.events.emit(DaemonEvent(subsystem: 'git', kind: 'changed', data: const {}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.editorOpen, isFalse);
});
test('after deactivate, editor events no longer open the split', () async {
await f.services.extensions.deactivate('builtin.editor');
emitEditor('editor.opened', id: 'b_9');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.arrangement.editorOpen, isFalse);
});
}
@@ -287,7 +287,7 @@ void main() {
// Emit files.changed for a file at root level — parent is ''.
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': 'README.md'}, ts: DateTime.now().toUtc()));
// Give the async refresh a tick.
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(lsCallCount, greaterThan(countAfterLoad));
});
@@ -305,7 +305,7 @@ void main() {
// 'lib' is not in _entries yet, so its parent 'lib/src' won't be there.
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': 'lib/src/foo.dart'}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(lsCallCount, countAfterLoad);
});
@@ -322,7 +322,7 @@ void main() {
final countAfterLoad = lsCallCount;
f.services.events.emit(DaemonEvent(subsystem: 'editor', kind: 'files.changed', data: {'path': 'README.md'}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(lsCallCount, countAfterLoad);
});
@@ -339,7 +339,7 @@ void main() {
final countAfterLoad = lsCallCount;
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.opened', data: {'path': 'README.md'}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(lsCallCount, countAfterLoad);
});
@@ -352,7 +352,7 @@ void main() {
final countAfterLoad = 1;
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': 'pubspec.yaml'}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Root '' is in _entries, so reload fires.
expect(countAfterLoad, 1); // just confirming test ran
@@ -364,7 +364,7 @@ void main() {
await c.load();
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': null}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// No crash — just checking the null-path guard.
});
});
@@ -488,7 +488,7 @@ void main() {
c.dispose();
ctrl = null; // prevent tearDown from double-disposing
f.services.events.emit(DaemonEvent(subsystem: 'files', kind: 'files.changed', data: {'path': 'README.md'}, ts: DateTime.now().toUtc()));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
// Test passes if no exception.
});
});
+1 -1
View File
@@ -33,7 +33,7 @@ void main() {
);
// Let the broadcast streams (bus / events) deliver.
Future<void> settle() => Future<void>.delayed(Duration.zero);
Future<void> settle() => pumpEventQueue();
group('load + status parsing', () {
test('hydrates branch / counts / file lists from git.status', () async {
+1
View File
@@ -101,6 +101,7 @@ void main() {
expect(find.text('Open Folder…'), findsOneWidget);
await tester.tap(find.text('File'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 20)); // flush the close under load
expect(find.text('Open Folder…'), findsNothing);
});
+1 -1
View File
@@ -120,7 +120,7 @@ void main() {
});
c.switchView(PqlView.markdown);
expect(c.view, PqlView.markdown);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(filesCalls, 1);
// Switching to the same view is a no-op.
c.switchView(PqlView.markdown);
@@ -69,7 +69,7 @@ void main() {
final c = make();
await c.run('foo');
emitMatch('s1', [m('a.dart', 1), m('a.dart', 5), m('b.dart', 2)]);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.matchCount, 3);
final g = c.grouped();
expect(g.keys, containsAll(['a.dart', 'b.dart']));
@@ -81,7 +81,7 @@ void main() {
final c = make();
await c.run('foo'); // activeSearchId == s1
emitMatch('OLD', [m('z.dart', 9)]);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.matchCount, 0);
});
@@ -91,7 +91,7 @@ void main() {
f.services.events.emit(
DaemonEvent(subsystem: 'search', kind: 'search.done', data: const {'searchId': 's1', 'cancelled': false}, ts: DateTime.now().toUtc()),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.running, isFalse);
expect(c.done, isTrue);
});
@@ -102,7 +102,7 @@ void main() {
f.services.events.emit(
DaemonEvent(subsystem: 'search', kind: 'search.error', data: const {'searchId': 's1', 'message': 'invalid regex: x'}, ts: DateTime.now().toUtc()),
);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.error, contains('invalid regex'));
expect(c.running, isFalse);
});
@@ -111,7 +111,7 @@ void main() {
final c = make();
await c.run('foo');
emitMatch('s1', [m('a.dart', 1)]);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c.matchCount, 1);
await c.run('bar');
expect(c.matchCount, 0); // cleared on new run
@@ -125,7 +125,7 @@ void main() {
});
final c = make();
c.openMatch(const SearchMatch(path: 'a.dart', line: 7, matchStart: 0, matchEnd: 3, preview: 'foo'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(sent!['path'], 'a.dart');
expect(sent!['line'], 7);
});
+4 -4
View File
@@ -47,14 +47,14 @@ void main() {
test('a load message loads the ticket', () async {
c = TicketDetailController(ipc: f.ipc, messages: f.services.messages);
f.services.messages.publish('builtin.tickets', 'load', {'id': 'T-1'});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c!.detail?.id, 'T-1');
});
test('a bare selection does NOT load (the nav re-emits as load)', () async {
c = TicketDetailController(ipc: f.ipc, messages: f.services.messages);
f.services.messages.publish('builtin.tickets', 'selection', {'id': 'T-9'});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(c!.detail, isNull);
});
});
@@ -78,9 +78,9 @@ void main() {
test('selection reveals + activates the static detail tab without churn', () async {
f.services.messages.publish('builtin.tickets', 'selection', {'id': 'T-1'});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
f.services.messages.publish('builtin.tickets', 'selection', {'id': 'T-2'});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'tickets.detail');
expect(f.services.panels.tabsFor(Slots.contextPanel).where((t) => t.id == 'tickets.detail').length, 1);
expect(f.services.arrangement.isVisible(Slots.contextPanel), isTrue);
+2 -2
View File
@@ -21,8 +21,8 @@ void main() {
bus.dispose();
});
// Bus delivery is async (broadcast stream), so settle a turn after publish.
Future<void> settle() => Future<void>.delayed(Duration.zero);
// Bus delivery is async (broadcast stream), so drain the queue after publish.
Future<void> settle() => pumpEventQueue();
test('returns null for an address that never reported', () {
expect(cache.get('decisions.panel'), isNull);
+2 -2
View File
@@ -74,7 +74,7 @@ void main() {
final sub = ring.changes.listen(events.add);
ring.add(_rec(LogLevel.info, 'x', 'a'));
ring.clear();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(events.length, 2);
await sub.cancel();
ring.dispose();
@@ -85,7 +85,7 @@ void main() {
final events = <void>[];
final sub = ring.changes.listen(events.add);
ring.clear();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(events, isEmpty);
await sub.cancel();
ring.dispose();
+1 -1
View File
@@ -30,7 +30,7 @@ void main() {
});
// Let the broadcast bus deliver.
Future<void> tick() => Future<void>.delayed(Duration.zero);
Future<void> tick() => pumpEventQueue();
test('starts empty', () {
expect(nav.current, isNull);
+4 -4
View File
@@ -12,7 +12,7 @@ void main() {
final events = <ClideEventEnvelope>[];
final sub = bus.stream.listen(events.add);
bus.emit(const ThemeChanged(themeName: 'summer-night'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(events, hasLength(1));
expect(events.first.event, isA<ThemeChanged>());
await sub.cancel();
@@ -26,7 +26,7 @@ void main() {
bus.emit(const ThemeChanged(themeName: 'a'));
bus.emit(const ExtensionActivated(id: 'builtin.git'));
bus.emit(const ThemeChanged(themeName: 'b'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(themes.map((e) => e.themeName), ['a', 'b']);
expect(extensions.map((e) => e.id), ['builtin.git']);
await s1.cancel();
@@ -39,7 +39,7 @@ void main() {
final s1 = bus.stream.listen((e) => a.add(e.event));
final s2 = bus.stream.listen((e) => b.add(e.event));
bus.emit(const ThemeChanged(themeName: 'x'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(a, hasLength(1));
expect(b, hasLength(1));
await s1.cancel();
@@ -57,7 +57,7 @@ void main() {
final sub = bus.stream.listen(capture.add);
final before = DateTime.now().toUtc();
bus.emit(const ThemeChanged(themeName: 'n'));
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
final after = DateTime.now().toUtc();
expect(capture, hasLength(1));
final ts = capture.first.timestamp;
+4 -4
View File
@@ -30,7 +30,7 @@ void main() {
final received = <Message>[];
final sub = bus.subscribe().listen(received.add);
bus.publish('git', 'status-changed', {'dirty': true});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(received, hasLength(1));
expect(received.first.publisher, 'git');
expect(received.first.channel, 'status-changed');
@@ -44,7 +44,7 @@ void main() {
bus.publish('git', 'a', const {});
bus.publish('pty', 'a', const {});
bus.publish('git', 'b', const {});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got.map((m) => m.channel), ['a', 'b']);
await sub.cancel();
});
@@ -55,7 +55,7 @@ void main() {
bus.publish('pty', 'output', const {});
bus.publish('pty', 'exit', const {});
bus.publish('git', 'output', const {});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got.map((m) => m.publisher), ['pty', 'git']);
await sub.cancel();
});
@@ -66,7 +66,7 @@ void main() {
bus.publish('git', 'status', const {});
bus.publish('git', 'other', const {});
bus.publish('pty', 'status', const {});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, hasLength(1));
await sub.cancel();
});
+2 -2
View File
@@ -175,9 +175,9 @@ void main() {
final s2 = f.services.events.on<ExtensionDeactivated>().listen((e) => deactivated.add(e.id));
f.services.extensions.register(_Ext(id: 'e'));
await f.services.extensions.activateAll();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
await f.services.extensions.deactivate('e');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(activated, ['e']);
expect(deactivated, ['e']);
await s1.cancel();
+1 -1
View File
@@ -46,7 +46,7 @@ void main() {
final sub = log.records.listen(out.add);
log.info('s', 'm1');
log.info('s', 'm2');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(out.map((r) => r.message), ['m1', 'm2']);
await sub.cancel();
await log.dispose();
+2 -2
View File
@@ -283,7 +283,7 @@ void main() {
addTearDown(n.dispose);
n.warn('clide CLI not on PATH', title: 'dogfood');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(toasts.entries, hasLength(1));
expect(toasts.entries.single.message, 'dogfood — clide CLI not on PATH');
@@ -299,7 +299,7 @@ void main() {
n.error('boom');
n.success('done');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(toasts.entries.map((e) => e.severity), [ToastSeverity.error, ToastSeverity.success]);
});
+3 -3
View File
@@ -24,7 +24,7 @@ void main() {
test('shows a toast for each message published to the toast channel', () async {
final (t, bus) = make();
publishToast(bus, 'builtin.git', 'Pushed to origin/main', severity: ToastSeverity.success, duration: Duration.zero);
await Future<void>.delayed(Duration.zero); // let the broadcast stream deliver
await pumpEventQueue(); // let the broadcast stream deliver
expect(t.entries.single.message, 'Pushed to origin/main');
expect(t.entries.single.severity, ToastSeverity.success);
});
@@ -33,7 +33,7 @@ void main() {
final (t, bus) = make();
bus.publish('x', toastChannel, {'severity': 'error'}); // no message
bus.publish('x', 'other-channel', {'message': 'nope'}); // wrong channel
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(t.entries, isEmpty);
});
@@ -41,7 +41,7 @@ void main() {
final (t, bus) = make();
bus.publish('x', toastChannel, {'message': 'a', 'severity': 'warning', 'durationMs': 0});
bus.publish('x', toastChannel, {'message': 'b', 'severity': 'bogus', 'durationMs': 0});
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(t.entries.map((e) => e.severity), [ToastSeverity.warning, ToastSeverity.info]);
});
});
+12 -12
View File
@@ -11,7 +11,7 @@ void main() {
v.add(2);
final got = <int>[];
v.stream.listen(got.add);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [2]);
});
@@ -19,10 +19,10 @@ void main() {
final v = ValueStream<int>();
final got = <int>[];
v.stream.listen(got.add);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, isEmpty);
v.add(7);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [7]);
});
@@ -32,7 +32,7 @@ void main() {
expect(v.value, isFalse);
final got = <bool>[];
v.stream.listen(got.add);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [false]);
});
@@ -42,7 +42,7 @@ void main() {
v.stream.listen(got.add);
v.add('a');
v.add('b');
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, ['a', 'b']);
});
@@ -53,7 +53,7 @@ void main() {
v.stream.listen(a.add);
v.stream.listen(b.add);
v.add(6);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(a, [5, 6]);
expect(b, [5, 6]);
});
@@ -71,7 +71,7 @@ void main() {
expect(v.valueOrNull, isNull);
final got = <String?>[];
v.stream.listen(got.add);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [null]);
});
@@ -81,14 +81,14 @@ void main() {
final done = <String>[];
v.stream.listen((_) {}, onDone: () => done.add('a'));
await v.close();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(done, ['a']);
expect(v.isClosed, isTrue);
final got = <int>[];
var closed = false;
v.stream.listen(got.add, onDone: () => closed = true);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [3], reason: 'the last value survives close for late readers');
expect(closed, isTrue);
});
@@ -105,13 +105,13 @@ void main() {
final got = <int>[];
final sub = v.stream.listen(got.add);
v.add(1);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
sub.pause();
v.add(2);
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [1]);
sub.resume();
await Future<void>.delayed(Duration.zero);
await pumpEventQueue();
expect(got, [1, 2]);
});
}