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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user