import 'dart:convert'; import 'package:eaimar/data/models/assistant_directive.dart'; import 'package:flutter_test/flutter_test.dart'; /// 用例里的 payload 全部是 2026-08-29 从真机上抓的原始帧(见 scratchpad 的 /// bailian_raw_frames.jsonl),不是手编的——协议里那几个坑(arguments 是 /// 字符串、参数是中文、纯指令帧 text 为空)只有照抄真实数据才守得住。 void main() { Map outputOf(String raw) => (json.decode(raw)['payload']['output'] as Map).cast(); group('tool_calls 解析', () { test('一次性闹钟:time + date', () { final output = outputOf(r''' {"payload":{"output":{"event":"RespondingContent", "text":"好的,已为你设置5点的闹钟,别忘了准时参加哦!","finished":true,"finish_reason":"stop", "extra_info":{"tool_calls":[{"function":{"name":"SET_clock", "arguments":"{\"time\": \"17:00\", \"date\": \"2026-08-29\"}"},"type":"FUNCTION"}]}, "dialog_id":"d1","round_id":"r1","llm_request_id":"l1"}}}'''); final ds = AssistantDirective.parseFromOutput(output, source: 'emai'); expect(ds, hasLength(1)); expect(ds.first.name, 'SET_clock'); expect(ds.first.args['time'], '17:00'); expect(ds.first.args['date'], '2026-08-29'); expect(ds.first.roundId, 'r1'); final r = AssistantReminder.fromSetClock(ds.first)!; expect(r.time, '17:00'); expect(r.date, '2026-08-29'); expect(r.repeat, RepeatRule.once); expect(r.content, isEmpty); // 这一轮模型没给 content }); test('重复闹钟:content + repeat,没有 date', () { final output = outputOf(r''' {"payload":{"output":{"event":"RespondingContent","text":"好的","finished":true, "finish_reason":"stop","extra_info":{"tool_calls":[{"function":{"name":"SET_clock", "arguments":"{\"content\": \"开会\", \"time\": \"08:00\", \"repeat\": \"周一\"}"}, "type":"FUNCTION"}]},"dialog_id":"d","round_id":"r2","llm_request_id":"l"}}}'''); final r = AssistantReminder.fromSetClock(AssistantDirective.parseFromOutput( output, source: 'emai', ).first)!; expect(r.content, '开会'); expect(r.time, '08:00'); expect(r.date, isNull); expect(r.repeat, RepeatRule.weekly); expect(r.weekday, DateTime.monday); expect(r.repeatRaw, '周一'); }); test('纯指令帧:text 为空,finish_reason=command_calls', () { final output = outputOf(r''' {"payload":{"output":{"event":"RespondingContent","text":"","spoken":"", "finished":true,"finish_reason":"command_calls", "extra_info":{"tool_calls":[{"function":{"name":"ROUTE_map", "arguments":"{\"endLoc_city\": \"深圳市\", \"endLoc_poi\": \"机场\"}"},"type":"FUNCTION"}]}, "dialog_id":"d","round_id":"r3","llm_request_id":"l"}}}'''); expect(output['text'], isEmpty); expect(output['finish_reason'], 'command_calls'); final ds = AssistantDirective.parseFromOutput(output, source: 'emai'); expect(ds.single.name, 'ROUTE_map'); expect(ds.single.args['endLoc_city'], '深圳市'); }); test('tool_infos 是服务端插件的结果,不能当成端侧指令', () { final output = outputOf(r''' {"payload":{"output":{"event":"RespondingContent","text":"Let me see.","finished":true, "finish_reason":"stop","extra_info":{"tool_infos":[ {"output":"","tool_name":"weather","success":true,"type":"plugin"}]}, "dialog_id":"d","round_id":"r4","llm_request_id":"l"}}}'''); expect(AssistantDirective.parseFromOutput(output, source: 'emai'), isEmpty); }); test('没有 extra_info 的普通回复不产生指令', () { final output = outputOf( '{"payload":{"output":{"event":"RespondingContent","text":"你好","finished":true}}}'); expect(AssistantDirective.parseFromOutput(output, source: 'emai'), isEmpty); }); test('arguments 是坏 JSON 时保留原文而不是整条丢掉', () { final output = outputOf(r''' {"payload":{"output":{"event":"RespondingContent","text":"","finished":true, "extra_info":{"tool_calls":[{"function":{"name":"SET_clock", "arguments":"{不是合法 JSON"},"type":"FUNCTION"}]},"round_id":"r5"}}}'''); final d = AssistantDirective.parseFromOutput(output, source: 'emai').single; expect(d.args, isEmpty); expect(d.rawArguments, '{不是合法 JSON'); // time 解不出来 → 不落提醒,交由调用方记 failed expect(AssistantReminder.fromSetClock(d), isNull); }); }); group('中文重复规则', () { test('每天 / 每日', () { expect(AssistantReminder.parseRepeat('每天').$1, RepeatRule.daily); expect(AssistantReminder.parseRepeat('每日').$1, RepeatRule.daily); }); test('周几:周日和周天都是 7', () { expect(AssistantReminder.parseRepeat('周三'), (RepeatRule.weekly, 3)); expect(AssistantReminder.parseRepeat('每周五'), (RepeatRule.weekly, 5)); expect(AssistantReminder.parseRepeat('星期日'), (RepeatRule.weekly, 7)); expect(AssistantReminder.parseRepeat('周天'), (RepeatRule.weekly, 7)); }); test('工作日 / 周末', () { expect(AssistantReminder.parseRepeat('工作日').$1, RepeatRule.weekdays); expect(AssistantReminder.parseRepeat('周末').$1, RepeatRule.weekend); }); test('空表示不重复,认不出的写法归 unknown 而不是瞎猜', () { expect(AssistantReminder.parseRepeat('').$1, RepeatRule.once); expect(AssistantReminder.parseRepeat('隔一天').$1, RepeatRule.unknown); }); }); group('12 小时制歧义校正', () { // 这一组用例来自真机实测:说 "3:30pm"、模型文案写"下午3:30", // 但 time 给的是 "03:30"。照收就会定到凌晨三点半。 String fix(String time, String reply) => AssistantReminder.disambiguateAfternoon(time, reply); test('文案说下午而小时 < 12 时 +12', () { expect(fix('03:30', '好的,我已经帮你设定了明天下午3:30的闹钟。'), '15:30'); expect(fix('09:00', '晚上9点提醒你'), '21:00'); expect(fix('03:30', 'Alarm set for 3:30 PM tomorrow'), '15:30'); }); test('文案说上午 / 没提时段时原样保留', () { expect(fix('08:00', '已经为你设定了每周一早上8点的闹钟'), '08:00'); expect(fix('08:00', '好的,已经设好了'), '08:00'); expect(fix('08:00', '上午8点到下午6点都提醒'), '08:00'); // 同时出现,不猜 }); test('已经是 24 小时制的不动', () { expect(fix('17:00', '下午5点的闹钟'), '17:00'); expect(fix('21:00', '晚上9点'), '21:00'); expect(fix('00:30', '凌晨的闹钟'), '00:30'); }); test('端到端:真实帧走完 fromSetClock 得到 15:30', () { final output = outputOf(r''' {"payload":{"output":{"event":"RespondingContent", "text":"好的,我已经帮你设定了明天下午3:30的闹钟。","finished":true,"finish_reason":"stop", "extra_info":{"tool_calls":[{"function":{"name":"SET_clock", "arguments":"{\"time\": \"03:30\", \"date\": \"2026-08-30\", \"content\": \"项目 review\"}"}, "type":"FUNCTION"}]},"dialog_id":"d","round_id":"r9","llm_request_id":"l"}}}'''); final r = AssistantReminder.fromSetClock( AssistantDirective.parseFromOutput(output, source: 'emai').single)!; expect(r.time, '15:30'); expect(r.content, '项目 review'); expect(r.date, '2026-08-30'); }); }); group('时间归一', () { AssistantReminder? build(String time) => AssistantReminder.fromSetClock( AssistantDirective( id: 'x', name: 'SET_clock', args: {'time': time}, rawArguments: '', dialogId: '', roundId: '', llmRequestId: '', source: 'emai', createdAt: DateTime.now(), ), ); test('补零与全角冒号', () { expect(build('8:0')?.time, '08:00'); expect(build('08:30')?.time, '08:30'); expect(build('21:00:00')?.time, '21:00'); }); test('非法时间不落提醒', () { expect(build('25:00'), isNull); expect(build('晚上八点'), isNull); expect(build(''), isNull); }); }); test('序列化可往返(将来要原样上传服务器)', () { final d = AssistantDirective( id: 'r#0', name: 'SET_clock', args: {'time': '08:00', 'repeat': '周一'}, rawArguments: '{"time":"08:00","repeat":"周一"}', dialogId: 'd', roundId: 'r', llmRequestId: 'l', source: 'earphone', createdAt: DateTime.parse('2026-08-29T10:00:00'), status: DirectiveStatus.done, ); final back = AssistantDirective.fromJson(json .decode(json.encode(d.toJson())) .cast()); expect(back.name, d.name); expect(back.args['repeat'], '周一'); expect(back.status, DirectiveStatus.done); expect(back.source, 'earphone'); expect(back.synced, isFalse); }); }