Files
OnBudget/test/core/database/migration_v3_test.dart
T
SandersandClaude Opus 4.8 eed9164150 Add CompanionDevice watch pairing and store raw AI responses
Two fixes for the notification-parsing pipeline:

- CompanionDeviceManager pairing (DEVICE_PROFILE_WATCH -> COMPANION_DEVICE_WATCH
  role -> RECEIVE_SENSITIVE_NOTIFICATIONS) via a new platform channel in
  MainActivity.kt, companion_device_channel.dart and companion_access_controller,
  surfaced as a card in parsing settings. Lifts the system "Confidential"
  redaction that hid VTB/T-Bank notification text from the listener.

- raw_messages.ai_response: the model's raw completion content is now persisted
  (schema v4 + migration) and shown in the parsing log detail panel. Unlike
  draftJson it survives inbox sweeps and is filled even for partial/ignored
  outcomes.

flutter analyze: no issues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:04:39 +03:00

147 lines
5.3 KiB
Dart

import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:new_budget/src/core/database/app_database.dart';
import 'package:new_budget/src/core/database/converters/enum_converters.dart';
import 'package:new_budget/src/features/notification_parsing/domain/enums.dart';
/// Миграция v2 → v3: единая модель правил (см. `AppDatabase._migrateV2ToV3`) —
/// колонка `kind` дропается table rewrite'ом, появляются `is_ignore` и
/// `auto_apply`; kind='ignore' → is_ignore=1, остальные данные целы.
///
/// Сценарий: raw-схема v2 + по правилу каждого старого kind заливаются в
/// `setup:` (до drift), `PRAGMA user_version = 2` заставляет drift выполнить
/// onUpgrade при открытии.
const _userId = 'u1';
const _sber = 'ru.sberbankmobile';
void _createV2Schema(dynamic raw) {
raw.execute('''
CREATE TABLE source_apps (
id TEXT NOT NULL PRIMARY KEY,
user_id TEXT NOT NULL,
package_name TEXT NOT NULL,
display_name TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
self_merchant INTEGER NOT NULL DEFAULT 0,
default_account_id TEXT,
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
UNIQUE (user_id, package_name)
);''');
raw.execute('''
CREATE TABLE parse_rules (
id TEXT NOT NULL PRIMARY KEY,
user_id TEXT NOT NULL,
package_name TEXT,
kind TEXT NOT NULL,
match_mode TEXT NOT NULL DEFAULT 'contains',
pattern TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
match_count INTEGER NOT NULL DEFAULT 0,
weight INTEGER NOT NULL DEFAULT 1,
last_match_at INTEGER,
tx_type TEXT,
merchant_canonical TEXT,
category_id TEXT,
account_id TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
);''');
// По правилу каждого старого kind.
raw.execute(
'INSERT INTO parse_rules '
'(id, user_id, package_name, kind, match_mode, pattern, priority, '
'match_count, tx_type, merchant_canonical, category_id, account_id, '
'enabled) VALUES '
"('r-merchant', '$_userId', '$_sber', 'merchantToCategory', 'contains', "
"'PYATEROCHKA', 1, 5, 'expense', 'Пятёрочка', 'cat-1', NULL, 1), "
"('r-sender', '$_userId', '$_sber', 'senderToAccount', 'contains', "
"'*3456', 0, 2, NULL, NULL, NULL, 'acc-1', 1), "
"('r-ignore', '$_userId', '$_sber', 'ignore', 'regex', "
"'заказ \\d+', 0, 0, NULL, NULL, NULL, NULL, 0)");
// Хвост цепочки (v3→v4) добавляет колонку в raw_messages — в реальной
// v2-БД таблица есть, здесь достаточно её минимальной схемы.
raw.execute('''
CREATE TABLE raw_messages (
id TEXT NOT NULL PRIMARY KEY,
user_id TEXT NOT NULL,
package_name TEXT NOT NULL,
title TEXT,
body TEXT NOT NULL,
received_at INTEGER NOT NULL,
dedup_hash TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
parse_attempt_count INTEGER NOT NULL DEFAULT 0,
last_parse_error TEXT,
draft_json TEXT,
diagnostics TEXT,
confidence_amount INTEGER,
confidence_account INTEGER,
confidence_type INTEGER,
confidence_merchant INTEGER,
confidence_category INTEGER,
transaction_id TEXT,
paired_with_id TEXT,
pair_deadline INTEGER,
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
);''');
raw.execute('PRAGMA user_version = 2');
}
void main() {
late AppDatabase db;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory(setup: _createV2Schema));
});
tearDown(() => db.close());
test('колонка kind исчезла из parse_rules', () async {
final cols = await db
.customSelect("PRAGMA table_info('parse_rules')")
.get();
final names = cols.map((r) => r.read<String>('name')).toSet();
expect(names, isNot(contains('kind')));
expect(names, containsAll(['is_ignore', 'auto_apply', 'tx_type']));
});
test('merchantToCategory → классифицирующее правило, данные целы', () async {
final r = await db.parseRulesDao.findById('r-merchant');
expect(r, isNotNull);
expect(r!.isIgnore, isFalse);
expect(r.autoApply, isTrue, reason: 'дефолт auto_apply=1');
expect(r.pattern, 'PYATEROCHKA');
expect(r.matchMode, MatchMode.contains);
expect(r.priority, 1);
expect(r.matchCount, 5);
expect(r.txTypeGuard, TransactionType.expense);
expect(r.merchantCanonical, 'Пятёрочка');
expect(r.categoryId, 'cat-1');
expect(r.accountId, isNull);
expect(r.enabled, isTrue);
});
test('senderToAccount → правило со счётом', () async {
final r = await db.parseRulesDao.findById('r-sender');
expect(r, isNotNull);
expect(r!.isIgnore, isFalse);
expect(r.accountId, 'acc-1');
expect(r.categoryId, isNull);
expect(r.merchantCanonical, isNull);
expect(r.matchCount, 2);
});
test('ignore → is_ignore=1, disabled сохраняется', () async {
final r = await db.parseRulesDao.findById('r-ignore');
expect(r, isNotNull);
expect(r!.isIgnore, isTrue);
expect(r.pattern, 'заказ \\d+');
expect(r.matchMode, MatchMode.regex);
expect(r.enabled, isFalse, reason: 'enabled=0 переносится как есть');
});
}