Files
zyt/server/tests/PrescriptionAiPolicyTest.php
2026-09-10 15:19:17 +08:00

68 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\prescriptionai\PrescriptionAiPolicy as Policy;
use app\common\service\prescriptionai\PrescriptionAiCipher as Cipher;
$checks = 0;
$expect = static function (bool $ok, string $message) use (&$checks): void {
if (!$ok) { throw new RuntimeException($message); }
$checks++;
};
$rx = ['id' => 1, 'diagnosis_id' => 10, 'patient_id' => 20, 'is_system_auto' => 0, 'void_status' => 0,
'age' => 50, 'prescription_type' => '饮片', 'herbs' => [['medicine_id' => 1, 'name' => '测试药', 'dosage' => 10, 'price' => 2]],
'aux_usage' => null, 'clinical_diagnosis' => 'fixture'];
$wire = $rx;
$wire['age'] = '50';
$wire['herbs'] = '[{"name":"测试药","medicine_id":"1","dosage":"10.00","price":999}]';
$wire['aux_usage'] = 'null';
$wire['audit_status'] = 1;
$wire['phone'] = '000000';
$expect(Policy::fingerprint($rx) === Policy::fingerprint($wire), 'JSON numbers, prices, audit and contact fields do not regenerate');
$changed = $rx;
$changed['herbs'][0]['dosage'] = 11;
$expect(Policy::fingerprint($rx) !== Policy::fingerprint($changed), 'dose change regenerates');
$changed = $rx;
$changed['herbs'][0]['processing'] = 'special fixture';
$expect(Policy::fingerprint($rx) !== Policy::fingerprint($changed), 'processing change regenerates');
foreach (['is_system_auto' => 1, 'void_status' => 1, 'delete_time' => 123, 'herbs' => '[]'] as $key => $value) {
$expect(!Policy::isManual(array_replace($rx, [$key => $value])), 'ineligible source: ' . $key);
}
$expect(Policy::aggregate(['success', 'retry_wait']) === 'running', 'retry remains active');
$expect(Policy::aggregate(['success', 'failed']) === 'partial', 'one result remains visible');
$expect(Policy::aggregate(['failed', 'failed']) === 'failed', 'both failures');
$expect(Policy::aggregate(['success', 'success']) === 'success', 'both results');
$expect(Policy::retryAt(3, 100, true, 3) === null && Policy::retryAt(1, 100, false, 3) === null, 'bounded and terminal failures');
$cipher = new Cipher(str_repeat('test-only-key-', 4));
$secret = ['report' => 'private synthetic fixture'];
$encrypted = $cipher->encrypt($secret, 'report:1');
$expect($encrypted !== $cipher->encrypt($secret, 'report:1'), 'fresh IV per encryption');
$expect($cipher->decrypt($encrypted, 'report:1') === $secret, 'authenticated round trip');
foreach (['wrong purpose', 'tampered', 'wrong key'] as $case) {
try {
$bytes = base64_decode(substr($encrypted, 3));
$bytes[30] = chr(ord($bytes[30]) ^ 1);
($case === 'wrong key' ? new Cipher(str_repeat('different-key-', 4)) : $cipher)->decrypt(
$case === 'tampered' ? 'v1:' . base64_encode($bytes) : $encrypted,
$case === 'wrong purpose' ? 'report:2' : 'report:1');
$expect(false, $case . ' rejected');
} catch (RuntimeException $e) { $checks++; }
}
$catalog = [['id' => 1, 'name' => '测试药']];
$candidate = ['prescription_type' => '饮片', 'dose_basis' => 'per_dose', 'herbs' => [[
'name' => '测试药', 'dosage' => 10, 'unit' => 'g', 'formula_type' => '主方', 'processing' => '无', 'instructions' => '无',
]]];
$compare = \app\common\service\prescriptionai\PrescriptionAiComparison::compare($rx, $candidate, $catalog);
$expect($compare['score'] === 100.0, 'explicit no additional processing matches catalog identity');
$candidate['herbs'][0]['processing'] = '未知';
$expect(\app\common\service\prescriptionai\PrescriptionAiComparison::compare($rx, $candidate, $catalog)['score'] === null, 'unknown processing is not absence');
$candidate['herbs'][0]['processing'] = '无';
$candidate['herbs'][0]['instructions'] = '先煎';
$candidate['herbs'][0]['dosage'] = 4;
$candidate['herbs'][] = array_replace($candidate['herbs'][0], ['dosage' => 6, 'instructions' => '后下']);
$expect(\app\common\service\prescriptionai\PrescriptionAiComparison::compare($rx, $candidate, $catalog)['score'] === null, 'different per-herb instructions prohibit duplicate merge');
echo "Prescription AI policy/cipher: {$checks} checks passed\n";