close()'), 'a failed round drops the possibly dead connection before the next round'); $expect(preg_match('/catch \(\\\\Throwable \$e\) \{[^}]*get_class\(\$e\)/s', $command) === 1, 'the failure line names the exception class so a wedged consumer can be diagnosed'); $expect(str_contains($command, "SQLSTATE\\[[A-Z0-9]{5}\\]"), 'database failures record their SQLSTATE'); $expect(!str_contains($command, '$e->getMessage()') || !str_contains($command, "writeln('PRESCRIPTION_AI storage_or_configuration_error ' . \$e->getMessage()"), 'the raw exception message, which can carry SQL values or clinical text, is never printed'); // The lease must outlast one upstream request, otherwise a healthy task looks abandoned. $requestTimeout = (int) (require dirname(__DIR__) . '/config/prescription_ai.php')['manual_analysis']['request_timeout']; $expect($requestTimeout > 0 && $requestTimeout < (int) $config['lease_seconds'], 'one request budget stays well inside the task lease'); $expect((int) $config['max_attempts'] >= 1 && (int) $config['lease_seconds'] >= 60, 'lease and attempt limits stay within a recoverable range'); // Several prescriptions must be analysed at the same time instead of queueing one by one. $expect((int) $config['max_parallel_per_model'] >= 2, 'more than one task may run per model'); $expect(str_contains($configSource, "env('prescription_analysis.MAX_PARALLEL_PER_MODEL'"), 'the concurrency limit is tunable without a code edit'); $expect(str_contains($configSource, "env('prescription_analysis.DAILY_PATIENT_BATCHES'") && str_contains($configSource, "env('prescription_analysis.DAILY_MODEL_TASKS'"), 'the daily budgets are tunable without a code edit'); // A context rebuild sweep must never delay a prescription that is waiting to be prepared. $expect(str_contains($command, '!$worked && time() >= $sweepAt'), 'source sweeps only run while the prepare lane has nothing to claim'); echo 'Prescription AI worker resilience: ' . $checks . " checks passed\n";