import 'package:flutter_test/flutter_test.dart'; import 'package:social_app/features/chat/data/ai/ai_decision_engine.dart'; import 'package:social_app/features/chat/data/models/ag_ui_event.dart'; import 'package:social_app/features/chat/data/tools/tool_registry.dart'; import 'package:social_app/features/chat/data/services/ag_ui_service.dart'; class TestableAgUiService extends AgUiService { TestableAgUiService({super.onEvent}); @override Future sendMessage(String content) async { await mockEventStream(content); } Future mockEventStream(String content) async { final threadId = 'thread_${DateTime.now().millisecondsSinceEpoch}'; final runId = 'run_${DateTime.now().millisecondsSinceEpoch}'; final engine = AiDecisionEngine(); onEvent(RunStartedEvent(threadId: threadId, runId: runId)); final forceTrigger = engine.tryForceTrigger(content); if (forceTrigger != null) { await mockToolCallFlowWithArgs(forceTrigger.toolName, forceTrigger.args); } else if (engine.shouldTriggerToolCall(content)) { await mockToolCallFlow(content, engine); } final replies = generateReplies(content, engine); if (replies.isNotEmpty) { await mockTextMessageStream(replies); } onEvent(RunFinishedEvent(threadId: threadId, runId: runId)); } Future mockToolCallFlow(String content, AiDecisionEngine engine) async { final args = engine.getToolCallArgs(content); if (args == null) return; await mockToolCallFlowWithArgs('create_calendar_event', args); } Future mockToolCallFlowWithArgs( String toolName, Map args, ) async { final toolCallId = 'tc_${DateTime.now().millisecondsSinceEpoch}'; onEvent(ToolCallStartEvent(toolCallId: toolCallId, toolCallName: toolName)); onEvent(ToolCallArgsEvent(toolCallId: toolCallId, delta: '{}')); onEvent(ToolCallEndEvent(toolCallId: toolCallId)); final validation = ToolRegistry.validateArgs(toolName, args); if (!validation.ok) { onEvent( ToolCallErrorEvent( toolCallId: toolCallId, error: validation.error ?? 'Validation failed', code: 'VALIDATION_ERROR', ), ); return; } try { ToolRegistry.initialize(); final result = await ToolRegistry.execute(toolName, args); final messageId = 'msg_${DateTime.now().millisecondsSinceEpoch}'; onEvent( ToolCallResultEvent( messageId: messageId, toolCallId: toolCallId, result: result, ), ); } catch (e) { onEvent( ToolCallErrorEvent( toolCallId: toolCallId, error: e.toString(), code: 'EXECUTION_ERROR', ), ); } } List generateReplies(String content, AiDecisionEngine engine) { final intent = engine.matchIntent(content); switch (intent) { case Intent.createEvent: return ['好的,我已经为您创建了日程安排。']; case Intent.searchEvent: return ['您今天有以下日程:\n- 10:00 团队会议\n- 14:00 产品评审']; case Intent.unknown: return ['我理解了您的问题,让我来帮您处理。']; } } Future mockTextMessageStream(List replies) async { for (final reply in replies) { final messageId = 'msg_${DateTime.now().millisecondsSinceEpoch}'; onEvent(TextMessageStartEvent(messageId: messageId, role: 'assistant')); onEvent(TextMessageContentEvent(messageId: messageId, delta: reply)); onEvent(TextMessageEndEvent(messageId: messageId)); } } } void main() { late TestableAgUiService service; late List capturedEvents; setUp(() { capturedEvents = []; ToolRegistry.initialize(); service = TestableAgUiService( onEvent: (event) { capturedEvents.add(event); }, ); }); group('AgUiService', () { test('sendMessage first emits RunStartedEvent', () async { await service.sendMessage('你好'); expect(capturedEvents.first, isA()); }); test('sendMessage last emits RunFinishedEvent', () async { await service.sendMessage('你好'); expect(capturedEvents.last, isA()); }); test('sendMessage emits events in correct order', () async { await service.sendMessage('你好'); expect(capturedEvents.first, isA()); expect(capturedEvents.last, isA()); final types = capturedEvents.map((e) => e.type).toList(); expect(types.first, AgUiEventType.runStarted); expect(types.last, AgUiEventType.runFinished); }); test('creating schedule text triggers tool call events', () async { await service.sendMessage('提醒我明天10点开会'); final toolCallStarts = capturedEvents .whereType() .toList(); final toolCallEnds = capturedEvents .whereType() .toList(); final toolCallResults = capturedEvents .whereType() .toList(); expect(toolCallStarts.isNotEmpty, true); expect(toolCallEnds.isNotEmpty, true); expect(toolCallResults.isNotEmpty, true); expect(toolCallStarts.first.toolCallName, 'create_calendar_event'); }); test('force trigger with #tool syntax', () async { await service.sendMessage( '#tool:create_calendar_event {"title": "Test", "startAt": "2026-03-01T10:00:00Z"}', ); final toolCallStarts = capturedEvents .whereType() .toList(); expect(toolCallStarts.isNotEmpty, true); expect(toolCallStarts.first.toolCallName, 'create_calendar_event'); }); test('text message events are emitted for unknown intent', () async { await service.sendMessage('你好'); final textStarts = capturedEvents .whereType() .toList(); final textContents = capturedEvents .whereType() .toList(); final textEnds = capturedEvents.whereType().toList(); expect(textStarts.isNotEmpty, true); expect(textContents.isNotEmpty, true); expect(textEnds.isNotEmpty, true); }); test('search intent does not trigger tool calls', () async { await service.sendMessage('今天有什么日程'); final toolCallStarts = capturedEvents .whereType() .toList(); expect(toolCallStarts.isEmpty, true); }); test('tool call with invalid args emits error', () async { await service.sendMessage('#tool:create_calendar_event {}'); final toolCallErrors = capturedEvents .whereType() .toList(); expect(toolCallErrors.isNotEmpty, true); expect(toolCallErrors.first.error, contains('Missing required fields')); }); }); }