68 lines
3.4 KiB
PHP
68 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Source-level contract for the background consumer. No framework bootstrap, database or HTTP:
|
|
// the command itself is a long-running process and cannot be exercised inside a unit test.
|
|
|
|
// Config files read env(); supply the fixture lookup the same way the other config tests do.
|
|
if (!function_exists('env')) {
|
|
function env(string $key, $default = null)
|
|
{
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
function prescriptionAiWorkerExpect(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
}
|
|
|
|
$checks = 0;
|
|
$expect = static function (bool $condition, string $message) use (&$checks): void {
|
|
$checks++;
|
|
prescriptionAiWorkerExpect($condition, $message);
|
|
};
|
|
|
|
$command = (string) file_get_contents(dirname(__DIR__) . '/app/command/PrescriptionAiWork.php');
|
|
$configSource = (string) file_get_contents(dirname(__DIR__) . '/config/prescription_analysis.php');
|
|
$config = require dirname(__DIR__) . '/config/prescription_analysis.php';
|
|
|
|
// A model task keeps its database connection idle for the whole upstream call. MySQL's
|
|
// wait_timeout is commonly shorter than that, so the consumer must reconnect instead of
|
|
// looping on a dead connection forever.
|
|
$expect(str_contains($command, "'break_reconnect'] = true"),
|
|
'the consumer enables database reconnection for long model calls');
|
|
$expect(str_contains($command, 'Db::connect()->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";
|