import 'package:flutter_test/flutter_test.dart'; import 'package:life_helper/core/constants.dart'; import 'package:life_helper/data/db/app_database.dart'; import 'package:life_helper/data/seed/seed_importer.dart'; const _protocols = ''' [ { "id": "morning_sunlight", "category": "light_circadian", "title": "아침 햇빛", "what": "기상 후 햇빛.", "when": "기상 후 30~60분.", "dose": "5~10분.", "why": "ipRGC 자극.", "how": ["나간다", "쳐다본다"], "check": "60분 이내 외출", "source_doc": "huberman-protocols.md" } ] '''; const _breakProtocols = ''' [ { "id": "alcohol", "category": "alcohol", "title": "음주", "huberman_summary": "ep 86", "phases": [{"week": 1, "what": "환경 정리"}], "default_common_frames": ["dopamine_reset"] } ] '''; const _commonFrames = ''' [ { "id": "dopamine_reset", "title": "도파민 리셋", "what": "30일 절제", "why": "수용체 회복", "check": "30일 무자극" } ] '''; const _methodologies = ''' [ { "id": "atomic_habits", "name": "Atomic Habits", "originator": "James Clear", "one_line_definition": "1% 개선", "core_unit": "1회 행동", "huberman_fit_score": 5, "is_core_engine": true } ] '''; const _framePatterns = ''' [ { "id": "fp_alcohol", "domain": "drink", "avoidance_keyword": "술 끊기", "l0_example": "술 끊기", "l2_suggestion": "저녁엔 무알콜", "l3_identity": "맑은 정신 추구" } ] '''; const _rewardMenuItems = ''' [ { "id": "rmi_walk", "tier_recommended": "T1", "title": "산책 30분" } ] '''; const _references = ''' [ { "id": "ref_x", "kind": "url", "title": "Sample", "url": "https://example.com" } ] '''; const _dietPatterns = ''' [ { "id": "med", "name": "지중해 식단", "core": "올리브유 + 채소", "evidence_strength": "strong" } ] '''; Future _stubLoader(String path) async { switch (path) { case 'assets/seed/protocols.json': return _protocols; case 'assets/seed/break_protocols.json': return _breakProtocols; case 'assets/seed/common_frames.json': return _commonFrames; case 'assets/seed/methodologies.json': return _methodologies; case 'assets/seed/frame_patterns.json': return _framePatterns; case 'assets/seed/reward_menu_items.json': return _rewardMenuItems; case 'assets/seed/references.json': return _references; case 'assets/seed/diet_patterns.json': return _dietPatterns; } throw StateError('unexpected asset: $path'); } void main() { late AppDatabase db; setUp(() { db = AppDatabase.memory(); }); tearDown(() async { await db.close(); }); test('first run: imports all 8 catalogs and sets marker', () async { final importer = SeedImporter(db, loadAsset: _stubLoader); final ran = await importer.importIfNeeded(); expect(ran, true); expect((await db.select(db.protocols).get()).length, 1); expect((await db.select(db.breakProtocols).get()).length, 1); expect((await db.select(db.commonFrames).get()).length, 1); expect((await db.select(db.methodologies).get()).length, 1); expect((await db.select(db.framePatterns).get()).length, 1); expect((await db.select(db.rewardMenuItems).get()).length, 1); expect((await db.select(db.references).get()).length, 1); expect((await db.select(db.dietPatterns).get()).length, 1); final marker = await (db.select(db.metaKv) ..where((t) => t.key.equals(kSeededV1Flag))) .getSingleOrNull(); expect(marker?.value, 'true'); }); test('idempotent: second run is no-op', () async { final importer = SeedImporter(db, loadAsset: _stubLoader); await importer.importIfNeeded(); final ran2 = await importer.importIfNeeded(); expect(ran2, false); expect((await db.select(db.protocols).get()).length, 1); }); test('partial failure rolls back (transactional)', () async { Future brokenLoader(String path) async { if (path.endsWith('diet_patterns.json')) { // Trigger a CHECK violation: evidence_strength must be one of the allowed values. return ''' [{"id":"bad","name":"X","core":"Y","evidence_strength":"bogus"}] '''; } return _stubLoader(path); } final importer = SeedImporter(db, loadAsset: brokenLoader); await expectLater(importer.importIfNeeded(), throwsA(isA())); expect((await db.select(db.protocols).get()).length, 0); final marker = await (db.select(db.metaKv) ..where((t) => t.key.equals(kSeededV1Flag))) .getSingleOrNull(); expect(marker, isNull); }); }