Replace the OpenRouter HTTP client with a DeepSeek client: - data/openrouter/ -> data/deepseek/; OpenRouterClient -> DeepSeekClient (+ DeepSeekCompletion / DeepSeek*Exception) - base URL api.deepseek.com, drop OpenRouter-only headers - DeepSeek has no strict json_schema: use response_format json_object and rely on the prompt for shape (ai_schema.dart removed) - secure-storage key openrouter_api_key -> deepseek_api_key - default model deepseek/deepseek-v4-flash -> deepseek-chat - l10n strings, test dart-defines (DEEPSEEK_API_KEY/_TEST_MODEL), docs Also bundles in-progress findBodyIgnoreRule (body-level ignore rules applied before the AI call) already present in the working tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
225 lines
5.9 KiB
Dart
225 lines
5.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/data/parser/rule_lookup.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/domain/entities/parse_rule.dart';
|
|
import 'package:new_budget/src/features/notification_parsing/domain/enums.dart';
|
|
|
|
ParseRule _rule({
|
|
required String id,
|
|
required String pattern,
|
|
MatchMode mode = MatchMode.contains,
|
|
ParseRuleKind kind = ParseRuleKind.merchantToCategory,
|
|
bool enabled = true,
|
|
int matchCount = 0,
|
|
int priority = 0,
|
|
String? categoryId = 'c1',
|
|
}) {
|
|
return ParseRule(
|
|
id: id,
|
|
userId: 'u1',
|
|
kind: kind,
|
|
matchMode: mode,
|
|
pattern: pattern,
|
|
enabled: enabled,
|
|
matchCount: matchCount,
|
|
priority: priority,
|
|
categoryId: categoryId,
|
|
createdAt: DateTime(2026, 1, 1),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
group('patternMatches', () {
|
|
test('contains is case-insensitive over body', () {
|
|
expect(
|
|
patternMatches('pyaterochka', MatchMode.contains,
|
|
body: 'Покупка PYATEROCHKA 1240'),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('exact matches normalized merchant only', () {
|
|
expect(
|
|
patternMatches('pyaterochka', MatchMode.exact,
|
|
body: 'long body', merchantRaw: 'PYATEROCHKA'),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
patternMatches('pyat', MatchMode.exact,
|
|
body: 'long body', merchantRaw: 'PYATEROCHKA'),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
test('regex match, invalid regex → false', () {
|
|
expect(patternMatches(r'\d{4}', MatchMode.regex, body: 'Карта 3456'),
|
|
isTrue);
|
|
expect(patternMatches('[invalid(', MatchMode.regex, body: 'x'), isFalse);
|
|
});
|
|
|
|
test('empty pattern → false', () {
|
|
expect(patternMatches(' ', MatchMode.contains, body: 'anything'),
|
|
isFalse);
|
|
});
|
|
});
|
|
|
|
group('findMerchantRule', () {
|
|
const body = 'Покупка PYATEROCHKA 1240';
|
|
|
|
test('returns matching enabled merchant rule', () {
|
|
final r = findMerchantRule(
|
|
[_rule(id: 'a', pattern: 'PYATEROCHKA')],
|
|
body: body,
|
|
);
|
|
expect(r?.id, 'a');
|
|
});
|
|
|
|
test('ignores disabled rules', () {
|
|
final r = findMerchantRule(
|
|
[_rule(id: 'a', pattern: 'PYATEROCHKA', enabled: false)],
|
|
body: body,
|
|
);
|
|
expect(r, isNull);
|
|
});
|
|
|
|
test('ignores ignore-kind rules', () {
|
|
final r = findMerchantRule(
|
|
[_rule(id: 'a', pattern: 'PYATEROCHKA', kind: ParseRuleKind.ignore)],
|
|
body: body,
|
|
);
|
|
expect(r, isNull);
|
|
});
|
|
|
|
test('conflict: longer pattern wins', () {
|
|
final r = findMerchantRule(
|
|
[
|
|
_rule(id: 'short', pattern: 'PYAT'),
|
|
_rule(id: 'long', pattern: 'PYATEROCHKA'),
|
|
],
|
|
body: body,
|
|
);
|
|
expect(r?.id, 'long');
|
|
});
|
|
|
|
test('conflict: equal length → higher matchCount wins', () {
|
|
final r = findMerchantRule(
|
|
[
|
|
_rule(id: 'a', pattern: 'PYATEROCHKA', matchCount: 1),
|
|
_rule(id: 'b', pattern: 'PYATEROCHKX', matchCount: 9),
|
|
],
|
|
body: 'PYATEROCHKA PYATEROCHKX',
|
|
);
|
|
expect(r?.id, 'b');
|
|
});
|
|
});
|
|
|
|
group('findSenderRule', () {
|
|
const body = 'Покупка PYATEROCHKA 1240';
|
|
|
|
ParseRule sender({
|
|
required String id,
|
|
required String pattern,
|
|
String? accountId = 'acc1',
|
|
bool enabled = true,
|
|
}) =>
|
|
_rule(
|
|
id: id,
|
|
pattern: pattern,
|
|
kind: ParseRuleKind.senderToAccount,
|
|
enabled: enabled,
|
|
categoryId: null,
|
|
).copyWith(accountId: accountId);
|
|
|
|
test('returns matching enabled sender rule', () {
|
|
final r = findSenderRule(
|
|
[sender(id: 's', pattern: 'PYATEROCHKA')],
|
|
body: body,
|
|
);
|
|
expect(r?.id, 's');
|
|
});
|
|
|
|
test('ignores rules without accountId', () {
|
|
final r = findSenderRule(
|
|
[sender(id: 's', pattern: 'PYATEROCHKA', accountId: null)],
|
|
body: body,
|
|
);
|
|
expect(r, isNull);
|
|
});
|
|
|
|
test('ignores merchant-kind rules', () {
|
|
final r = findSenderRule(
|
|
[_rule(id: 'm', pattern: 'PYATEROCHKA')],
|
|
body: body,
|
|
);
|
|
expect(r, isNull);
|
|
});
|
|
});
|
|
|
|
group('findIgnoreRule', () {
|
|
test('finds enabled ignore rule', () {
|
|
final r = findIgnoreRule(
|
|
[_rule(id: 'i', pattern: 'спам', kind: ParseRuleKind.ignore)],
|
|
body: 'это спам реклама',
|
|
);
|
|
expect(r?.id, 'i');
|
|
});
|
|
});
|
|
|
|
group('findBodyIgnoreRule', () {
|
|
test('matches contains ignore rule by body (pre-AI)', () {
|
|
final r = findBodyIgnoreRule(
|
|
[
|
|
_rule(
|
|
id: 'i',
|
|
pattern: 'Доставлен заказ',
|
|
kind: ParseRuleKind.ignore),
|
|
],
|
|
'Доставлен заказ №123',
|
|
);
|
|
expect(r?.id, 'i');
|
|
});
|
|
|
|
test('matches regex ignore rule by body', () {
|
|
final r = findBodyIgnoreRule(
|
|
[
|
|
_rule(
|
|
id: 'i',
|
|
pattern: r'заказ \d+',
|
|
mode: MatchMode.regex,
|
|
kind: ParseRuleKind.ignore),
|
|
],
|
|
'Доставлен заказ 123',
|
|
);
|
|
expect(r?.id, 'i');
|
|
});
|
|
|
|
test('skips exact ignore rules (need merchant, only known after AI)', () {
|
|
final r = findBodyIgnoreRule(
|
|
[
|
|
_rule(
|
|
id: 'i',
|
|
pattern: 'Доставлен заказ №123',
|
|
mode: MatchMode.exact,
|
|
kind: ParseRuleKind.ignore),
|
|
],
|
|
'Доставлен заказ №123',
|
|
);
|
|
expect(r, isNull);
|
|
});
|
|
|
|
test('skips disabled and non-ignore rules', () {
|
|
final r = findBodyIgnoreRule(
|
|
[
|
|
_rule(
|
|
id: 'a',
|
|
pattern: 'спам',
|
|
kind: ParseRuleKind.ignore,
|
|
enabled: false),
|
|
_rule(id: 'b', pattern: 'спам', kind: ParseRuleKind.merchantToCategory),
|
|
],
|
|
'это спам',
|
|
);
|
|
expect(r, isNull);
|
|
});
|
|
});
|
|
}
|