227 lines
8.3 KiB
Dart
227 lines
8.3 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(
|
|
predicate(
|
|
(error) => error.toString().contains('trace-closed-42'),
|
|
),
|
|
),
|
|
);
|
|
} 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الأداة المستخدمة: الآلة الحاسبة المحلية.');
|
|
});
|
|
});
|
|
}
|