[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
This commit is contained in:
82
app/test/ai/tools/catalog_tools_test.dart
Normal file
82
app/test/ai/tools/catalog_tools_test.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user