test(chat): add comprehensive unit tests

This commit is contained in:
qzl
2026-02-28 13:49:51 +08:00
parent dd90f48c6f
commit 92781ddbbe
6 changed files with 1177 additions and 0 deletions
@@ -0,0 +1,224 @@
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<void> sendMessage(String content) async {
await mockEventStream(content);
}
Future<void> 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<void> mockToolCallFlow(String content, AiDecisionEngine engine) async {
final args = engine.getToolCallArgs(content);
if (args == null) return;
await mockToolCallFlowWithArgs('create_calendar_event', args);
}
Future<void> mockToolCallFlowWithArgs(
String toolName,
Map<String, dynamic> 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<String> 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<void> mockTextMessageStream(List<String> 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<AgUiEvent> 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<RunStartedEvent>());
});
test('sendMessage last emits RunFinishedEvent', () async {
await service.sendMessage('你好');
expect(capturedEvents.last, isA<RunFinishedEvent>());
});
test('sendMessage emits events in correct order', () async {
await service.sendMessage('你好');
expect(capturedEvents.first, isA<RunStartedEvent>());
expect(capturedEvents.last, isA<RunFinishedEvent>());
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<ToolCallStartEvent>()
.toList();
final toolCallEnds = capturedEvents
.whereType<ToolCallEndEvent>()
.toList();
final toolCallResults = capturedEvents
.whereType<ToolCallResultEvent>()
.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<ToolCallStartEvent>()
.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<TextMessageStartEvent>()
.toList();
final textContents = capturedEvents
.whereType<TextMessageContentEvent>()
.toList();
final textEnds = capturedEvents.whereType<TextMessageEndEvent>().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<ToolCallStartEvent>()
.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<ToolCallErrorEvent>()
.toList();
expect(toolCallErrors.isNotEmpty, true);
expect(toolCallErrors.first.error, contains('Missing required fields'));
});
});
}