Files
Aq-Accounting-Flutter/test/ai_refresh_test.dart
T
2026-09-28 17:16:34 +08:00

116 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AiEvent> _events;
@override
Stream<AiEvent> chat({
int? sessionId,
int? configId,
int? personaId,
required String content,
}) async* {
for (final e in _events) {
yield e;
}
}
@override
Future<List<ChatSessionItem>> 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<AiEvent> 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<void>.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<void>.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<void>.delayed(Duration.zero);
expect(builds(), 1, reason: '只读工具不该让 App 白刷一遍数据');
});
}