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";