test(chat): add comprehensive unit tests
This commit is contained in:
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user