refactor: 重构聊天数据层至core并简化首页UI
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/app/di/injection.dart';
|
||||
import 'package:social_app/app/router/app_router.dart';
|
||||
import 'package:social_app/app/router/app_routes.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
import 'package:social_app/core/chat/chat_api.dart';
|
||||
import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_state.dart';
|
||||
|
||||
@@ -13,7 +14,7 @@ void main() {
|
||||
if (sl.isRegistered<ChatBloc>()) {
|
||||
await sl.unregister<ChatBloc>();
|
||||
}
|
||||
sl.registerSingleton<ChatBloc>(ChatBloc(apiClient: _FakeApiClient()));
|
||||
sl.registerSingleton<ChatBloc>(ChatBloc(chatApi: _FakeChatApi()));
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
@@ -43,6 +44,17 @@ void main() {
|
||||
expect(result, AppRoutes.homeMain);
|
||||
});
|
||||
|
||||
test('keeps authenticated home access on home route', () {
|
||||
final result = resolveAuthRedirect(
|
||||
authState: const AuthAuthenticated(
|
||||
user: AuthUser(id: 'u1', phone: '13800138000'),
|
||||
),
|
||||
matchedLocation: AppRoutes.homeMain,
|
||||
);
|
||||
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('redirects auth checking state to boot route', () {
|
||||
final result = resolveAuthRedirect(
|
||||
authState: AuthLoading(),
|
||||
@@ -69,37 +81,50 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeApiClient implements IApiClient {
|
||||
class _FakeChatApi implements ChatApi {
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
|
||||
Future<void> cancelRun({required String threadId, required String runId}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) {
|
||||
Future<Map<String, dynamic>> createRun(Map<String, dynamic> runInput) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<String>> getSseLines(
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
Future<Map<String, dynamic>> fetchHistory({
|
||||
String? threadId,
|
||||
DateTime? beforeDate,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> patch<T>(String path, {data, Options? options}) {
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) {
|
||||
Future<Stream<String>> streamRunEvents(
|
||||
String threadId, {
|
||||
String? lastEventId,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> put<T>(String path, {data, Options? options}) {
|
||||
Future<String> transcribeAudio(String filePath) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> uploadAttachment({
|
||||
required String threadId,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
required Uint8List bytes,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/app/services/app_prewarm_orchestrator.dart';
|
||||
import 'package:social_app/core/chat/chat_api.dart';
|
||||
import 'package:social_app/core/chat/chat_history_repository.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
import 'package:social_app/data/cache/cache_store.dart';
|
||||
import 'package:social_app/features/calendar/data/repositories/calendar_repository.dart';
|
||||
import 'package:social_app/features/messages/data/repositories/inbox_repository.dart';
|
||||
import 'package:social_app/features/chat/data/repositories/chat_history_repository.dart';
|
||||
|
||||
class _FakeApiClient implements IApiClient {
|
||||
@override
|
||||
@@ -44,15 +46,65 @@ class _FakeApiClient implements IApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeChatApi implements ChatApi {
|
||||
@override
|
||||
Future<void> cancelRun({required String threadId, required String runId}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> createRun(Map<String, dynamic> runInput) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> fetchHistory({
|
||||
String? threadId,
|
||||
DateTime? beforeDate,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<String>> streamRunEvents(
|
||||
String threadId, {
|
||||
String? lastEventId,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> transcribeAudio(String filePath) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> uploadAttachment({
|
||||
required String threadId,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
required Uint8List bytes,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late HybridCacheStore store;
|
||||
late _FakeApiClient apiClient;
|
||||
late _FakeChatApi chatApi;
|
||||
late CalendarRepository calendarRepository;
|
||||
late InboxRepository inboxRepository;
|
||||
late ChatHistoryRepository chatHistoryRepository;
|
||||
|
||||
setUp(() {
|
||||
apiClient = _FakeApiClient();
|
||||
chatApi = _FakeChatApi();
|
||||
store = HybridCacheStore(
|
||||
memory: MemoryCacheStore(),
|
||||
persistent: PersistentCacheStore(),
|
||||
@@ -60,7 +112,7 @@ void main() {
|
||||
calendarRepository = CalendarRepository(apiClient: apiClient, store: store);
|
||||
inboxRepository = InboxRepositoryImpl(apiClient: apiClient, store: store);
|
||||
chatHistoryRepository = ChatHistoryRepository(
|
||||
apiClient: apiClient,
|
||||
chatApi: chatApi,
|
||||
store: store,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,60 +1,87 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/data/network/i_api_client.dart';
|
||||
import 'package:social_app/core/chat/chat_api.dart';
|
||||
import 'package:social_app/core/chat/chat_history_repository.dart';
|
||||
import 'package:social_app/data/cache/cache_store.dart';
|
||||
import 'package:social_app/features/chat/data/repositories/chat_history_repository.dart';
|
||||
|
||||
class _FakeApiClient implements IApiClient {
|
||||
final Map<String, dynamic> _getResponses = <String, dynamic>{};
|
||||
final Map<String, int> getCalls = <String, int>{};
|
||||
class _FakeChatApi implements ChatApi {
|
||||
final Map<String, dynamic> _historyResponses = <String, dynamic>{};
|
||||
final Map<String, int> historyCalls = <String, int>{};
|
||||
|
||||
void setGet(String path, dynamic data) => _getResponses[path] = data;
|
||||
void setHistory(String key, dynamic data) => _historyResponses[key] = data;
|
||||
|
||||
@override
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) async {
|
||||
getCalls[path] = (getCalls[path] ?? 0) + 1;
|
||||
if (!_getResponses.containsKey(path)) {
|
||||
throw StateError('missing GET mock for $path');
|
||||
Future<Map<String, dynamic>> fetchHistory({
|
||||
String? threadId,
|
||||
DateTime? beforeDate,
|
||||
}) async {
|
||||
final key = _historyKey(threadId: threadId, beforeDate: beforeDate);
|
||||
historyCalls[key] = (historyCalls[key] ?? 0) + 1;
|
||||
if (!_historyResponses.containsKey(key)) {
|
||||
throw StateError('missing history mock for $key');
|
||||
}
|
||||
return Response<T>(
|
||||
requestOptions: RequestOptions(path: path),
|
||||
data: _getResponses[path] as T,
|
||||
);
|
||||
return _historyResponses[key] as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> delete<T>(String path, {data, Options? options}) {
|
||||
Future<void> cancelRun({required String threadId, required String runId}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<String>> getSseLines(
|
||||
String path, {
|
||||
Map<String, String>? headers,
|
||||
Future<Map<String, dynamic>> createRun(Map<String, dynamic> runInput) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> fetchAttachmentPreview(String previewPath) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<String>> streamRunEvents(
|
||||
String threadId, {
|
||||
String? lastEventId,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> patch<T>(String path, {data, Options? options}) {
|
||||
Future<String> transcribeAudio(String filePath) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> post<T>(String path, {data, Options? options}) {
|
||||
Future<Map<String, dynamic>> uploadAttachment({
|
||||
required String threadId,
|
||||
required String filename,
|
||||
required String mimeType,
|
||||
required Uint8List bytes,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> put<T>(String path, {data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
String _historyKey({String? threadId, DateTime? beforeDate}) {
|
||||
final threadPart = (threadId == null || threadId.isEmpty)
|
||||
? 'default'
|
||||
: threadId;
|
||||
if (beforeDate == null) {
|
||||
return 'first:$threadPart';
|
||||
}
|
||||
final day = DateTime(
|
||||
beforeDate.year,
|
||||
beforeDate.month,
|
||||
beforeDate.day,
|
||||
).toIso8601String().substring(0, 10);
|
||||
return 'before:$threadPart:$day';
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('loads first-page history from cache on second read', () async {
|
||||
final client = _FakeApiClient();
|
||||
client.setGet('/api/v1/agent/history', {
|
||||
final chatApi = _FakeChatApi();
|
||||
chatApi.setHistory('first:default', {
|
||||
'scope': 'history_day',
|
||||
'threadId': 't1',
|
||||
'day': '2026-03-29',
|
||||
@@ -72,7 +99,7 @@ void main() {
|
||||
});
|
||||
|
||||
final repository = ChatHistoryRepository(
|
||||
apiClient: client,
|
||||
chatApi: chatApi,
|
||||
store: HybridCacheStore(
|
||||
memory: MemoryCacheStore(),
|
||||
persistent: PersistentCacheStore(),
|
||||
@@ -84,6 +111,6 @@ void main() {
|
||||
|
||||
expect(first.threadId, 't1');
|
||||
expect(second.messages.length, 1);
|
||||
expect(client.getCalls['/api/v1/agent/history'], 1);
|
||||
expect(chatApi.historyCalls['first:default'], 1);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user