Files

235 lines
8.6 KiB
Dart

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<void>();
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<void>.delayed(const Duration(milliseconds: 20));
request.response.write('event: heartbeat\ndata: {}\n\n');
await request.response.flush();
await Future<void>.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 = <String>[];
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الأداة المستخدمة: الآلة الحاسبة المحلية.');
});
});
}