@@ -433,4 +433,103 @@ public function testDefaultTimeoutIsThirtySeconds(): void
433433 $ this ->assertTrue ($ result ['success ' ]);
434434 $ this ->assertEquals ('ok ' , $ result ['result ' ]);
435435 }
436+
437+ // =========================================================================
438+ // Error Handling and Edge Cases
439+ // =========================================================================
440+
441+ /**
442+ * Test execution timeout triggers error response
443+ */
444+ public function testExecuteTimeout (): void
445+ {
446+ // Use a very short timeout with code that sleeps
447+ $ result = $ this ->tinkerTools ->execute ('sleep(5); return "done"; ' , 1 );
448+
449+ $ this ->assertFalse ($ result ['success ' ]);
450+ $ this ->assertStringContainsString ('timed out ' , $ result ['error ' ]);
451+ $ this ->assertEquals ('RuntimeException ' , $ result ['type ' ]);
452+ }
453+
454+ /**
455+ * Test empty stdout from subprocess returns error with exit code
456+ */
457+ public function testEmptyStdoutReturnsError (): void
458+ {
459+ // Code that exits without output
460+ $ result = $ this ->tinkerTools ->execute ('exit(0); ' );
461+
462+ $ this ->assertFalse ($ result ['success ' ]);
463+ $ this ->assertArrayHasKey ('error ' , $ result );
464+ $ this ->assertArrayHasKey ('exit_code ' , $ result );
465+ }
466+
467+ /**
468+ * Test null PHP binary returns error
469+ */
470+ public function testNullPhpBinaryReturnsError (): void
471+ {
472+ // Use reflection to set phpBinary to a value that will make getPhpBinary return null
473+ $ tinkerTools = new TinkerTools ();
474+
475+ // Mock by setting an invalid configured path and clearing cached value
476+ $ originalConfig = Configure::read ('Synapse.tinker.php_binary ' );
477+ Configure::write ('Synapse.tinker.php_binary ' , '/definitely/not/a/real/path ' );
478+
479+ // Create fresh instance - the config path isn't executable so it will try other methods
480+ // We need to test the case where getPhpBinary returns null
481+ // This is hard to achieve without modifying system, so test the error message path instead
482+ $ tinkerTools ->setPhpBinary ('/nonexistent/php/binary ' );
483+ $ result = $ tinkerTools ->execute ('return 1; ' );
484+
485+ $ this ->assertFalse ($ result ['success ' ]);
486+ $ this ->assertArrayHasKey ('error ' , $ result );
487+
488+ Configure::write ('Synapse.tinker.php_binary ' , $ originalConfig );
489+ }
490+
491+ /**
492+ * Test getPhpBinary with non-executable configured path falls back
493+ */
494+ public function testGetPhpBinaryFallsBackWhenConfiguredPathNotExecutable (): void
495+ {
496+ $ originalConfig = Configure::read ('Synapse.tinker.php_binary ' );
497+
498+ // Set a path that exists but isn't executable (or doesn't exist)
499+ Configure::write ('Synapse.tinker.php_binary ' , '/tmp/not-executable-file ' );
500+
501+ $ tinkerTools = new TinkerTools ();
502+ $ phpBinary = $ tinkerTools ->getPhpBinary ();
503+
504+ // Should fall back to which php or PHP_BINARY
505+ $ this ->assertNotEquals ('/tmp/not-executable-file ' , $ phpBinary );
506+ $ this ->assertNotNull ($ phpBinary );
507+
508+ Configure::write ('Synapse.tinker.php_binary ' , $ originalConfig );
509+ }
510+
511+ /**
512+ * Test getBinPath uses ROOT constant when available
513+ */
514+ public function testGetBinPathUsesRootConstant (): void
515+ {
516+ $ tinkerTools = new TinkerTools ();
517+ $ binPath = $ tinkerTools ->getBinPath ();
518+
519+ // ROOT is defined in test environment
520+ $ this ->assertEquals (ROOT . '/bin ' , $ binPath );
521+ }
522+
523+ /**
524+ * Test process that outputs to stderr
525+ */
526+ public function testProcessWithStderrOutput (): void
527+ {
528+ // Trigger a PHP warning/notice that goes to stderr
529+ $ result = $ this ->tinkerTools ->execute ('trigger_error("test warning", E_USER_WARNING); return "ok"; ' );
530+
531+ // Should still succeed, warnings don't stop execution
532+ $ this ->assertTrue ($ result ['success ' ]);
533+ $ this ->assertEquals ('ok ' , $ result ['result ' ]);
534+ }
436535}
0 commit comments