Files
zyt/server/tests/AssistantTimingErrorCodesR39Test.php
2026-10-27 14:03:35 +08:00

74 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\service\AssistantSseTiming;
$checks = 0;
function r39TimingExpect(bool $condition, string $message): void
{
global $checks;
++$checks;
if (!$condition) {
throw new RuntimeException($message);
}
}
$timing = new AssistantSseTiming();
$codes = [
'TCM_FACT_STATE_CONFLICT', 'TCM_REPORT_INCOMPLETE', 'TASK_ANALYSIS_MISSING',
'STREAM_CONTENT_MISMATCH', 'UPSTREAM_CLEANUP_PENDING',
];
foreach (['upstream_done', 'sse_done'] as $phase) {
foreach ($codes as $code) {
$event = $timing->event($phase, [
'status' => 'error', 'error_code' => $code,
'patient_id' => 123456, 'patient_name' => 'PRIVATE_PATIENT',
'content' => 'PRIVATE_MODEL_BODY',
'clinical_fact_state_issues' => ['UNKNOWN_TO_POSITIVE:PRIVATE_SOURCE'],
'token' => 'PRIVATE_TOKEN',
]);
r39TimingExpect(($event['error_code'] ?? null) === $code,
'Existing failure code must survive at ' . $phase . ': ' . $code);
r39TimingExpect(($event['status'] ?? null) === 'error' && $event['trace_id'] === $timing->id(),
'Failure status and request trace must remain paired');
r39TimingExpect(array_keys($event) === ['trace_id', 'phase', 'request_elapsed_ms', 'status', 'error_code'],
'Diagnostic addition must not retain clinical text, identities or credentials');
}
}
foreach (['TCM_FACT_STATE_CONFLICT PRIVATE_MODEL_BODY', 'UNKNOWN_TO_POSITIVE:PRIVATE_SOURCE',
'UNKNOWN_ERROR', 'UPSTREAM_CLEANUP_PENDING\nPRIVATE_TOKEN', [], 123, null] as $bad) {
r39TimingExpect(!array_key_exists('error_code', $timing->event('sse_done', ['error_code' => $bad])),
'Unlisted or untyped diagnostic values must still be discarded');
}
// Exercise the actual buffered log channel used by the SSE timing writer.
$app = new \think\App(dirname(__DIR__) . '/');
$directory = sys_get_temp_dir() . '/zyt-r39-timing-' . bin2hex(random_bytes(8));
$app->config->set(['default' => 'r39', 'channels' => ['r39' => [
'type' => 'File', 'path' => $directory, 'single' => 'timing', 'realtime_write' => false,
]]], 'log');
$app->make('log');
$file = $directory . '/timing.log';
try {
foreach ($codes as $code) {
$timing->log('sse_done', ['status' => 'error', 'error_code' => $code,
'content' => 'PRIVATE_MODEL_BODY', 'token' => 'PRIVATE_TOKEN']);
}
$logged = is_file($file) ? (string) file_get_contents($file) : '';
foreach ($codes as $code) {
r39TimingExpect(str_contains($logged, '"error_code":"' . $code . '"'),
'SSE exit path must persist the concrete safe code: ' . $code);
}
r39TimingExpect(str_contains($logged, $timing->id())
&& !str_contains($logged, 'PRIVATE_MODEL_BODY') && !str_contains($logged, 'PRIVATE_TOKEN'),
'Persisted failure remains correlatable and contains no raw payload');
} finally {
if (is_file($file)) unlink($file);
if (is_dir($directory)) rmdir($directory);
}
echo 'R39 timing error code checks: ' . $checks . " passed\n";