import 'package:flutter_test/flutter_test.dart'; import 'package:life_helper/data/ai/llm_service.dart'; import 'package:life_helper/domain/ai/frame_candidate.dart'; import 'package:life_helper/domain/ai/suggest_frame.dart'; import 'package:life_helper/domain/models/frame_pattern.dart'; import 'package:life_helper/domain/models/habit.dart'; final _patterns = [ const FramePatternModel( id: 'fp_alcohol', domain: 'drink', avoidanceKeyword: '술 끊기', l0Example: '술 끊기', l2Suggestion: '무알콜', l3Identity: '맑은 정신', ), ]; const _input = SuggestFrameInput( rawText: '술 끊고 싶어', habitType: HabitType.breakHabit, ); void main() { test('happy path: returns up to 3 validated candidates', () async { final llm = MockLlmService(); await llm.load(); llm.enqueueResponse({ 'candidates': [ {'level': 'L2', 'framed_text': '저녁엔 무알콜 음료', 'confidence': 0.9}, {'level': 'L3', 'framed_text': '나는 맑은 정신의 사람이다'}, {'level': 'L2', 'framed_text': '주말엔 운동 우선'}, ], }); final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, ); expect(result, hasLength(3)); expect(result[0].level, FrameLevel.l2); }); test('L0/L1 candidates discarded by validateFrameLevel', () async { final llm = MockLlmService(); await llm.load(); llm.enqueueResponse({ 'candidates': [ {'level': 'L0', 'framed_text': '술 안 마시기'}, {'level': 'L1', 'framed_text': '음주 중단'}, {'level': 'L2', 'framed_text': '무알콜 마시기'}, ], }); final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, ); expect(result, hasLength(1)); expect(result.first.level, FrameLevel.l2); }); test('timeout → empty list (graceful)', () async { final llm = MockLlmService(); await llm.load(); llm.responseDelay = const Duration(milliseconds: 200); llm.enqueueResponse({ 'candidates': [ {'level': 'L2', 'framed_text': '무알콜'}, ], }); final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, timeout: const Duration(milliseconds: 50), ); expect(result, isEmpty); }); test('StateError (not loaded) → empty list (graceful)', () async { final llm = MockLlmService(); // not loaded final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, ); expect(result, isEmpty); }); test('malformed JSON → empty list (graceful)', () async { final llm = MockLlmService(); await llm.load(); llm.enqueueResponse({'foo': 'bar'}); // no candidates key final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, ); expect(result, isEmpty); }); test('empty rawText → llm not called', () async { final llm = MockLlmService(); await llm.load(); final result = await suggestFrame( const SuggestFrameInput( rawText: ' ', habitType: HabitType.breakHabit, ), llm: llm, framePatterns: _patterns, ); expect(result, isEmpty); expect(llm.callCount, 0); }); test('rawText > 200 chars → empty list, llm not called', () async { final llm = MockLlmService(); await llm.load(); final result = await suggestFrame( SuggestFrameInput( rawText: 'a' * 201, habitType: HabitType.breakHabit, ), llm: llm, framePatterns: _patterns, ); expect(result, isEmpty); expect(llm.callCount, 0); }); test('graceful: arbitrary throw is caught', () async { final llm = MockLlmService(); await llm.load(); llm.enqueueError(Exception('boom')); final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, ); expect(result, isEmpty); }); test('result truncated at 3 even if more valid candidates returned', () async { final llm = MockLlmService(); await llm.load(); llm.enqueueResponse({ 'candidates': [ {'level': 'L2', 'framed_text': 'a'}, {'level': 'L2', 'framed_text': 'b'}, {'level': 'L2', 'framed_text': 'c'}, {'level': 'L2', 'framed_text': 'd'}, {'level': 'L2', 'framed_text': 'e'}, ], }); final result = await suggestFrame( _input, llm: llm, framePatterns: _patterns, ); expect(result, hasLength(3)); }); }