Files
BudgetApp/test/integration/ai_processing_integration_test.dart
T
2025-08-02 15:25:25 +03:00

440 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:budget_app/services/ai_transaction_processing_service.dart';
import 'package:budget_app/services/openrouter_ai_service.dart';
import 'package:budget_app/models/ai_rule.dart';
import 'package:budget_app/models/ai_exceptions.dart';
import '../test_config.dart';
import '../mocks/fake_repositories.dart';
import '../helpers/test_data.dart';
import '../helpers/test_objects.dart';
void main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
await TestConfig.initialize();
group('AI Processing Integration Tests', () {
late AiTransactionProcessingService service;
late FakeRepositories fakeRepositories;
setUp(() {
fakeRepositories = FakeRepositories();
fakeRepositories.loadTestData();
});
tearDown(() {
fakeRepositories.clearAll();
});
group('API Diagnostics', () {
test('should validate OpenRouter API connection', () async {
if (!TestConfig.shouldRunIntegrationTests()) {
print('🚫 Skipping API diagnostic: Integration tests disabled');
return;
}
if (!TestConfig.isAiConfigured()) {
print('🚫 Skipping API diagnostic: AI not configured');
return;
}
final aiSettings = TestConfig.getTestAiSettings();
final aiService = OpenRouterAiService(aiSettings);
print('🔍 API Diagnostic Test:');
print(' API Key: ${TestConfig.getTestApiKey()?.substring(0, 20)}...');
print(' Base URL: ${aiSettings.baseUrl}');
print(' Model: ${aiSettings.defaultModel}');
print(' Timeout: ${aiSettings.timeoutSeconds}s');
// Тест 1: Проверка health
print('🌡️ Testing API health...');
final isHealthy = await aiService.checkHealth();
print(' Health check result: ${isHealthy ? '✅ OK' : '❌ FAILED'}');
if (!isHealthy) {
fail('❌ OpenRouter API health check failed. Проверьте API ключ и сеть.');
}
// Тест 2: Простое сообщение
print('💬 Testing simple message...');
try {
final response = await aiService.sendMessage('Hello, this is a test message.');
print(' Simple message result: ✅ OK (${response.length} chars)');
if (response.trim().isEmpty) {
fail('❌ OpenRouter returned empty response for simple message');
}
// Проверяем что ответ не является error message
if (response.toLowerCase().contains('error') ||
response.toLowerCase().contains('not a json response')) {
fail('❌ OpenRouter returned error response: $response');
}
} catch (e) {
fail('❌ OpenRouter simple message test failed: $e');
}
print('✅ API diagnostic passed - OpenRouter is working correctly');
});
});
group('With Real AI Service', () {
bool shouldSkip = false;
setUp(() {
// Проверяем после инициализации dotenv
shouldSkip = !TestConfig.shouldRunIntegrationTests() || !TestConfig.isAiConfigured();
if (shouldSkip) {
return;
}
final aiSettings = TestConfig.getTestAiSettings();
final aiService = OpenRouterAiService(aiSettings);
service = AiTransactionProcessingService(
aiService: aiService,
prefilledRepository: fakeRepositories.prefilledTransactionRepository,
categoryRepository: fakeRepositories.categoryRepository,
aiRuleRepository: fakeRepositories.aiRuleRepository,
);
});
test('should process real bank transaction SMS', () async {
if (shouldSkip) {
print('Skipping integration test: AI not configured or tests disabled');
return;
}
// Act & Assert - с новыми exceptions тест будет падать автоматически при ошибках
print('🚀 Testing SMS processing: ${TestSmsData.bankTransactionSms}');
final result = await service.processSmsByAi(
TestSmsData.bankTransactionSms,
'integration_test_001',
);
// Если мы дошли сюда, значит AI успешно обработал SMS
expect(result.smsMessageId, equals('integration_test_001'));
print('✅ AI successfully processed bank transaction SMS');
if (result.amount != null) {
// Это транзакция
expect(result.amount, isNot(equals(0.0)));
expect(result.salesPoint, isNotNull);
expect(result.exclusionRegex, isNull);
print('✓ Processed as transaction: ${result.salesPoint}, ${result.amount}');
} else {
// Это не-транзакция
expect(result.exclusionRegex, isNotNull);
print('✓ Processed as non-transaction with regex: ${result.exclusionRegex}');
}
// Проверяем что сохранилось в репозиторий
final saved = fakeRepositories.prefilledTransactionRepository
.findBySmsId('integration_test_001');
expect(saved, isNotNull);
});
test('should process promotional SMS as non-transaction', () async {
if (shouldSkip) {
print('Skipping integration test: AI not configured or tests disabled');
return;
}
// Act - промо SMS может либо быть обработан, либо вызвать exception
print('🚀 Testing promotional SMS processing: ${TestSmsData.promotionalSms}');
try {
final result = await service.processSmsByAi(
TestSmsData.promotionalSms,
'integration_test_002',
);
// Если обработало успешно, проверяем результат
print('✓ AI processed promotional SMS');
if (result.amount == null) {
expect(result.exclusionRegex, isNotNull, reason: 'Non-transaction SMS should have exclusionRegex');
expect(result.salesPoint, isNull, reason: 'Non-transaction SMS should not have salesPoint');
print('✅ Correctly identified as non-transaction: ${result.exclusionRegex}');
} else {
print('⚠️ AI identified promotional SMS as transaction - this may need prompt tuning');
}
} catch (e) {
// Промо SMS может вызывать ошибки - это нормально
print('⚠️ Promotional SMS caused exception: $e');
print('️ This may be expected behavior if AI cannot determine SMS type');
}
});
test('should create AI rules from processed transactions', () async {
if (shouldSkip) {
print('Skipping integration test: AI not configured or tests disabled');
return;
}
// Arrange: Сначала обрабатываем SMS (с exceptions тест автоматически упадет при ошибках)
print('🚀 Testing rule creation from SMS: ${TestSmsData.onlinePaymentSms}');
final prefilled = await service.processSmsByAi(
TestSmsData.onlinePaymentSms,
'integration_test_003',
);
// Если мы дошли сюда, SMS был успешно обработан
print('✅ SMS processed successfully for rule creation');
// Act: Создаем правила на основе обработанной транзакции
if (prefilled.amount != null && prefilled.salesPoint != null) {
// Создаем правило для точки продаж
final pointOfSaleRule = await service.createAiRuleFromPrefilled(
prefilled,
AiRuleType.pointOfSale,
);
// Assert
expect(pointOfSaleRule, isNotNull);
expect(pointOfSaleRule!.type, equals(AiRuleType.pointOfSale));
expect(pointOfSaleRule.merchantPattern, isNotNull);
expect(pointOfSaleRule.categoryId, isNotNull);
print('✓ Created point of sale rule: ${pointOfSaleRule.name}');
// Проверяем что правило сохранилось
final savedRules = fakeRepositories.aiRuleRepository.getPointOfSaleRules();
expect(savedRules.length, greaterThan(2)); // Исходные + новое
}
if (prefilled.exclusionRegex != null) {
// Создаем правило пропуска
final skipRule = await service.createAiRuleFromPrefilled(
prefilled,
AiRuleType.skipTemplate,
);
// Assert
expect(skipRule, isNotNull);
expect(skipRule!.type, equals(AiRuleType.skipTemplate));
expect(skipRule.skipRegex, equals(prefilled.exclusionRegex));
print('✓ Created skip rule: ${skipRule.name}');
}
});
test('should handle multiple SMS in sequence', () async {
if (shouldSkip) {
print('Skipping integration test: AI not configured or tests disabled');
return;
}
final testSms = [
TestSmsData.atmWithdrawalSms,
TestSmsData.bankAdvertisingSms,
TestSmsData.securitySms,
];
final results = <String, dynamic>{};
final errors = <String, String>{};
int successCount = 0;
print('🚀 Testing batch SMS processing (${testSms.length} messages)...');
// Act: Обрабатываем несколько SMS подряд с timeout
for (int i = 0; i < testSms.length; i++) {
final smsId = 'batch_test_${i + 1}';
final sms = testSms[i];
print('🔄 Starting iteration $i/${testSms.length - 1} for $smsId');
print('💬 Processing $smsId: ${sms.substring(0, 50)}...');
try {
// С новыми exceptions - либо успех, либо exception
final result = await service.processSmsByAi(sms, smsId)
.timeout(Duration(seconds: 45));
// Если мы дошли сюда - это успех
results[smsId] = result;
successCount++;
print('✅ $smsId: Success');
} catch (e) {
// Любая ошибка теперь является exception
errors[smsId] = e.toString();
results[smsId] = null;
print('❌ $smsId: Exception - ${e.runtimeType}: $e');
// Проверяем тип ошибки
if (e.toString().contains('OpenRouterApiException') ||
e.toString().contains('AiConfigurationException')) {
print('⚠️ Critical API error detected - this indicates API/config issues');
}
}
print('✅ Iteration $i completed, moving to next');
}
print('🎯 Loop completed - analyzing results...');
// Assert: Анализируем результаты
int transactionCount = 0;
int nonTransactionCount = 0;
int failureCount = errors.length;
for (final entry in results.entries) {
final result = entry.value;
if (result != null) {
if (result.amount != null) {
transactionCount++;
print('✓ ${entry.key}: Transaction (${result.salesPoint})');
} else {
nonTransactionCount++;
print('✓ ${entry.key}: Non-transaction');
}
}
}
// Показываем детали ошибок
if (errors.isNotEmpty) {
print('❌ Failed SMS processing errors:');
for (final entry in errors.entries) {
print(' - ${entry.key}: ${entry.value}');
}
}
print('📊 Batch processing summary:');
print(' ✅ Transactions: $transactionCount');
print(' 🚫 Non-transactions: $nonTransactionCount');
print(' ❌ Failures: $failureCount');
print(' 📈 Success count: $successCount/${testSms.length}');
print(' 🏆 Success rate: ${((successCount / testSms.length) * 100).toInt()}%');
// Новая логика: если все SMS падают с API ошибками - это проблема с API
if (successCount == 0) {
// Проверяем типы ошибок
final hasApiErrors = errors.values.any((error) =>
error.contains('OpenRouterApiException') ||
error.contains('AiConfigurationException'));
if (hasApiErrors) {
fail('❌ Критическая проблема: все SMS падают с API ошибками. '
'Проверьте OpenRouter API ключ, модель и сеть. Errors: $errors');
} else {
fail('❌ Ни одно SMS не было обработано успешно. Errors: $errors');
}
}
// Мягкая проверка: минимум 1 SMS должен быть обработан успешно
if (successCount >= 1) {
print('✅ At least one SMS processed successfully - test passed');
}
// Проверяем что результаты сохранились
final allPrefilled = await fakeRepositories.prefilledTransactionRepository.getAll();
final expectedMinCount = successCount + 2; // Исходные + новые успешные
expect(allPrefilled.length, greaterThanOrEqualTo(expectedMinCount),
reason: 'Repository should contain at least $expectedMinCount records');
print('✅ Batch test passed: ${allPrefilled.length} records in repository');
});
});
group('Performance Tests', () {
bool shouldSkip = false;
setUp(() {
shouldSkip = !TestConfig.shouldRunIntegrationTests() || !TestConfig.isAiConfigured();
if (shouldSkip) {
return;
}
final aiSettings = TestConfig.getTestAiSettings();
final aiService = OpenRouterAiService(aiSettings);
service = AiTransactionProcessingService(
aiService: aiService,
prefilledRepository: fakeRepositories.prefilledTransactionRepository,
categoryRepository: fakeRepositories.categoryRepository,
aiRuleRepository: fakeRepositories.aiRuleRepository,
);
});
test('should handle batch processing within reasonable time', () async {
if (shouldSkip) {
print('Skipping performance test: AI not configured or tests disabled');
return;
}
final stopwatch = Stopwatch()..start();
// Act: Обрабатываем несколько SMS
final futures = <Future>[];
for (int i = 0; i < 3; i++) {
futures.add(service.processSmsByAi(
TestSmsData.allTransactionSms[i % TestSmsData.allTransactionSms.length],
'perf_test_$i',
));
}
await Future.wait(futures);
stopwatch.stop();
// Assert: Проверяем время выполнения
final elapsed = stopwatch.elapsedMilliseconds;
print('Batch processing took: ${elapsed}ms');
// Разумное время: меньше 30 секунд для 3 запросов
expect(elapsed, lessThan(30000),
reason: 'Batch processing should complete within 30 seconds');
});
});
group('Error Handling Integration', () {
test('should gracefully handle AI service errors', () async {
if (!TestConfig.shouldRunIntegrationTests()) {
print('Skipping error handling test: Integration tests disabled');
return;
}
// Создаем сервис с неправильным API ключом
final badAiSettings = TestConfig.getTestAiSettings()
..apiKey = 'invalid_api_key_123';
final badAiService = OpenRouterAiService(badAiSettings);
service = AiTransactionProcessingService(
aiService: badAiService,
prefilledRepository: fakeRepositories.prefilledTransactionRepository,
categoryRepository: fakeRepositories.categoryRepository,
aiRuleRepository: fakeRepositories.aiRuleRepository,
);
// Act & Assert
expect(
() async => await service.processSmsByAi(
TestSmsData.bankTransactionSms,
'error_test_001',
),
throwsA(isA<AiException>()),
reason: 'Должно выбрасываться исключение AiException при ошибке сервиса',
);
print('✓ Gracefully handled AI service error by throwing exception');
// Проверяем что ничего не сохранилось
final saved = fakeRepositories.prefilledTransactionRepository
.findBySmsId('error_test_001');
expect(saved, isNull);
});
});
});
}
/// Helper для вывода статистики тестов
void printTestStats(FakeRepositories repositories) {
final stats = repositories.getStats();
print('Test repositories stats:');
for (final entry in stats.entries) {
print(' ${entry.key}: ${entry.value}');
}
}