import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_app/core/auth/session_token_store.dart'; import 'package:flutter_app/core/network/api_repository.dart'; void main() { group('ApiRepository.runAgent SSE stream', () { test('includes the audit ID when the server closes before done', () async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); server.listen((request) async { await utf8.decoder.bind(request).join(); request.response.headers.contentType = ContentType.json; if (request.uri.path == '/v1/auth/local-session') { request.response.write( jsonEncode({ 'access_token': 'test-session', 'user': {'mode': 'local'}, }), ); } else if (request.uri.path == '/v1/agent/run/stream') { request.response.statusCode = HttpStatus.ok; request.response.headers.set('x-agent-audit-id', 'trace-closed-42'); request.response.headers.contentType = ContentType( 'text', 'event-stream', charset: 'utf-8', ); request.response.write( 'event: progress\ndata: {"message":"بدأ الوكيل"}\n\n', ); } else { request.response.statusCode = HttpStatus.notFound; } await request.response.close(); }); try { final repository = ApiRepository( baseUrl: 'http://${server.address.address}:${server.port}', sessionTokenStore: EphemeralSessionTokenStore(), ); await expectLater( repository.runAgent('اختبار انقطاع البث'), throwsA( allOf( predicate( (error) => error.toString().contains('trace-closed-42'), ), predicate( (error) => error.toString().contains('آخر حدث: progress'), ), predicate( (error) => error.toString().contains('عدد النبضات: 0'), ), ), ), ); } finally { await server.close(force: true); } }); test('includes the audit ID for an SSE error event', () async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); server.listen((request) async { await utf8.decoder.bind(request).join(); request.response.statusCode = HttpStatus.ok; if (request.uri.path == '/v1/auth/local-session') { request.response.headers.contentType = ContentType.json; request.response.write( jsonEncode({ 'access_token': 'test-session', 'user': {'mode': 'local'}, }), ); } else if (request.uri.path == '/v1/agent/run/stream') { request.response.headers.set('x-agent-audit-id', 'trace-error-17'); request.response.headers.contentType = ContentType( 'text', 'event-stream', charset: 'utf-8', ); request.response.write( 'event: error\ndata: {"message":"تعذر إكمال المهمة"}\n\n', ); } await request.response.close(); }); try { final repository = ApiRepository( baseUrl: 'http://${server.address.address}:${server.port}', sessionTokenStore: EphemeralSessionTokenStore(), ); await expectLater( repository.runAgent('اختبار خطأ البث'), throwsA( predicate((error) => error.toString().contains('trace-error-17')), ), ); } finally { await server.close(force: true); } }); test( 'keeps progress through heartbeat and returns the done result', () async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); final seenRequests = <({String path, String? authorization, String body})>[]; final serverDone = Completer(); server.listen((request) async { final body = await utf8.decoder.bind(request).join(); seenRequests.add(( path: request.uri.path, authorization: request.headers.value( HttpHeaders.authorizationHeader, ), body: body, )); if (request.uri.path == '/v1/auth/local-session') { request.response.headers.contentType = ContentType.json; request.response.write( jsonEncode({ 'access_token': 'test-session', 'user': {'mode': 'local'}, }), ); await request.response.close(); return; } if (request.uri.path != '/v1/agent/run/stream') { request.response.statusCode = HttpStatus.notFound; await request.response.close(); return; } request.response.headers.contentType = ContentType( 'text', 'event-stream', charset: 'utf-8', ); request.response.write( 'event: progress\ndata: {"message":"بدأ البحث"}\n\n', ); await request.response.flush(); await Future.delayed(const Duration(milliseconds: 20)); request.response.write('event: heartbeat\ndata: {}\n\n'); await request.response.flush(); await Future.delayed(const Duration(milliseconds: 20)); request.response.write( 'event: done\ndata: {"result":"اكتمل البحث","steps":[{"tool":"search_workspace","status":"completed"}],"files":["README.md"]}\n\n', ); await request.response.close(); if (!serverDone.isCompleted) serverDone.complete(); }); final progress = []; try { final repository = ApiRepository( baseUrl: 'http://${server.address.address}:${server.port}', sessionTokenStore: EphemeralSessionTokenStore(), ); final result = await repository.runAgent( 'اقرأ README', workspacePath: 'C:/workspace', workspaceFiles: const ['README.md'], skillId: 'code_explain', onProgress: progress.add, ); await serverDone.future; expect(result.answer, contains('اكتمل البحث')); expect(result.answer, contains('الملفات المقروءة: README.md.')); expect(progress, contains('بدأ البحث')); expect( progress, contains('الوكيل ما زال يعمل، أنتظر النموذج المحلي...'), ); expect(seenRequests.map((request) => request.path), [ '/v1/auth/local-session', '/v1/agent/run/stream', ]); expect(seenRequests.last.authorization, 'Bearer test-session'); expect(jsonDecode(seenRequests.last.body), { 'task': 'اقرأ README', 'workspace_path': 'C:/workspace', 'workspace_files': ['README.md'], 'skill_id': 'code_explain', }); } finally { await server.close(force: true); } }, ); }); group('AgentRunResult.fromApiData', () { test('preserves and labels every completed tool step', () { final result = AgentRunResult.fromApiData({ 'result': 'الإجابة النهائية.', 'tool': 'search_knowledge', 'steps': [ {'tool': 'search_workspace', 'status': 'completed'}, {'tool': 'search_knowledge', 'status': 'completed'}, ], 'files': ['lib/main.dart'], }); expect(result.steps, hasLength(2)); expect(result.steps.first.label, 'البحث في مساحة العمل'); expect(result.steps.last.label, 'البحث في فهرس المعرفة'); expect(result.steps.last.statusLabel, 'اكتملت'); expect(result.answer, contains('1. البحث في مساحة العمل — اكتملت.')); expect(result.answer, contains('2. البحث في فهرس المعرفة — اكتملت.')); expect(result.answer, contains('الملفات المقروءة: lib/main.dart.')); }); test('uses the legacy tool field when the API returns no steps', () { final result = AgentRunResult.fromApiData({ 'result': '391', 'tool': 'calculator', }); expect(result.steps, isEmpty); expect(result.answer, '391\n\nالأداة المستخدمة: الآلة الحاسبة المحلية.'); }); }); }