import 'package:flutter_test/flutter_test.dart'; import 'package:life_helper/ai/tools/habit_tools.dart'; import 'package:life_helper/ai/tools/tool_envelope.dart'; import 'package:life_helper/core/constants.dart'; import 'package:life_helper/core/time.dart'; import 'package:life_helper/data/db/daos/habit_dao.dart'; import 'package:life_helper/domain/models/habit.dart'; import '_tool_test_helpers.dart'; void main() { group('add_habit', () { test('정상 build → ToolOk', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); final r = await addHabitTool.handler({ 'protocol_id': 'morning_sunlight', 'frame_level': 'L2', 'framed_text': '아침에 햇빛 보기', }, ctx.deps); expect(r, isA(), reason: '$r'); final data = (r as ToolOk).data; expect(data['type'], 'build'); expect(data['frame_level'], 'L2'); expect(data['habit_id'], isNotEmpty); }); test('정상 break → type=break', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); final r = await addHabitTool.handler({ 'protocol_id': 'alcohol', 'frame_level': 'L3', 'framed_text': '맑은 정신을 즐긴다', }, ctx.deps); expect(r, isA(), reason: '$r'); expect((r as ToolOk).data['type'], 'break'); }); test('L0 프레임 → validation 거부', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); final r = await addHabitTool.handler({ 'protocol_id': 'morning_sunlight', 'frame_level': 'L0', 'framed_text': '게으름 피우지 마', }, ctx.deps); expect(r, isA()); expect((r as ToolErr).code, 'validation'); }); test('미존재 protocol_id → not_found', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); final r = await addHabitTool.handler({ 'protocol_id': 'no_such', 'frame_level': 'L2', 'framed_text': '뭐든', }, ctx.deps); expect(r, isA()); expect((r as ToolErr).code, 'not_found'); }); test('R7 회피 키워드 → r7_avoidance', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); // 시드 framePatterns 에 "술 끊기" avoidance keyword 존재. final r = await addHabitTool.handler({ 'protocol_id': 'alcohol', 'frame_level': 'L2', 'framed_text': '술 끊기 해야지', }, ctx.deps); expect(r, isA()); expect((r as ToolErr).code, 'r7_avoidance'); }); test('R3 quota (build 3개) 초과', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); // 3개 사전 삽입. for (var i = 0; i < 3; i++) { await ctx.deps.habitDao.insertWithVariants(HabitDraft( userId: kLocalDefaultUserId, type: HabitType.build, title: 'pre_$i', protocolId: 'morning_sunlight', frameLevel: FrameLevel.l2, frameFramedText: 'pre$i', startedAt: dateOnly(nowKst()), )); } final r = await addHabitTool.handler({ 'protocol_id': 'morning_sunlight', 'frame_level': 'L2', 'framed_text': '4번째 시도', }, ctx.deps); expect(r, isA()); expect((r as ToolErr).code, 'r3_quota'); }); }); group('list_active_habits', () { test('0개일 때', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); final r = await listActiveHabitsTool.handler({}, ctx.deps); expect(r, isA()); final data = (r as ToolOk).data; expect(data['count'], 0); expect(data['build_quota_remaining'], 3); expect(data['break_quota_remaining'], 1); }); test('add_habit 후 1개', () async { final ctx = await bootstrapToolDeps(); addTearDown(() => ctx.db.close()); await addHabitTool.handler({ 'protocol_id': 'morning_sunlight', 'frame_level': 'L2', 'framed_text': '햇빛', }, ctx.deps); final r = await listActiveHabitsTool.handler({}, ctx.deps); expect(r, isA()); final data = (r as ToolOk).data; expect(data['count'], 1); expect(data['build_count'], 1); expect(data['build_quota_remaining'], 2); }); }); }