mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 18:57:05 +08:00
tests: improve chat viewmodel tests(cv: 70)
This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:apidash/dashbot/providers/providers.dart';
|
||||||
|
import 'package:apidash/dashbot/models/models.dart' show ChatMessage;
|
||||||
|
import 'package:apidash/dashbot/repository/repository.dart';
|
||||||
|
import 'package:apidash/dashbot/constants.dart';
|
||||||
|
import 'package:apidash/models/models.dart';
|
||||||
|
import 'package:apidash/dashbot/services/agent/prompt_builder.dart';
|
||||||
|
import 'package:apidash/providers/settings_providers.dart';
|
||||||
|
import 'package:apidash_core/apidash_core.dart';
|
||||||
|
import 'package:apidash/providers/collection_providers.dart';
|
||||||
|
import '../../../../providers/helpers.dart';
|
||||||
|
|
||||||
|
/// AI-enabled flow tests for ChatViewmodel.
|
||||||
|
///
|
||||||
|
/// This file contains tests specifically for AI-enabled chat functionality,
|
||||||
|
// A mock ChatRemoteRepository returning configurable responses
|
||||||
|
class MockChatRemoteRepository extends ChatRemoteRepository {
|
||||||
|
String? mockResponse;
|
||||||
|
Exception? mockError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> sendChat({required AIRequestModel request}) async {
|
||||||
|
if (mockError != null) throw mockError!;
|
||||||
|
return mockResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PromptCaptureBuilder extends PromptBuilder {
|
||||||
|
final PromptBuilder _inner;
|
||||||
|
_PromptCaptureBuilder(this._inner);
|
||||||
|
String? lastSystemPrompt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String buildSystemPrompt(RequestModel? req, ChatMessageType type,
|
||||||
|
{String? overrideLanguage, List<ChatMessage> history = const []}) {
|
||||||
|
final r = _inner.buildSystemPrompt(req, type,
|
||||||
|
overrideLanguage: overrideLanguage, history: history);
|
||||||
|
lastSystemPrompt = r;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? detectLanguage(String text) => _inner.detectLanguage(text);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getUserMessageForTask(ChatMessageType type) =>
|
||||||
|
_inner.getUserMessageForTask(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
late ProviderContainer container;
|
||||||
|
late MockChatRemoteRepository mockRepo;
|
||||||
|
late _PromptCaptureBuilder promptCapture;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await testSetUpTempDirForHive();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to obtain a default PromptBuilder by reading the real provider in a temp container
|
||||||
|
PromptBuilder basePromptBuilder() {
|
||||||
|
final temp = ProviderContainer();
|
||||||
|
final pb = temp.read(promptBuilderProvider);
|
||||||
|
temp.dispose();
|
||||||
|
return pb;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProviderContainer createTestContainer(
|
||||||
|
{String? aiExplanation, String? actionsJson}) {
|
||||||
|
mockRepo = MockChatRemoteRepository();
|
||||||
|
if (aiExplanation != null) {
|
||||||
|
// Build a response optionally with actions
|
||||||
|
final actionsPart = actionsJson ?? '[]';
|
||||||
|
mockRepo.mockResponse =
|
||||||
|
'{"explanation":"$aiExplanation","actions":$actionsPart}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proper AI model JSON matching AIRequestModel.fromJson keys
|
||||||
|
final aiModelJson = {
|
||||||
|
'modelApiProvider': 'openai',
|
||||||
|
'model': 'gpt-test',
|
||||||
|
'apiKey': 'sk-test',
|
||||||
|
'system_prompt': '',
|
||||||
|
'user_prompt': '',
|
||||||
|
'model_configs': [],
|
||||||
|
'stream': false,
|
||||||
|
};
|
||||||
|
|
||||||
|
final baseSettings = SettingsModel(defaultAIModel: aiModelJson);
|
||||||
|
promptCapture = _PromptCaptureBuilder(basePromptBuilder());
|
||||||
|
|
||||||
|
return createContainer(overrides: [
|
||||||
|
chatRepositoryProvider.overrideWithValue(mockRepo),
|
||||||
|
settingsProvider.overrideWith(
|
||||||
|
(ref) => ThemeStateNotifier(settingsModel: baseSettings)),
|
||||||
|
// Force no selected request so chat uses the stable 'global' session key
|
||||||
|
selectedRequestModelProvider.overrideWith((ref) => null),
|
||||||
|
promptBuilderProvider.overrideWith((ref) => promptCapture),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('ChatViewmodel AI Enabled Flow', () {
|
||||||
|
test('processes valid AI explanation + actions list', () async {
|
||||||
|
container = createTestContainer(
|
||||||
|
aiExplanation: 'Here is your code',
|
||||||
|
actionsJson:
|
||||||
|
'[{"action":"other","target":"code","field":"generated","value":"print(\\"hi\\")"}]',
|
||||||
|
);
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
|
||||||
|
await vm.sendMessage(
|
||||||
|
text: 'Generate code', type: ChatMessageType.generateCode);
|
||||||
|
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
// Expect exactly 2 messages: user + system response
|
||||||
|
expect(msgs.length, equals(2));
|
||||||
|
final user = msgs.first;
|
||||||
|
final system = msgs.last;
|
||||||
|
expect(user.role, MessageRole.user);
|
||||||
|
expect(system.role, MessageRole.system);
|
||||||
|
expect(system.actions, isNotNull);
|
||||||
|
expect(system.actions!.length, equals(1));
|
||||||
|
expect(system.content, contains('Here is your code'));
|
||||||
|
expect(promptCapture.lastSystemPrompt, isNotNull);
|
||||||
|
expect(promptCapture.lastSystemPrompt, contains('Generate'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles empty AI response (adds fallback message)', () async {
|
||||||
|
container = createTestContainer();
|
||||||
|
mockRepo.mockResponse = ''; // Explicit empty
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
|
||||||
|
await vm.sendMessage(
|
||||||
|
text: 'Explain', type: ChatMessageType.explainResponse);
|
||||||
|
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
expect(msgs, isNotEmpty);
|
||||||
|
expect(msgs.last.content, contains('No response'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles null AI response (adds fallback message)', () async {
|
||||||
|
container = createTestContainer();
|
||||||
|
mockRepo.mockResponse = null; // Explicit null
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
await vm.sendMessage(text: 'Debug', type: ChatMessageType.debugError);
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
expect(msgs, isNotEmpty);
|
||||||
|
expect(msgs.last.content, contains('No response'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles malformed actions field gracefully', () async {
|
||||||
|
container = createTestContainer();
|
||||||
|
mockRepo.mockResponse =
|
||||||
|
'{"explanation":"Something","actions":"not-a-list"}';
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
await vm.sendMessage(
|
||||||
|
text: 'Gen test', type: ChatMessageType.generateTest);
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
expect(msgs, isNotEmpty);
|
||||||
|
final sys = msgs.last;
|
||||||
|
expect(sys.content, contains('Something'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles malformed top-level JSON gracefully (no crash, fallback)',
|
||||||
|
() async {
|
||||||
|
container = createTestContainer();
|
||||||
|
// This will cause MessageJson.safeParse to catch and ignore malformed content
|
||||||
|
mockRepo.mockResponse =
|
||||||
|
'{"explanation":"ok","actions": [ { invalid json }';
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
await vm.sendMessage(
|
||||||
|
text: 'Gen code', type: ChatMessageType.generateCode);
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
expect(msgs.length, equals(2)); // user + system with raw content
|
||||||
|
expect(msgs.last.content, contains('explanation'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles missing explanation key (still stores raw response)',
|
||||||
|
() async {
|
||||||
|
container = createTestContainer();
|
||||||
|
mockRepo.mockResponse = '{"note":"Just a note","actions": []}';
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
await vm.sendMessage(
|
||||||
|
text: 'Explain', type: ChatMessageType.explainResponse);
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
expect(msgs.length, equals(2));
|
||||||
|
expect(msgs.last.content, contains('note'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('catches repository exception and appends error system message',
|
||||||
|
() async {
|
||||||
|
container = createTestContainer();
|
||||||
|
mockRepo.mockError = Exception('boom');
|
||||||
|
final vm = container.read(chatViewmodelProvider.notifier);
|
||||||
|
await vm.sendMessage(text: 'Doc', type: ChatMessageType.generateDoc);
|
||||||
|
final msgs = vm.currentMessages;
|
||||||
|
expect(msgs, isNotEmpty);
|
||||||
|
expect(msgs.last.content, contains('Error:'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user