import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:qiangbao_accounting/api/ai_api.dart'; import 'package:qiangbao_accounting/models/ai.dart'; import 'package:qiangbao_accounting/models/statistics_response.dart'; import 'package:qiangbao_accounting/providers/ai_provider.dart'; import 'package:qiangbao_accounting/providers/bill_provider.dart'; /// 假的 AI 接口:按脚本吐一串 SSE 事件。 class FakeAiApi extends AiApi { FakeAiApi(this._events) : super(Dio()); final List _events; @override Stream chat({ int? sessionId, int? configId, int? personaId, required String content, }) async* { for (final e in _events) { yield e; } } @override Future> sessions() async => const []; } /// 回归测试:AI 在服务端写库后,App 必须主动让相关 provider 失效。 /// /// 背景(这是个真实踩过的 bug):`IndexedStack` 让首页仪表盘和「我的」页常驻, /// 它们 watch 的 monthlyStatisticsProvider / budgetProvider / accountProvider /// **永远不会被 autoDispose 回收**,只能靠「写操作后显式失效」更新。 /// 以前所有写操作都在 App 内,会调 invalidateBillData; /// AI 改成在服务端直接写库之后,App 毫不知情 —— 于是账单列表刷新了 /// (它是 push 出来的页面,provider 被回收过),但汇总和预算环还是旧数, /// 看起来就是「列表对了、图表没对」。 void main() { final stats = StatisticsResponse( totalIncome: 0, totalExpense: 134, balance: -134, categoryStatistics: const [], dailyStatistics: const [], ); /// 模拟「首页仪表盘一直挂在树上」,让 provider 有常驻监听者 Future<(ProviderContainer, int Function())> boot(List events) async { var builds = 0; final container = ProviderContainer( overrides: [ aiApiProvider.overrideWithValue(FakeAiApi(events)), monthlyStatisticsProvider.overrideWith((ref, month) { builds++; return Future.value(stats); }), ], ); final anchor = DateTime(2026, 9); final sub = container.listen(monthlyStatisticsProvider(anchor), (_, _) {}); addTearDown(sub.close); addTearDown(container.dispose); await container.read(monthlyStatisticsProvider(anchor).future); return (container, () => builds); } test('AI 调了 create_bill 之后,账单汇总会被失效重取', () async { final (container, builds) = await boot([ const AiEvent.tool('create_bill'), const AiEvent.delta('记好啦宝'), const AiEvent.done(1), ]); expect(builds(), 1, reason: '初始只取过一次'); await container.read(assistantProvider.notifier).send('帮我记一笔 25 块'); await container.read(monthlyStatisticsProvider(DateTime(2026, 9)).future); await Future.delayed(Duration.zero); expect(builds(), 2, reason: 'AI 写了账单,汇总必须重取'); }); test('AI 调了 create_note 不会误伤账单数据', () async { final (container, builds) = await boot([ const AiEvent.tool('create_note'), const AiEvent.delta('记好了宝'), const AiEvent.done(1), ]); expect(builds(), 1); await container.read(assistantProvider.notifier).send('帮我记一下:周三开会'); await Future.delayed(Duration.zero); expect(builds(), 1, reason: '只是记笔记,账单汇总不该被失效'); }); test('纯查询不触发任何重取', () async { final (container, builds) = await boot([ const AiEvent.tool('query_bills'), const AiEvent.tool('web_search'), const AiEvent.delta('这个月花了 134'), const AiEvent.done(1), ]); expect(builds(), 1); await container.read(assistantProvider.notifier).send('这个月花了多少'); await Future.delayed(Duration.zero); expect(builds(), 1, reason: '只读工具不该让 App 白刷一遍数据'); }); }