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
83 lines
2.9 KiB
Dart
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');
|
|
});
|
|
});
|
|
}
|