@@ -8,6 +8,7 @@ import 'package:querya_desktop/core/extensions/models/extension_type.dart';
88import 'package:querya_desktop/core/extensions/models/sandbox_capabilities.dart' ;
99import 'package:querya_desktop/core/extensions/rpc/plugin_rpc_bridge.dart' ;
1010import 'package:querya_desktop/core/extensions/rpc/plugin_rpc_exceptions.dart' ;
11+ import 'package:querya_desktop/core/extensions/sandbox/sandbox_log_paths.dart' ;
1112import 'package:querya_desktop/core/extensions/sandbox/sandbox_process_runner.dart' ;
1213
1314class _FakeProcess implements Process {
@@ -66,9 +67,11 @@ void main() {
6667
6768 setUp (() async {
6869 tempBase = await Directory .systemTemp.createTemp ('querya_rpc_bridge_' );
70+ SandboxLogPaths .mockLogsDirectory = tempBase;
6971 });
7072
7173 tearDown (() async {
74+ SandboxLogPaths .mockLogsDirectory = null ;
7275 try {
7376 if (await tempBase.exists ()) {
7477 await tempBase.delete (recursive: true );
@@ -316,4 +319,105 @@ void main() {
316319 expect (bridge.isStarted, isFalse);
317320 await sub.cancel ();
318321 });
322+
323+ test ('unexpected exit fires onProcessExited callback' , () async {
324+ final process = _FakeProcess ();
325+ final sub = process.stdinLines.listen ((line) {
326+ final req = jsonDecode (line) as Map <String , dynamic >;
327+ if (req['method' ] == 'system.handshake' ) {
328+ process.reply ({
329+ 'jsonrpc' : '2.0' ,
330+ 'id' : req['id' ] as int ,
331+ 'result' : {'ok' : true },
332+ });
333+ }
334+ });
335+
336+ final runner = SandboxProcessRunner (
337+ platformOverride: 'windows' ,
338+ scratchBaseDirectory: tempBase,
339+ processStarter: (
340+ exe,
341+ args, {
342+ String ? workingDirectory,
343+ Map <String , String >? environment,
344+ bool includeParentEnvironment = true ,
345+ bool runInShell = false ,
346+ ProcessStartMode mode = ProcessStartMode .normal,
347+ }) async =>
348+ process,
349+ );
350+
351+ int ? capturedExitCode;
352+ final bridge = PluginRpcBridge (
353+ processRunner: runner,
354+ enableWatchdog: false ,
355+ enableStderrPipe: false ,
356+ onProcessExited: (code) {
357+ capturedExitCode = code;
358+ },
359+ );
360+
361+ await bridge.start (
362+ manifest: testManifest,
363+ pluginExecutable: '/opt/driver' ,
364+ allowUnsandboxedLaunch: true ,
365+ );
366+
367+ process.completeExit (137 ); // SIGKILL
368+ await Future <void >.delayed (const Duration (milliseconds: 20 ));
369+
370+ expect (capturedExitCode, 137 );
371+ expect (bridge.isStarted, isFalse);
372+ await sub.cancel ();
373+ });
374+
375+ test ('handshake timeout includes recent stderr output in exception' , () async {
376+ final process = _FakeProcess ();
377+ final sub = process.stdinLines.listen ((_) {
378+ process._stderrController.add (
379+ utf8.encode ('FATAL: libclickhouse.so not found\n ' ),
380+ );
381+ });
382+
383+ final runner = SandboxProcessRunner (
384+ platformOverride: 'windows' ,
385+ scratchBaseDirectory: tempBase,
386+ processStarter: (
387+ exe,
388+ args, {
389+ String ? workingDirectory,
390+ Map <String , String >? environment,
391+ bool includeParentEnvironment = true ,
392+ bool runInShell = false ,
393+ ProcessStartMode mode = ProcessStartMode .normal,
394+ }) async =>
395+ process,
396+ );
397+
398+ final bridge = PluginRpcBridge (
399+ processRunner: runner,
400+ handshakeTimeout: const Duration (milliseconds: 50 ),
401+ enableWatchdog: false ,
402+ enableStderrPipe: true ,
403+ );
404+
405+ final startFuture = bridge.start (
406+ manifest: testManifest,
407+ pluginExecutable: '/opt/driver' ,
408+ allowUnsandboxedLaunch: true ,
409+ );
410+
411+ await expectLater (
412+ startFuture,
413+ throwsA (
414+ isA <PluginProtocolTimeoutException >().having (
415+ (e) => e.message,
416+ 'message' ,
417+ contains ('FATAL: libclickhouse.so not found' ),
418+ ),
419+ ),
420+ );
421+ await sub.cancel ();
422+ });
319423}
0 commit comments