Files
zyt/server/tests/PatientAiReportSnapshotTest.php
T
2026-08-14 14:37:30 +08:00

239 lines
9.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\tcm\PatientAiReportLogic;
function patientSnapshotExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
$reflection = new ReflectionClass(PatientAiReportLogic::class);
$build = $reflection->getMethod('buildSourceSnapshotFromRows');
$canonicalJson = $reflection->getMethod('canonicalJson');
$sanitize = $reflection->getMethod('sanitizeSnapshotForUpstream');
$decodeAttachments = $reflection->getMethod('decodeAttachmentArray');
patientSnapshotExpect(
$decodeAttachments->invoke(null, 'https://legacy.test/only.pdf') === ['https://legacy.test/only.pdf'],
'legacy single-URL attachment is retained as one item'
);
patientSnapshotExpect(
$decodeAttachments->invoke(null, '/a.pdf, /b.jpg/c.png') === ['/a.pdf', '/b.jpg', '/c.png'],
'legacy ASCII and Chinese comma-delimited attachments are all retained'
);
patientSnapshotExpect(
$decodeAttachments->invoke(null, '"/quoted-single.pdf"') === ['/quoted-single.pdf'],
'legacy JSON string attachment is retained as one item'
);
$sources = [
'patient_id' => 88,
'diagnoses' => [[
'id' => 101,
'patient_id' => 88,
'patient_name' => '张某',
'gender' => 1,
'age' => 52,
'diagnosis_date' => 1722384000,
'symptoms' => '口渴、乏力',
'tongue_coating' => '舌红,苔薄黄',
'pulse' => '弦数',
'doctor_advice' => '复查空腹血糖',
'report_files' => '["https://private.test/report-a.pdf?token=secret"]',
]],
'doctor_notes' => [[
'id' => 1,
'diagnosis_id' => 101,
'doctor_id' => 7,
'note_date' => '2026-08-01',
'content' => '舌苔较前转薄,检验报告待复核',
'tongue_images' => '["/uploads/tongue.jpg"]',
'report_files' => '["/uploads/lab.pdf"]',
]],
'tracking_notes' => [[
'id' => 2,
'diagnosis_id' => 101,
'admin_id' => 8,
'note_date' => '2026-08-02',
'content' => '患者自述夜间口渴减轻',
]],
'blood_records' => [[
'id' => 3,
'diagnosis_id' => 101,
'patient_id' => 88,
'record_date' => 1785600000,
'fasting_blood_sugar' => '7.1',
'systolic_pressure' => 128,
'diastolic_pressure' => 82,
]],
'diet_records' => [[
'id' => 4,
'diagnosis_id' => 101,
'patient_id' => 88,
'record_date' => 1785600000,
'breakfast_foods' => '鸡蛋、燕麦',
]],
'exercise_records' => [[
'id' => 5,
'diagnosis_id' => 101,
'patient_id' => 88,
'record_date' => 1785600000,
'exercise_type' => '步行',
'duration' => 35,
'intensity' => 2,
]],
'im_messages' => [[
'id' => 6,
'diagnosis_id' => 101,
'patient_id' => 88,
'msg_time' => 1785600100,
'is_from_doctor' => 0,
'msg_type' => 'text',
'text' => '今天空腹血糖7.1',
'file_name' => 'patient-zhang-lab-result.pdf',
'from_staff_name' => '王医生',
]],
'wechat_messages' => [[
'id' => 7,
'diagnosis_id' => 101,
'patient_id' => 88,
'chat_time' => 1785600200,
'direction' => 0,
'msg_type' => 'text',
'content' => '请按时复诊',
]],
'call_records' => [[
'id' => 9,
'diagnosis_id' => 101,
'call_type' => 2,
'status' => 2,
'start_time' => 1785600300,
'duration' => 600,
'recording_urls' => '["https://private.test/playback.m3u8?sign=sensitive"]',
'recording_status' => 2,
]],
'transcript_segments' => [
[
'id' => 10,
'call_record_id' => 9,
'transcription_session_id' => 'session-secret',
'segment_id' => 'segment-1',
'speaker_role' => 'doctor',
'timestamp_ms' => 1000,
'text' => '最近口渴是否减轻?',
],
[
'id' => 11,
'call_record_id' => 9,
'transcription_session_id' => 'session-secret',
'segment_id' => 'segment-2',
'speaker_role' => 'patient',
'timestamp_ms' => 2000,
'text' => '减轻了,联系电话13812345678。',
],
],
];
$snapshot = $build->invoke(null, $sources);
patientSnapshotExpect(is_array($snapshot), 'snapshot is structured');
patientSnapshotExpect($snapshot['patient']['patient_id'] === 88, 'stable patient id is retained');
patientSnapshotExpect($snapshot['patient']['patient_name'] === '张某', 'server snapshot retains audited patient identity');
patientSnapshotExpect($snapshot['diagnoses'][0]['tongue_coating'] === '舌红,苔薄黄', 'tongue coating is aggregated');
patientSnapshotExpect($snapshot['diagnoses'][0]['pulse'] === '弦数', 'pulse is aggregated');
patientSnapshotExpect($snapshot['diagnoses'][0]['doctor_advice'] === '复查空腹血糖', 'diagnosis doctor advice is aggregated');
patientSnapshotExpect($snapshot['doctor_notes'][0]['content'] === '舌苔较前转薄,检验报告待复核', 'doctor notes are aggregated');
patientSnapshotExpect(count($snapshot['doctor_notes'][0]['report_files']) === 1, 'doctor report attachment records are aggregated');
patientSnapshotExpect(count($snapshot['daily_records']['blood_glucose_pressure']) === 1, 'blood daily records are aggregated');
patientSnapshotExpect(count($snapshot['daily_records']['diet']) === 1, 'diet daily records are aggregated');
patientSnapshotExpect(count($snapshot['daily_records']['exercise']) === 1, 'exercise daily records are aggregated');
patientSnapshotExpect(count($snapshot['chat_records']['tencent_im']) === 1, 'IM chat is aggregated');
patientSnapshotExpect(count($snapshot['chat_records']['wechat_work']) === 1, 'WeChat Work chat is aggregated');
patientSnapshotExpect(count($snapshot['video_calls'][0]['segments']) === 2, 'every call includes transcript segments');
patientSnapshotExpect(
str_contains($snapshot['video_calls'][0]['transcript_text'], '医生:最近口渴是否减轻?')
&& str_contains($snapshot['video_calls'][0]['transcript_text'], '患者:减轻了'),
'transcript_text is rebuilt from segments when live call columns are absent'
);
patientSnapshotExpect(count($snapshot['video_calls'][0]['recording_urls']) === 1, 'playback records remain in server snapshot');
$summary = $snapshot['source_summary'];
foreach ([
'diagnosis_count' => 1,
'doctor_note_count' => 1,
'tracking_note_count' => 1,
'blood_record_count' => 1,
'diet_record_count' => 1,
'exercise_record_count' => 1,
'im_message_count' => 1,
'wechat_message_count' => 1,
'call_record_count' => 1,
'transcript_segment_count' => 2,
'recording_asset_count' => 1,
] as $field => $count) {
patientSnapshotExpect($summary[$field] === $count, "summary {$field} is correct");
}
$canonicalOne = $canonicalJson->invoke(null, $snapshot);
$reordered = array_reverse($snapshot, true);
$canonicalTwo = $canonicalJson->invoke(null, $reordered);
patientSnapshotExpect(hash('sha256', $canonicalOne) === hash('sha256', $canonicalTwo), 'source hash is key-order stable');
$upstream = $sanitize->invoke(null, $snapshot);
$upstreamJson = json_encode($upstream, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
patientSnapshotExpect(!str_contains($upstreamJson, '张某'), 'patient name is removed upstream');
patientSnapshotExpect(!str_contains($upstreamJson, '13812345678'), 'phone embedded in transcript is redacted upstream');
patientSnapshotExpect(!str_contains($upstreamJson, 'private.test'), 'private attachment and playback URLs are removed upstream');
patientSnapshotExpect(!str_contains($upstreamJson, 'patient-zhang-lab-result.pdf'), 'attachment filename is removed upstream');
patientSnapshotExpect(!str_contains($upstreamJson, '王医生'), 'staff name is removed upstream');
patientSnapshotExpect($upstream['patient']['patient_id'] === '[已脱敏]', 'patient id is removed upstream');
patientSnapshotExpect(
$upstream['chat_records']['tencent_im'][0]['file_name'] === '[已脱敏]',
'filename-shaped fields are redacted upstream'
);
patientSnapshotExpect(str_contains($upstreamJson, 'attachment_count'), 'attachment presence remains available upstream');
$longText = str_repeat('超长病历段落甲乙丙。', 20000);
$manyNotes = [];
for ($index = 1; $index <= 240; $index++) {
$manyNotes[] = [
'id' => $index,
'diagnosis_id' => 501,
'content' => "随访记录-{$index}",
];
}
$completeSnapshot = $build->invoke(null, [
'patient_id' => 500,
'diagnoses' => [[
'id' => 501,
'patient_id' => 500,
'patient_name' => '完整性测试患者',
'symptoms' => $longText,
]],
'doctor_notes' => $manyNotes,
]);
patientSnapshotExpect(
$completeSnapshot['diagnoses'][0]['symptoms'] === $longText,
'long source text is not truncated in the persisted snapshot'
);
patientSnapshotExpect(
count($completeSnapshot['doctor_notes']) === 240
&& $completeSnapshot['doctor_notes'][0]['content'] === '随访记录-1'
&& $completeSnapshot['doctor_notes'][239]['content'] === '随访记录-240',
'large multi-record source sets retain every record in order'
);
patientSnapshotExpect(
$completeSnapshot['source_summary']['doctor_note_count'] === 240
&& $completeSnapshot['source_summary']['snapshot_complete'] === true
&& $completeSnapshot['source_summary']['may_be_truncated'] === false,
'source summary declares the complete untruncated multi-record snapshot'
);
echo "Patient AI report snapshot aggregation: OK\n";