156 lines
5.3 KiB
Dart
156 lines
5.3 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'package:qiangbao_accounting/api/ai_api.dart';
|
|
import 'package:qiangbao_accounting/models/ai.dart';
|
|
import 'package:qiangbao_accounting/models/ai_persona.dart';
|
|
import 'package:qiangbao_accounting/providers/ai_provider.dart';
|
|
import 'package:qiangbao_accounting/providers/ai_persona_provider.dart';
|
|
|
|
/// 「会话内人设固定」的回归测试。
|
|
///
|
|
/// 这是这次需求里**最容易悄悄坏掉**的一条:人设现在是「每轮 send 时现读」还是
|
|
/// 「建会话时定下」,代码上只差一行 —— 但坏掉的表现是「聊到一半助手换了性格」,
|
|
/// 而且要用户真的去设置页切一次人设才能发现。
|
|
///
|
|
/// 所以这里直接断言「send 时带上的是 state.personaId,不是全局偏好」。
|
|
class _CapturingAiApi extends AiApi {
|
|
_CapturingAiApi() : super(Dio());
|
|
|
|
/// 每次 chat 调用时带上的 personaId,按调用顺序记下来
|
|
final List<int?> personaIdsSent = [];
|
|
|
|
@override
|
|
Stream<AiEvent> chat({
|
|
int? sessionId,
|
|
int? configId,
|
|
int? personaId,
|
|
required String content,
|
|
}) async* {
|
|
personaIdsSent.add(personaId);
|
|
// 模拟一个最小可用的回答,让 send() 能正常走完
|
|
yield const AiEvent.delta('好');
|
|
yield const AiEvent.done(1);
|
|
}
|
|
|
|
@override
|
|
Future<List<ChatSessionItem>> sessions() async => const [];
|
|
|
|
@override
|
|
Future<List<AiPersonaItem>> personas() async => const [];
|
|
}
|
|
|
|
void main() {
|
|
// LocalStorage 是单例,select() 会写盘。测试里给它一个空实现,
|
|
// 否则会去真的要 SharedPreferences(在纯单测环境里拿不到)
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
SharedPreferences.setMockInitialValues({});
|
|
|
|
ProviderContainer makeContainer(_CapturingAiApi api) {
|
|
final c = ProviderContainer(overrides: [
|
|
aiApiProvider.overrideWithValue(api),
|
|
]);
|
|
addTearDown(c.dispose);
|
|
return c;
|
|
}
|
|
|
|
group('★ 会话内人设固定', () {
|
|
test('新对话选的人设会一直用下去(中途全局偏好变了也不受影响)', () async {
|
|
final api = _CapturingAiApi();
|
|
final c = makeContainer(api);
|
|
final notifier = c.read(assistantProvider.notifier);
|
|
|
|
// 用户在「新对话」弹层里选了人设 7
|
|
notifier.newChat(personaId: 7);
|
|
expect(c.read(assistantProvider).personaId, 7);
|
|
|
|
// 中途用户在设置页把全局偏好换成了 9
|
|
c.read(selectedAiPersonaProvider.notifier).select(9);
|
|
|
|
// 继续在这个会话里说话
|
|
await notifier.send('第一句');
|
|
await notifier.send('第二句');
|
|
|
|
// 两次都该用 7,不该跟着全局偏好变成 9
|
|
expect(api.personaIdsSent, [7, 7],
|
|
reason: '会话锁定的人设不该被全局偏好覆盖');
|
|
});
|
|
|
|
test('新对话不选人设时用 null(内置默认)', () async {
|
|
final api = _CapturingAiApi();
|
|
final c = makeContainer(api);
|
|
final notifier = c.read(assistantProvider.notifier);
|
|
|
|
notifier.newChat();
|
|
expect(c.read(assistantProvider).personaId, isNull);
|
|
|
|
await notifier.send('随便说点');
|
|
|
|
expect(api.personaIdsSent, [null]);
|
|
});
|
|
|
|
test('newChat 会清掉上一个人设(不残留)', () async {
|
|
final api = _CapturingAiApi();
|
|
final c = makeContainer(api);
|
|
final notifier = c.read(assistantProvider.notifier);
|
|
|
|
notifier.newChat(personaId: 3);
|
|
expect(c.read(assistantProvider).personaId, 3);
|
|
|
|
notifier.newChat();
|
|
expect(c.read(assistantProvider).personaId, isNull,
|
|
reason: '第二次新对话没选人设,不该残留上一次的');
|
|
});
|
|
|
|
test('打开历史会话时用那条会话绑定的人设', () async {
|
|
final api = _CapturingAiApi();
|
|
final c = makeContainer(api);
|
|
final notifier = c.read(assistantProvider.notifier);
|
|
|
|
// 打开一个三个月前的会话,它绑的是人设 5
|
|
await notifier.openSession(42, personaId: 5);
|
|
expect(c.read(assistantProvider).personaId, 5);
|
|
expect(c.read(assistantProvider).sessionId, 42);
|
|
|
|
// 在这条历史会话里继续说话,仍然用 5
|
|
await notifier.send('接着聊');
|
|
|
|
expect(api.personaIdsSent, [5]);
|
|
});
|
|
});
|
|
|
|
group('ChatSessionItem 解析(会话列表带人设)', () {
|
|
test('完整字段', () {
|
|
final s = ChatSessionItem.fromJson({
|
|
'id': 1,
|
|
'title': '聊记账',
|
|
'updateTime': '2026-09-29 10:00:00',
|
|
'personaId': 7,
|
|
'personaName': '花花',
|
|
'personaAvatar': 'persona/a.jpg',
|
|
});
|
|
|
|
expect(s.id, 1);
|
|
expect(s.personaId, 7);
|
|
expect(s.personaName, '花花');
|
|
expect(s.personaAvatar, 'persona/a.jpg');
|
|
});
|
|
|
|
test('★ 缺字段不崩(老后端 / 人设已删)', () {
|
|
final s = ChatSessionItem.fromJson({'id': 1, 'title': 'x'});
|
|
|
|
expect(s.personaId, isNull);
|
|
expect(s.personaName, '默认人设');
|
|
// 头像空串归一成 null,Avatar 组件据此显示默认图标
|
|
expect(s.personaAvatar, '');
|
|
});
|
|
|
|
test('人设名缺失时回落成「默认人设」,不显示空白', () {
|
|
final s = ChatSessionItem.fromJson({'id': 1, 'personaName': ''});
|
|
expect(s.personaName, '默认人设');
|
|
});
|
|
});
|
|
}
|