test Flutter agent SSE heartbeat handling
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
|
||||
## الحالة الحالية — 2026-10-04
|
||||
|
||||
- 2026-10-04 — اختبار عقد بث الوكيل في عميل Flutter: أُضيف خادم HTTP محلي للاختبار يرسل أحداث تقدم و`heartbeat` ثم `done`، ويتحقق من عرض رسالة النبضة والجواب النهائي وتمرير Bearer ومساحة المشروع والملفات المختارة والمهارة في الطلب. اختبار الملف المستهدف نجح 3/3، ومجموعة Flutter الكاملة نجحت 24/24، و`flutter analyze --no-pub` بلا ملاحظات. هذا يثبت فك أحداث SSE في عميل Dart ولا يغني عن تجربة النافذة يدويًا أو عن طلب حي طويل؛ مشكلة عدم ظهور نافذة نسخة LocalAppData لواجهة التحكم موثقة في بند التحقق المرئي أدناه.
|
||||
|
||||
- 2026-10-04 — إصلاح سبب انتهاء مهلة الوكيل بعد عدة دقائق: بعد إضافة SSE keep-alive، كشف الفحص أن كل استدعاء نموذج داخل حلقة الوكيل ما زال يستخدم المهلة الافتراضية 180 ثانية، رغم أن العميل والبث يسمحان حتى 10 دقائق. رُفعت مهلة كل خطوة نموذج في الوكيل إلى 600 ثانية لتتسق مع مهلة البث، وأضيف اختبار يثبت قيمة المهلة. بعد إعادة تشغيل Uvicorn للمشروع على 8000، نجحت اختبارات `test_timeouts_and_cancellation` 5/5، وأعاد `/health` الحالة `ok`. طلب وكيل حي أعاد HTTP 200 واكتمل بعد 41.9 ثانية مع نبضتي keep-alive؛ طلب حي ثانٍ اكتمل بعد 12.2 ثانية مع أحداث `done` وإغلاق الجلسة. لا يثبت هذا اختبارًا يستغرق خمس دقائق فعليًا، لكنه يزيل حد الـ180 ثانية الذي كان يطابق الانقطاع المبلغ عنه؛ ما زال اختبار نافذة Flutter التفاعلي غير منجز.
|
||||
|
||||
- 2026-10-04 — متابعة بلاغ انقطاع الوكيل بعد 4–5 دقائق: `/health` على `127.0.0.1:8000` سليم ويستخدم `gemma4:e2b`. نفذت طلبًا حيًا عبر `/v1/agent/run/stream` مع `app/main.py`؛ استغرق 90.2 ثانية، وأعاد HTTP 200 ثم أحداث التقدم وkeep-alive وانتهى بـ`done` ومعرّفي تتبع للطلب والتدقيق. لم يتكرر الانقطاع في هذه التجربة. حُوّلت نبضة SSE من تعليق صامت إلى `event: heartbeat` مرئي يعرض للواجهة أن الوكيل ما زال يعمل؛ اختبار انتظار خامل اصطناعي نجح (1/1) وأكد heartbeat ثم النتيجة النهائية. أُتبع ذلك بطلب حي استمر 357 ثانية، موثق أدناه؛ لا يزال تحقق Windows اليدوي مطلوبًا.
|
||||
|
||||
@@ -1,7 +1,107 @@
|
||||
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(
|
||||
'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({
|
||||
|
||||
Reference in New Issue
Block a user