Files
life-helper/app/test/ai/tools/catalog_tools_test.dart
joungmin b1bed4d5ca [03-Developer] #260 in-app tool calling (Gemma 4 multi-turn)
ADR-0005 in-process tool runtime — 6 tools (catalog 2 + tracker 2 +
habit 2), ToolDispatcher with JSON-schema validation + modal ConfirmGate
for destructive ops, multi-turn LlmChatSession abstraction wired to
flutter_gemma 0.16.5 (ToolChoice.auto), ChatSessionController with
MAX_TURNS=4 safety + 8-turn history hint, ChatScreen entry behind AI
opt-in. R3/R7/R8 enforced inside handlers. 41 new tests (envelope,
catalog/tracker/habit tools, dispatcher, controller loop) — 151 total
passing.

Refs #260
2026-06-15 10:42:43 +09:00

83 lines
2.9 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:life_helper/ai/tools/catalog_tools.dart';
import 'package:life_helper/ai/tools/tool_envelope.dart';
import '_tool_test_helpers.dart';
void main() {
group('search_catalog', () {
test('전체 검색 (인자 없음)', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r = await searchCatalogTool.handler({}, ctx.deps);
expect(r, isA<ToolOk>());
final data = (r as ToolOk).data;
expect(data['count'], greaterThan(0));
// stub 3 items: protocol + break + diet
expect(data['count'], 3);
final items = data['items'] as List;
expect(items.first.containsKey('id'), true);
expect(items.first.containsKey('summary'), true);
});
test('카테고리 필터', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r = await searchCatalogTool
.handler({'category': 'breakHabit'}, ctx.deps);
expect(r, isA<ToolOk>());
final items = ((r as ToolOk).data['items'] as List);
expect(items.length, 1);
expect((items.first as Map)['category'], 'breakHabit');
});
test('잘못된 카테고리 → validation 에러', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r = await searchCatalogTool
.handler({'category': 'no_such'}, ctx.deps);
expect(r, isA<ToolErr>());
expect((r as ToolErr).code, 'validation');
});
test('limit 범위 검증', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r = await searchCatalogTool.handler({'limit': 99}, ctx.deps);
expect(r, isA<ToolErr>());
expect((r as ToolErr).code, 'validation');
});
});
group('query_protocol', () {
test('정상 조회 → kind 분기', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r = await queryProtocolTool
.handler({'id': 'morning_sunlight'}, ctx.deps);
expect(r, isA<ToolOk>());
final data = (r as ToolOk).data;
expect(data['kind'], 'protocol');
expect(data['what'], isNotNull);
});
test('break 항목 → kind=break', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r =
await queryProtocolTool.handler({'id': 'alcohol'}, ctx.deps);
expect(r, isA<ToolOk>());
expect((r as ToolOk).data['kind'], 'break');
});
test('미존재 id → not_found', () async {
final ctx = await bootstrapToolDeps();
addTearDown(() => ctx.db.close());
final r =
await queryProtocolTool.handler({'id': 'no_such'}, ctx.deps);
expect(r, isA<ToolErr>());
expect((r as ToolErr).code, 'not_found');
});
});
}