[$unit, $expected]) { rejectDiagnosticExpect(AssistantClinicalRejectDiagnostic::forms($unit) === $expected, 'Finite lexical classification fixture ' . $index); } $timing = new AssistantSseTiming(); $private = 'SYNTHETIC_PHI_SENTINEL_患者姓名_123456789_任务_TASK_URL_https://private.invalid/s999'; $issues = [ ['code' => 'PULSE_EXCLUDED_FROM_ASSESSMENT', 'source_id' => $private, 'quote' => $private, 'field' => $private], ['code' => 'UNKNOWN_TO_NEGATIVE', 'source_id' => 's999'], ['code' => 'PULSE_EXCLUDED_FROM_ASSESSMENT'], ['code' => $private], ['code' => 'UNKNOWN_TO_NEGATIVE:s999'], ['code' => "UNKNOWN_TO_NEGATIVE\n" . $private], ['code' => ['UNKNOWN_TO_NEGATIVE']], ['code' => new stdClass()], $private, 17, null, (object) ['code' => 'UNKNOWN_TO_POSITIVE'], ]; $event = AssistantClinicalRejectDiagnostic::event($timing, $private . ',脉象未记录。', $issues); rejectDiagnosticExpect(array_keys($event) === ['trace_id', 'pulse_forms', 'clinical_issue_codes', 'unit_chars'], 'The event has exactly four safe fields'); rejectDiagnosticExpect($event['trace_id'] === $timing->id() && $event['clinical_issue_codes'] === ['PULSE_EXCLUDED_FROM_ASSESSMENT', 'UNKNOWN_TO_NEGATIVE'] && $event['pulse_forms'] === ['missing_pulse_statement'] && $event['unit_chars'] === mb_strlen($private . ',脉象未记录。', 'UTF-8'), 'Only generated trace, finite forms, exact codes and character count survive'); rejectDiagnosticExpect(!str_contains(json_encode($event), $private), 'The event never contains synthetic private text'); $allowed = (new ReflectionClass(AssistantSseTiming::class))->getReflectionConstant('CLINICAL_ISSUE_CODES')->getValue(); $bounded = AssistantClinicalRejectDiagnostic::event($timing, str_repeat('字', 100001), array_map(static fn (string $code): array => ['code' => $code], array_merge($allowed, $allowed))); rejectDiagnosticExpect($bounded['clinical_issue_codes'] === array_slice($allowed, 0, 16) && $bounded['pulse_forms'] === ['other'] && $bounded['unit_chars'] === 100001, 'Unknown prose stays bounded and issue codes retain the existing sixteen-code limit'); // Use only an isolated temporary File log; no application boot, network or database. $app = new \think\App(dirname(__DIR__) . '/'); $directory = sys_get_temp_dir() . '/zyt-reject-diagnostic-' . bin2hex(random_bytes(8)); $app->config->set(['default' => 'diagnostic', 'channels' => ['diagnostic' => [ 'type' => 'File', 'path' => $directory, 'single' => 'rejection', 'realtime_write' => false, ]]], 'log'); $app->make('log'); $file = $directory . '/rejection.log'; try { AssistantClinicalRejectDiagnostic::log($timing, $private . ',本流程不采集脉象。', $issues); rejectDiagnosticExpect(is_file($file), 'The diagnostic reaches the immediate File write before SSE exits'); $logged = is_file($file) ? (string) file_get_contents($file) : ''; rejectDiagnosticExpect(str_contains($logged, '"pulse_forms":["workflow_rule_narration"]') && str_contains($logged, '"clinical_issue_codes":["PULSE_EXCLUDED_FROM_ASSESSMENT","UNKNOWN_TO_NEGATIVE"]') && str_contains($logged, $timing->id()), 'Persisted diagnostics retain only bounded correlated metadata'); foreach ([$private, 'SYNTHETIC_PHI', '123456789', 'private.invalid', 'source_id', 'quote', 'field', 's999', 'TASK_URL'] as $secret) { rejectDiagnosticExpect(!str_contains($logged, $secret), 'Persisted output discards arbitrary private fields and issue values'); } // Execute the exact controller callback with a throwing logger to verify the // production observer cannot turn a logging failure into a stream failure. $controller = (string) file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/tcm/DiagnosisController.php'); $pattern = <<<'REGEX' ~'on_clinical_rejection'\s*=>\s*(static function \(string \$unit, array \$issues\) use \(\$timing\): void \{.*?\n \}),~s REGEX; rejectDiagnosticExpect(preg_match($pattern, $controller, $matches) === 1, 'Controller installs the rejection observer'); $callback = eval('use app\\adminapi\\service\\AssistantClinicalRejectDiagnostic; return ' . $matches[1] . ';'); $throwingLog = new class { public int $calls = 0; public function write(...$arguments): void { ++$this->calls; throw new RuntimeException('synthetic log failure'); } }; $app->instance('log', $throwingLog); $callback($private, $issues); rejectDiagnosticExpect($throwingLog->calls === 1, 'Controller swallows diagnostic exceptions after calling the logger'); } finally { if (is_file($file)) unlink($file); if (is_dir($directory)) rmdir($directory); } echo 'Clinical rejection diagnostic: ' . $checks . " checks passed (synthetic offline fixtures).\n";