137 lines
8.4 KiB
PHP
137 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\service\AssistantSseTiming;
|
|
use app\adminapi\service\TcmClinicalFactStateGuard;
|
|
use app\adminapi\service\TcmConsultationPulsePolicy;
|
|
|
|
$checks = 0;
|
|
function clinicalTelemetryExpect(bool $condition, string $message): void
|
|
{
|
|
++$GLOBALS['checks'];
|
|
if (!$condition) throw new RuntimeException($message);
|
|
}
|
|
|
|
$timing = new AssistantSseTiming();
|
|
$ledger = TcmClinicalFactStateGuard::build(['present_illness' => '未记录固定刺痛。']);
|
|
$polarity = TcmClinicalFactStateGuard::validate($ledger, '当前无固定刺痛。');
|
|
$pulse = TcmConsultationPulsePolicy::validate($ledger, '脉象未记录。');
|
|
foreach (['polarity' => $polarity, 'pulse' => $pulse] as $kind => $review) {
|
|
$codes = array_column($review['issues'], 'code');
|
|
clinicalTelemetryExpect(!$review['valid'] && $codes !== [], 'Synthetic guard must produce concrete issues: ' . $kind);
|
|
$event = $timing->event('upstream_done', [
|
|
'status' => 'error', 'error_code' => 'TCM_FACT_STATE_CONFLICT',
|
|
'clinical_issue_codes' => $codes, 'clinical_issue_count' => count($codes), 'clinical_repair_count' => 1,
|
|
]);
|
|
clinicalTelemetryExpect($event['clinical_issue_codes'] === $codes
|
|
&& $event['clinical_issue_count'] === count($codes) && $event['clinical_repair_count'] === 1,
|
|
'Actual pulse and polarity issues remain distinguishable: ' . $kind);
|
|
clinicalTelemetryExpect($event['trace_id'] === $timing->id() && $event['phase'] === 'upstream_done',
|
|
'Clinical diagnostics retain their request trace and phase');
|
|
}
|
|
clinicalTelemetryExpect($timing->event('upstream_done', ['error_code' => 'PULSE_EXCLUDED_FROM_ASSESSMENT'])['error_code']
|
|
=== 'PULSE_EXCLUDED_FROM_ASSESSMENT', 'The precise pulse failure is an allowed terminal error code');
|
|
|
|
$private = [
|
|
'patient_id' => 7654321, 'patient_name' => 'PRIVATE_PATIENT', 'source_id' => 'PRIVATE_SOURCE_ID',
|
|
'source_ids' => ['s999'], 'content' => 'PRIVATE_MODEL_BODY', 'prompt' => 'PRIVATE_PROMPT',
|
|
'token' => 'PRIVATE_TOKEN', 'url' => 'https://private.invalid/PRIVATE_URL',
|
|
'clinical_fact_state_issues' => [['code' => 'UNKNOWN_TO_NEGATIVE', 'source_id' => 'PRIVATE_SOURCE_ID',
|
|
'quote' => 'PRIVATE_QUOTE']],
|
|
];
|
|
$badCodes = ['PRIVATE_MODEL_BODY', 'UNKNOWN_NEW_FAILURE', 'UNKNOWN_TO_NEGATIVE:s999',
|
|
"UNKNOWN_TO_NEGATIVE\nPRIVATE_TOKEN", ' UNKNOWN_TO_NEGATIVE', 'unknown_to_negative',
|
|
'', 123, 1.25, true, false, null, ['code' => 'UNKNOWN_TO_NEGATIVE'],
|
|
(object) ['code' => 'UNKNOWN_TO_NEGATIVE']];
|
|
$event = $timing->event('upstream_done', $private + [
|
|
'clinical_issue_codes' => array_merge($badCodes, ['UNKNOWN_TO_NEGATIVE', 'PULSE_EXCLUDED_FROM_ASSESSMENT',
|
|
'UNKNOWN_TO_NEGATIVE', 'PULSE_EXCLUDED_FROM_ASSESSMENT']),
|
|
'clinical_issue_count' => 4, 'clinical_repair_count' => 1,
|
|
]);
|
|
clinicalTelemetryExpect($event['clinical_issue_codes'] === ['UNKNOWN_TO_NEGATIVE', 'PULSE_EXCLUDED_FROM_ASSESSMENT'],
|
|
'Only exact safe strings survive, once each, in first occurrence order');
|
|
clinicalTelemetryExpect(array_keys($event) === ['trace_id', 'phase', 'request_elapsed_ms',
|
|
'clinical_issue_count', 'clinical_repair_count', 'clinical_issue_codes'],
|
|
'No raw source metadata, identity, content, URL or credential field survives');
|
|
clinicalTelemetryExpect($timing->event('upstream_done', ['clinical_issue_codes' => $badCodes])['clinical_issue_codes'] === [],
|
|
'An array containing only unlisted or invalid values becomes an empty safe list');
|
|
clinicalTelemetryExpect($timing->event('upstream_done', ['clinical_issue_codes' => ['PRIVATE_ARRAY_KEY' => 'UNKNOWN_TO_POSITIVE']])['clinical_issue_codes']
|
|
=== ['UNKNOWN_TO_POSITIVE'], 'Arbitrary input keys are not persisted');
|
|
|
|
foreach (['UNKNOWN_TO_NEGATIVE', 42, 1.5, true, null, (object) ['code' => 'UNKNOWN_TO_NEGATIVE']] as $bad) {
|
|
clinicalTelemetryExpect(!array_key_exists('clinical_issue_codes',
|
|
$timing->event('upstream_done', ['clinical_issue_codes' => $bad])), 'The list itself must be an array');
|
|
}
|
|
foreach (['clinical_issue_count', 'clinical_repair_count'] as $field) {
|
|
foreach (['2', 2.5, true, null, [], new stdClass()] as $bad) {
|
|
clinicalTelemetryExpect(!array_key_exists($field, $timing->event('upstream_done', [$field => $bad])),
|
|
'Clinical counts use integer-only timing validation');
|
|
}
|
|
clinicalTelemetryExpect($timing->event('upstream_done', [$field => -1])[$field] === 0,
|
|
'Clinical counts cannot become negative');
|
|
}
|
|
foreach (['prepare', 'sse_start', 'attachment', 'first_delta', 'sse_done'] as $phase) {
|
|
clinicalTelemetryExpect(!array_key_exists('clinical_issue_codes',
|
|
$timing->event($phase, ['clinical_issue_codes' => ['UNKNOWN_TO_NEGATIVE']])),
|
|
'Issue enums belong to the upstream completion diagnostic');
|
|
}
|
|
|
|
// The table spans every issue emitted by the five local guards used by this stream.
|
|
$allowed = [
|
|
'FACT_STATE_LEDGER_UNAVAILABLE', 'FACT_STATE_INVALID_UTF8', 'SOURCE_ANNOTATION_INVALID',
|
|
'UNSUPPORTED_NEGATIVE_REFERENCE', 'ATTRIBUTED_SOURCE_QUOTE_INVALID',
|
|
'UNKNOWN_TO_NEGATIVE', 'UNKNOWN_TO_POSITIVE', 'POSITIVE_TO_NEGATIVE', 'NEGATIVE_TO_POSITIVE',
|
|
'NEGATION_SCOPE_WIDENED', 'NEGATIVE_NOT_IN_SOURCE', 'NEGATIVE_TO_UNKNOWN',
|
|
'POSITIVE_TO_UNKNOWN', 'RECORDED_TO_UNKNOWN', 'TONGUE_FEATURE_NOT_IN_CURRENT_SOURCE',
|
|
'PULSE_FEATURE_NOT_IN_CURRENT_SOURCE', 'CONSULTATION_INVALID_UTF8', 'PULSE_EXCLUDED_FROM_ASSESSMENT',
|
|
'EVIDENCE_INDEX_UNAVAILABLE', 'EVIDENCE_INVALID_UTF8', 'EXAM_ROLE_MISSING',
|
|
'EXAM_HEADING_METADATA_UNSUPPORTED', 'EXAM_FACT_PROTOCOL_INVALID', 'EXAM_NEGATIVE_HEADING_UNSUPPORTED',
|
|
'LONGITUDINAL_FACT_PROTOCOL_INVALID', 'EXAM_KNOWN_ROLE_TO_UNKNOWN', 'EVIDENCE_SOURCE_DATE_MISMATCH',
|
|
'LONGITUDINAL_SOURCE_NOT_HISTORICAL', 'EXAM_SOURCE_QUOTE_INVALID', 'EXAM_SOURCE_NOT_CURRENT',
|
|
'EXAM_ROLE_MISMATCH', 'DENIAL_SOURCE_QUOTE_INVALID', 'UNQUOTED_PATIENT_DENIAL',
|
|
'HERBAL_PREMISE_SELF_CONFLICT', 'REPORT_BOUND_EXCEEDED', 'REPORT_FINAL_CONTRACT_INVALID',
|
|
'REPORT_CONTINUATION_REPEATED_SECTION', 'REPORT_FINAL_SECTION_UNDELIVERED',
|
|
];
|
|
foreach ($allowed as $code) {
|
|
clinicalTelemetryExpect($timing->event('upstream_done', ['clinical_issue_codes' => [$code]])['clinical_issue_codes'] === [$code],
|
|
'A current local guard issue must remain observable: ' . $code);
|
|
}
|
|
$bounded = $timing->event('upstream_done', [
|
|
'clinical_issue_codes' => array_merge(array_fill(0, 20, 'UNKNOWN_NEW_FAILURE'), array_fill(0, 20, $allowed[0]), $allowed),
|
|
]);
|
|
clinicalTelemetryExpect($bounded['clinical_issue_codes'] === array_slice($allowed, 0, 16),
|
|
'The maximum is sixteen unique accepted codes, independent of invalid or duplicate entries');
|
|
|
|
// Exercise the same immediate File-channel write used before SSE exits.
|
|
$app = new \think\App(dirname(__DIR__) . '/');
|
|
$directory = sys_get_temp_dir() . '/zyt-clinical-telemetry-' . bin2hex(random_bytes(8));
|
|
$app->config->set(['default' => 'clinical', 'channels' => ['clinical' => [
|
|
'type' => 'File', 'path' => $directory, 'single' => 'timing', 'realtime_write' => false,
|
|
]]], 'log');
|
|
$app->make('log');
|
|
$file = $directory . '/timing.log';
|
|
try {
|
|
$timing->log('upstream_done', $private + [
|
|
'status' => 'error', 'error_code' => 'PULSE_EXCLUDED_FROM_ASSESSMENT',
|
|
'clinical_issue_codes' => ['PULSE_EXCLUDED_FROM_ASSESSMENT', 'UNKNOWN_TO_NEGATIVE', 'PRIVATE_QUOTE'],
|
|
'clinical_issue_count' => 3, 'clinical_repair_count' => 1,
|
|
]);
|
|
clinicalTelemetryExpect(is_file($file), 'The telemetry is written before framework request-end flushing');
|
|
$logged = is_file($file) ? (string) file_get_contents($file) : '';
|
|
clinicalTelemetryExpect(str_contains($logged, '"clinical_issue_codes":["PULSE_EXCLUDED_FROM_ASSESSMENT","UNKNOWN_TO_NEGATIVE"]')
|
|
&& str_contains($logged, '"clinical_issue_count":3') && str_contains($logged, '"clinical_repair_count":1')
|
|
&& str_contains($logged, '"error_code":"PULSE_EXCLUDED_FROM_ASSESSMENT"')
|
|
&& str_contains($logged, $timing->id()), 'Persisted failure contains only safe diagnostics correlated to this request');
|
|
foreach (['PRIVATE_', '7654321', 's999', 'private.invalid', 'clinical_fact_state_issues', 'source_id'] as $secret) {
|
|
clinicalTelemetryExpect(!str_contains($logged, $secret), 'Persisted diagnostics must discard all raw source and private payloads');
|
|
}
|
|
} finally {
|
|
if (is_file($file)) unlink($file);
|
|
if (is_dir($directory)) rmdir($directory);
|
|
}
|
|
|
|
echo 'Clinical failure telemetry: ' . $checks . " checks passed (pure offline fixtures).\n";
|