diff --git a/test/builtin/claude/extension_commands_test.dart b/test/builtin/claude/extension_commands_test.dart index e3fd2091..a3b84a80 100644 --- a/test/builtin/claude/extension_commands_test.dart +++ b/test/builtin/claude/extension_commands_test.dart @@ -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.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.delayed(Duration.zero); + await pumpEventQueue(); f.services.events.emit(const ProjectOpened(path: '/repo-one')); - await Future.delayed(Duration.zero); + await pumpEventQueue(); f.services.events.emit(const ProjectOpened(path: '/repo-two')); - await Future.delayed(Duration.zero); + await pumpEventQueue(); // No live sessions in this fixture — the sweep runs over an empty set. expect(activeSessionOrchestrator!.sessions, isEmpty); }); diff --git a/test/builtin/claude/session_lifecycle_test.dart b/test/builtin/claude/session_lifecycle_test.dart index bda90daf..322e5796 100644 --- a/test/builtin/claude/session_lifecycle_test.dart +++ b/test/builtin/claude/session_lifecycle_test.dart @@ -230,7 +230,7 @@ void main() { } expect(orch.sessions, isEmpty); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(created.single.killed, isTrue); }); @@ -247,7 +247,7 @@ void main() { } expect(orch.sessions, isEmpty); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(created.every((p) => p.killed), isTrue); }); diff --git a/test/builtin/claude/session_orchestrator_test.dart b/test/builtin/claude/session_orchestrator_test.dart index e2eaac57..631a1556 100644 --- a/test/builtin/claude/session_orchestrator_test.dart +++ b/test/builtin/claude/session_orchestrator_test.dart @@ -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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); final tyreProc = created[1]; expect(tyreProc.writes.any((w) => w.contains('[team] lead: pick up T-9')), isTrue); }); diff --git a/test/builtin/claude/stream_json_session_test.dart b/test/builtin/claude/stream_json_session_test.dart index 5ff5b500..f3711998 100644 --- a/test/builtin/claude/stream_json_session_test.dart +++ b/test/builtin/claude/stream_json_session_test.dart @@ -210,7 +210,7 @@ void main() { }, }), ); - await Future.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.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.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 = []; session.modelErrors.listen(errors.add); proc.emit(initEvent()); // model: claude-opus-4-7 - await Future.delayed(Duration.zero); + await pumpEventQueue(); session.setModel('bogus-model'); - await Future.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(items, hasLength(2)); expect(items[0], isA()); @@ -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.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.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.delayed(Duration.zero); + await pumpEventQueue(); final late = []; session.statusStream.listen(late.add); - await Future.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 = []; 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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(statuses.last.cost, closeTo(0.042, 1e-9)); }); @@ -344,7 +344,7 @@ void main() { }, ), ); - await Future.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.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': {}})); - await Future.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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); final parts = items.whereType().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.delayed(Duration.zero); + await pumpEventQueue(); final parts = items.whereType().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.delayed(Duration.zero); + await pumpEventQueue(); final tool = items.whereType().single; expect(tool.uuid, isNot('partial-msg-3')); expect(items.last, isA()); @@ -438,11 +438,11 @@ void main() { proc.emit(streamTextDelta('first')); proc.emit(streamMessageStop()); proc.emit(jsonEncode({'type': 'result', 'result': '', 'usage': {}})); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); final parts = items.whereType().toList(); expect(parts.last.text, 'second'); }); @@ -452,14 +452,14 @@ void main() { proc.emit(''); proc.emit('not json'); proc.emit(' '); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); expect(proc.writes, hasLength(1)); final sent = jsonDecode(proc.writes.single) as Map; @@ -484,7 +484,7 @@ void main() { }, }), ); - await Future.delayed(Duration.zero); + await pumpEventQueue(); final u = items.whereType().single; expect(u.injected, isTrue); }); @@ -501,7 +501,7 @@ void main() { }, }), ); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(items.whereType().single.injected, isFalse); }); @@ -509,7 +509,7 @@ void main() { final emitted = []; session.pendingPromptStream.listen(emitted.add); proc.emit(canUseTool('req-1')); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); final p = session.pendingPrompt!; session.resolvePrompt(p.promptId, AllowTool(p.input)); @@ -557,13 +557,13 @@ void main() { }, }), ); - await Future.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.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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(statuses.last.permissionMode, 'plan'); proc.emit(canUseTool('exit-1', tool: 'ExitPlanMode', input: {'plan': 'do the thing'})); - await Future.delayed(Duration.zero); + await pumpEventQueue(); final p = session.pendingPrompt!; expect(p.toolName, 'ExitPlanMode'); session.resolvePrompt(p.promptId, AllowTool(p.input)); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); proc.emit(canUseTool('exit-2', tool: 'ExitPlanMode', input: {'plan': 'x'})); - await Future.delayed(Duration.zero); + await pumpEventQueue(); session.resolvePrompt(session.pendingPrompt!.promptId, const DenyTool('keep planning')); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); proc.emit(canUseTool('w1')); // a Write - await Future.delayed(Duration.zero); + await pumpEventQueue(); session.resolvePrompt(session.pendingPrompt!.promptId, AllowTool(const {})); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); session.noteEffort('xhigh'); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); final notice = items.whereType().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.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.delayed(Duration.zero); + await pumpEventQueue(); session.resolvePrompt( 'aq', AllowTool(const { 'answers': {'Pet': 'Dogs'}, }), ); - await Future.delayed(Duration.zero); + await pumpEventQueue(); final echo = items.whereType().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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(statuses.last.permissionMode, 'plan'); session.setPermissionMode('acceptEdits'); - await Future.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.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.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.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.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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); final r = mcpResponseOf(mproc.writes.last); expect(r['result'], isA()); }); 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.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 = []; session.endedStream.listen(ends.add); session.send('do something'); - await Future.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.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 = []; session.pendingPromptStream.listen(pendings.add); proc.emit(canUseTool('p1')); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(session.pendingPrompt, isNotNull); proc.exit.complete(1); - await Future.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.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.delayed(Duration.zero); + await pumpEventQueue(); final run = session.workflows['toolu_wf']; expect(run, isNotNull); @@ -994,7 +994,7 @@ void main() { final items = []; session.items.listen(items.add); p.emit(jsonEncode({'type': 'system', 'subtype': 'task_progress', 'tool_use_id': 'toolu_wf', 'workflow_progress': const []})); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(items, isEmpty); expect(session.workflows.containsKey('toolu_wf'), isTrue); }); diff --git a/test/builtin/claude/team_broker_test.dart b/test/builtin/claude/team_broker_test.dart index dd973450..edc03fd2 100644 --- a/test/builtin/claude/team_broker_test.dart +++ b/test/builtin/claude/team_broker_test.dart @@ -168,7 +168,7 @@ void main() { final events = []; final sub = broker.changes.listen((_) => events.add(null)); broker.removeMember('teammate:tyre'); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); expect(done, isTrue); }); }); @@ -223,7 +223,7 @@ void main() { final events = []; final sub = broker.changes.listen((_) => events.add(null)); broker.reassignTask(id, 'primary'); - await Future.delayed(Duration.zero); // let the broadcast event deliver + await pumpEventQueue(); // let the broadcast event deliver await sub.cancel(); expect(events, hasLength(1)); }); diff --git a/test/builtin/claude/team_chat_model_test.dart b/test/builtin/claude/team_chat_model_test.dart index 668f4754..0f9bb3d7 100644 --- a/test/builtin/claude/team_chat_model_test.dart +++ b/test/builtin/claude/team_chat_model_test.dart @@ -40,7 +40,7 @@ void main() { final events = []; final sub = model.changes.listen((_) => events.add(null)); broker.sendMessage('primary', 'tyre', 'hello tyre'); - await Future.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.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.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.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.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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(resolvedName, 'tyre'); interruptModel.dispose(); }); @@ -163,7 +163,7 @@ void main() { }, ); interruptModel.postAsUser('abort all', interrupt: true); - await Future.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(done, isTrue); }); } diff --git a/test/builtin/claude/ticket_pick_up_test.dart b/test/builtin/claude/ticket_pick_up_test.dart index 250f949f..713945ad 100644 --- a/test/builtin/claude/ticket_pick_up_test.dart +++ b/test/builtin/claude/ticket_pick_up_test.dart @@ -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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(accepted, isFalse); expect(statusCalls, isEmpty); @@ -87,7 +87,7 @@ void main() { ipc: ipc, messages: messages, ); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(accepted, isTrue); // prompt still delivered expect(statusCalls, isEmpty); // but no transition diff --git a/test/builtin/decisions/decision_reader_test.dart b/test/builtin/decisions/decision_reader_test.dart index 387a6916..29212c9a 100644 --- a/test/builtin/decisions/decision_reader_test.dart +++ b/test/builtin/decisions/decision_reader_test.dart @@ -97,9 +97,9 @@ void main() { test('decisions.detail tab count stays at 1 after multiple selections', () async { _select(f, 'D-1'); - await Future.delayed(Duration.zero); + await pumpEventQueue(); _select(f, 'D-2'); - await Future.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.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.delayed(Duration.zero); + await pumpEventQueue(); _select(f, 'D-2'); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); _select(f, 'D-5'); - await Future.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); // Then send a bad message. f.services.messages.publish('builtin.decisions', 'selection', {'id': null}); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); expect( f.services.panels.activeTabIn(Slots.contextPanel), diff --git a/test/builtin/diff/diff_controller_test.dart b/test/builtin/diff/diff_controller_test.dart index b955ebb0..20a59777 100644 --- a/test/builtin/diff/diff_controller_test.dart +++ b/test/builtin/diff/diff_controller_test.dart @@ -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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(diffCalls, greaterThan(before)); }); diff --git a/test/builtin/editor/editor_extension_test.dart b/test/builtin/editor/editor_extension_test.dart index b94e26c3..ebc9942e 100644 --- a/test/builtin/editor/editor_extension_test.dart +++ b/test/builtin/editor/editor_extension_test.dart @@ -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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(f.services.arrangement.editorOpen, isTrue); emitEditor('editor.active-changed', id: null); - await Future.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(f.services.arrangement.editorOpen, isFalse); }); } diff --git a/test/builtin/files/file_tree_controller_test.dart b/test/builtin/files/file_tree_controller_test.dart index 50bb79fc..492c7fd9 100644 --- a/test/builtin/files/file_tree_controller_test.dart +++ b/test/builtin/files/file_tree_controller_test.dart @@ -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.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.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.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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); // Test passes if no exception. }); }); diff --git a/test/builtin/git/git_controller_test.dart b/test/builtin/git/git_controller_test.dart index 27a09403..731cb1bd 100644 --- a/test/builtin/git/git_controller_test.dart +++ b/test/builtin/git/git_controller_test.dart @@ -33,7 +33,7 @@ void main() { ); // Let the broadcast streams (bus / events) deliver. - Future settle() => Future.delayed(Duration.zero); + Future settle() => pumpEventQueue(); group('load + status parsing', () { test('hydrates branch / counts / file lists from git.status', () async { diff --git a/test/builtin/menubar/menu_bar_test.dart b/test/builtin/menubar/menu_bar_test.dart index ae228f3c..972ea07c 100644 --- a/test/builtin/menubar/menu_bar_test.dart +++ b/test/builtin/menubar/menu_bar_test.dart @@ -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); }); diff --git a/test/builtin/pql/pql_controller_test.dart b/test/builtin/pql/pql_controller_test.dart index 5916df10..c0bbed12 100644 --- a/test/builtin/pql/pql_controller_test.dart +++ b/test/builtin/pql/pql_controller_test.dart @@ -120,7 +120,7 @@ void main() { }); c.switchView(PqlView.markdown); expect(c.view, PqlView.markdown); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(filesCalls, 1); // Switching to the same view is a no-op. c.switchView(PqlView.markdown); diff --git a/test/builtin/search/find_in_files_controller_test.dart b/test/builtin/search/find_in_files_controller_test.dart index 95c6f0a6..3dfdd7e5 100644 --- a/test/builtin/search/find_in_files_controller_test.dart +++ b/test/builtin/search/find_in_files_controller_test.dart @@ -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.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.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.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(sent!['path'], 'a.dart'); expect(sent!['line'], 7); }); diff --git a/test/builtin/tickets/ticket_detail_test.dart b/test/builtin/tickets/ticket_detail_test.dart index 23d262ff..f2f39dc5 100644 --- a/test/builtin/tickets/ticket_detail_test.dart +++ b/test/builtin/tickets/ticket_detail_test.dart @@ -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.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.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.delayed(Duration.zero); + await pumpEventQueue(); f.services.messages.publish('builtin.tickets', 'selection', {'id': 'T-2'}); - await Future.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); diff --git a/test/kernel/filter_state_test.dart b/test/kernel/filter_state_test.dart index 056f06c1..15ccce9b 100644 --- a/test/kernel/filter_state_test.dart +++ b/test/kernel/filter_state_test.dart @@ -21,8 +21,8 @@ void main() { bus.dispose(); }); - // Bus delivery is async (broadcast stream), so settle a turn after publish. - Future settle() => Future.delayed(Duration.zero); + // Bus delivery is async (broadcast stream), so drain the queue after publish. + Future settle() => pumpEventQueue(); test('returns null for an address that never reported', () { expect(cache.get('decisions.panel'), isNull); diff --git a/test/kernel/log_ring_test.dart b/test/kernel/log_ring_test.dart index e5655851..0ef4cdec 100644 --- a/test/kernel/log_ring_test.dart +++ b/test/kernel/log_ring_test.dart @@ -74,7 +74,7 @@ void main() { final sub = ring.changes.listen(events.add); ring.add(_rec(LogLevel.info, 'x', 'a')); ring.clear(); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(events.length, 2); await sub.cancel(); ring.dispose(); @@ -85,7 +85,7 @@ void main() { final events = []; final sub = ring.changes.listen(events.add); ring.clear(); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(events, isEmpty); await sub.cancel(); ring.dispose(); diff --git a/test/kernel/reader_nav_test.dart b/test/kernel/reader_nav_test.dart index 0c739674..2926f44c 100644 --- a/test/kernel/reader_nav_test.dart +++ b/test/kernel/reader_nav_test.dart @@ -30,7 +30,7 @@ void main() { }); // Let the broadcast bus deliver. - Future tick() => Future.delayed(Duration.zero); + Future tick() => pumpEventQueue(); test('starts empty', () { expect(nav.current, isNull); diff --git a/test/kernel/src/events/bus_test.dart b/test/kernel/src/events/bus_test.dart index d40561be..86c559d0 100644 --- a/test/kernel/src/events/bus_test.dart +++ b/test/kernel/src/events/bus_test.dart @@ -12,7 +12,7 @@ void main() { final events = []; final sub = bus.stream.listen(events.add); bus.emit(const ThemeChanged(themeName: 'summer-night')); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(events, hasLength(1)); expect(events.first.event, isA()); 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.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.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.delayed(Duration.zero); + await pumpEventQueue(); final after = DateTime.now().toUtc(); expect(capture, hasLength(1)); final ts = capture.first.timestamp; diff --git a/test/kernel/src/events/message_bus_test.dart b/test/kernel/src/events/message_bus_test.dart index e360a3e0..144f97b3 100644 --- a/test/kernel/src/events/message_bus_test.dart +++ b/test/kernel/src/events/message_bus_test.dart @@ -30,7 +30,7 @@ void main() { final received = []; final sub = bus.subscribe().listen(received.add); bus.publish('git', 'status-changed', {'dirty': true}); - await Future.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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(got, hasLength(1)); await sub.cancel(); }); diff --git a/test/kernel/src/extensions_manager_test.dart b/test/kernel/src/extensions_manager_test.dart index f724dddc..3d6a5e74 100644 --- a/test/kernel/src/extensions_manager_test.dart +++ b/test/kernel/src/extensions_manager_test.dart @@ -175,9 +175,9 @@ void main() { final s2 = f.services.events.on().listen((e) => deactivated.add(e.id)); f.services.extensions.register(_Ext(id: 'e')); await f.services.extensions.activateAll(); - await Future.delayed(Duration.zero); + await pumpEventQueue(); await f.services.extensions.deactivate('e'); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(activated, ['e']); expect(deactivated, ['e']); await s1.cancel(); diff --git a/test/kernel/src/log_test.dart b/test/kernel/src/log_test.dart index ebd42f2c..db80cd89 100644 --- a/test/kernel/src/log_test.dart +++ b/test/kernel/src/log_test.dart @@ -46,7 +46,7 @@ void main() { final sub = log.records.listen(out.add); log.info('s', 'm1'); log.info('s', 'm2'); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(out.map((r) => r.message), ['m1', 'm2']); await sub.cancel(); await log.dispose(); diff --git a/test/kernel/src/services_stubs_test.dart b/test/kernel/src/services_stubs_test.dart index 3d87f30a..e58b2365 100644 --- a/test/kernel/src/services_stubs_test.dart +++ b/test/kernel/src/services_stubs_test.dart @@ -283,7 +283,7 @@ void main() { addTearDown(n.dispose); n.warn('clide CLI not on PATH', title: 'dogfood'); - await Future.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.delayed(Duration.zero); + await pumpEventQueue(); expect(toasts.entries.map((e) => e.severity), [ToastSeverity.error, ToastSeverity.success]); }); diff --git a/test/kernel/src/toast_test.dart b/test/kernel/src/toast_test.dart index 9e60954b..16cbcd6a 100644 --- a/test/kernel/src/toast_test.dart +++ b/test/kernel/src/toast_test.dart @@ -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.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.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.delayed(Duration.zero); + await pumpEventQueue(); expect(t.entries.map((e) => e.severity), [ToastSeverity.warning, ToastSeverity.info]); }); }); diff --git a/test/util/value_stream_test.dart b/test/util/value_stream_test.dart index 1a8f9dec..db8e68e8 100644 --- a/test/util/value_stream_test.dart +++ b/test/util/value_stream_test.dart @@ -11,7 +11,7 @@ void main() { v.add(2); final got = []; v.stream.listen(got.add); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(got, [2]); }); @@ -19,10 +19,10 @@ void main() { final v = ValueStream(); final got = []; v.stream.listen(got.add); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(got, isEmpty); v.add(7); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(got, [7]); }); @@ -32,7 +32,7 @@ void main() { expect(v.value, isFalse); final got = []; v.stream.listen(got.add); - await Future.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.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.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 = []; v.stream.listen(got.add); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(got, [null]); }); @@ -81,14 +81,14 @@ void main() { final done = []; v.stream.listen((_) {}, onDone: () => done.add('a')); await v.close(); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(done, ['a']); expect(v.isClosed, isTrue); final got = []; var closed = false; v.stream.listen(got.add, onDone: () => closed = true); - await Future.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 = []; final sub = v.stream.listen(got.add); v.add(1); - await Future.delayed(Duration.zero); + await pumpEventQueue(); sub.pause(); v.add(2); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(got, [1]); sub.resume(); - await Future.delayed(Duration.zero); + await pumpEventQueue(); expect(got, [1, 2]); }); }