Files
zyt/server/tests/NihaixiaClinicalSkillTest.php
T
2026-10-09 09:36:17 +08:00

85 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\NihaixiaClinicalSkill;
function skillExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
$reference = NihaixiaClinicalSkill::reference();
skillExpect(str_contains($reference, NihaixiaClinicalSkill::VERSION), 'pinned skill version is present');
skillExpect(str_contains($reference, '六经主证:太阳:'), 'reference is read from the installed six-meridian table');
skillExpect(str_contains($reference, '条件线索:太阳病(脉浮 + 头项强痛 + 恶寒)'), 'clinical clues are read from the installed diagnostic formulas');
skillExpect(str_contains($reference, '辨证步骤:定表里→分阴阳→辨寒热'), 'seven-step method is read from the installed skill');
skillExpect(str_contains($reference, '未满足关键证据时不得强行归经'), 'diagnostic clues require patient evidence');
skillExpect(str_contains($reference, '不得据此建议停用现有治疗'), 'historical cases cannot override treatment safety');
skillExpect(strlen($reference) < 3000, 'reference is bounded for staged clinical prompts');
$question = '请分析当前患者的口渴和高血糖。';
$query = NihaixiaClinicalSkill::augmentQuery($question);
skillExpect(str_contains($query, $question), 'original clinical question is preserved');
skillExpect(substr_count($query, NihaixiaClinicalSkill::VERSION) === 1, 'query receives the skill once');
skillExpect(NihaixiaClinicalSkill::augmentQuery($query) === $query, 'query retries do not duplicate the skill');
$messages = [
['role' => 'system', 'content' => '只输出 JSON'],
['role' => 'user', 'content' => $question],
];
$augmented = NihaixiaClinicalSkill::augmentMessages($messages);
skillExpect(count($augmented) === 3, 'chat messages receive one skill system reference');
skillExpect(($augmented[1]['content'] ?? '') === '只输出 JSON', 'original system format is preserved');
skillExpect(($augmented[2]['content'] ?? '') === $question, 'original user question is preserved');
skillExpect(NihaixiaClinicalSkill::augmentMessages($augmented) === $augmented, 'stream retries do not duplicate the skill');
$source = NihaixiaClinicalSkill::knowledgeSource();
skillExpect($source['commit'] === '270e94e637c4249dc411e54ac6715f21247de3cb', 'metadata pins the complete vendored commit');
skillExpect($source['url'] === '' && $source['path'] === 'SKILL.md', 'metadata points to the locally installed skill without an external link');
skillExpect($source['sections'] === ['六经速查表', '六经辨证诊断公式', '七步走辨证思维模式'], 'metadata names only the extracted sections');
$savedSource = $source;
$savedSource['url'] = 'https://github.com/coinweth/nihaixia-skill/blob/' . $source['commit'] . '/SKILL.md';
unset($savedSource['path']);
$localizedSource = NihaixiaClinicalSkill::localizeKnowledgeSource($savedSource);
skillExpect($localizedSource['url'] === '' && $localizedSource['path'] === 'SKILL.md'
&& $localizedSource['commit'] === $source['commit'] && $localizedSource['sections'] === $source['sections'],
'saved skill source loses the external URL and retains its local file');
skillExpect(NihaixiaClinicalSkill::knowledgeSourceForPromptVersion('patient-longitudinal-report-v2') === null, 'legacy prompt versions do not acquire fabricated sources');
skillExpect(NihaixiaClinicalSkill::knowledgeSourceForPromptVersion('patient-longitudinal-report-nihaixia-ancient-v4') === $source, 'new ancient-book reports retain the existing skill attribution');
foreach ([
[app\adminapi\logic\tcm\DiagnosisAiLogic::class, ['report_content' => '{}']],
[app\adminapi\logic\tcm\PatientAiReportLogic::class, ['report_json' => '{}', 'source_summary_json' => '{"diagnosis_count":2}', 'source_diagnosis_ids_json' => '[11,12]', 'source_snapshot' => 'PRIVATE PATIENT INPUT']],
] as [$class, $row]) {
$format = new ReflectionMethod($class, 'formatReportRow');
$args = $class === app\adminapi\logic\tcm\DiagnosisAiLogic::class ? ['',] : [];
$legacy = $format->invoke(null, $row + ['prompt_version' => 'report-v2'], ...$args);
$currentVersion = $class === app\adminapi\logic\tcm\DiagnosisAiLogic::class
? 'patient-context-case-explain-nihaixia-v3'
: 'patient-longitudinal-report-nihaixia-v3';
$current = $format->invoke(null, $row + ['prompt_version' => $currentVersion], ...$args);
skillExpect($legacy['knowledge_source'] === null, $class . ' omits a claim for old reports');
skillExpect($current['knowledge_source'] === $source, $class . ' returns a pinned source for saved skill reports');
skillExpect(!str_contains(json_encode($current), 'PRIVATE PATIENT INPUT'), 'public report metadata does not expose the source snapshot');
if ($class === app\adminapi\logic\tcm\PatientAiReportLogic::class) {
skillExpect($current['source_summary'] === ['diagnosis_count' => 2], 'patient summary comes from the saved report');
skillExpect($current['source_diagnosis_ids'] === [11, 12], 'patient source IDs come from the saved report');
}
}
$formatDiagnosis = new ReflectionMethod(app\adminapi\logic\tcm\DiagnosisAiLogic::class, 'formatReportRow');
$currentSources = ['source_summary' => ['diagnosis_count' => 2], 'source_diagnosis_ids' => [11, 12]];
$matching = $formatDiagnosis->invoke(null, ['case_fingerprint' => 'matching', 'report_content' => '{}'], 'matching', $currentSources);
$stale = $formatDiagnosis->invoke(null, ['case_fingerprint' => 'old', 'report_content' => '{}'], 'matching', $currentSources);
skillExpect($matching['source_diagnosis_ids'] === [11, 12], 'matching report exposes current source IDs');
skillExpect($matching['source_summary'] === ['diagnosis_count' => 2], 'matching report exposes current source counts');
skillExpect($stale['source_diagnosis_ids'] === [] && $stale['source_summary'] === [], 'stale report does not borrow current patient sources');
echo "Nihaixia clinical skill contract passed.\n";