142 lines
5.3 KiB
PHP
142 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\logic\tcm\PatientAiReportLogic;
|
|
|
|
final class PatientAiReportHistoryQueryDouble
|
|
{
|
|
/** @var array<int,array<string,mixed>> */
|
|
public static array $rows = [];
|
|
|
|
public static function where(string $field, $value): self
|
|
{
|
|
return new self();
|
|
}
|
|
|
|
public function field(array $fields): self
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function order(string $field, string $direction): self
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function select(): self
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
/** @return array<int,array<string,mixed>> */
|
|
public function toArray(): array
|
|
{
|
|
return self::$rows;
|
|
}
|
|
}
|
|
|
|
patientPermissionExpect(
|
|
class_alias(PatientAiReportHistoryQueryDouble::class, 'app\\common\\model\\tcm\\PatientAiReport'),
|
|
'history model test double is installed before logic autoload'
|
|
);
|
|
|
|
function patientPermissionExpect(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
fwrite(STDERR, "FAIL: {$message}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
$reflection = new ReflectionClass(PatientAiReportLogic::class);
|
|
$hasPermission = $reflection->getMethod('hasPermission');
|
|
patientPermissionExpect(
|
|
$hasPermission->invoke(null, 1, ['root' => 1], 'tcm.diagnosis/patientaireports') === true,
|
|
'root remains compatible without menu rows'
|
|
);
|
|
patientPermissionExpect(
|
|
$hasPermission->invoke(null, 0, ['root' => 0], 'tcm.diagnosis/patientaireports') === false,
|
|
'invalid unauthenticated admin fails closed'
|
|
);
|
|
|
|
$source = file_get_contents($reflection->getFileName());
|
|
patientPermissionExpect(is_string($source), 'logic source is readable');
|
|
patientPermissionExpect(
|
|
str_contains($source, 'MyPatientLogic::applyScope($query, $adminId, $adminInfo)'),
|
|
'patient access applies doctor/assistant/team department scope'
|
|
);
|
|
patientPermissionExpect(
|
|
str_contains($source, "->where('d.patient_id', \$patientId)")
|
|
&& str_contains($source, "->whereNull('d.delete_time')")
|
|
&& str_contains($source, "->where('d.status', 1)"),
|
|
'authorization derives visible diagnosis rows from the stable patient id'
|
|
);
|
|
patientPermissionExpect(
|
|
str_contains($source, "->whereIn('diagnosis_id', \$diagnosisIds)"),
|
|
'all subordinate sources are restricted to authorized diagnosis ids'
|
|
);
|
|
$historyMethod = $reflection->getMethod('buildHistoryPayload');
|
|
$sourceLines = file($reflection->getFileName());
|
|
$historySource = is_array($sourceLines) ? implode('', array_slice(
|
|
$sourceLines,
|
|
$historyMethod->getStartLine() - 1,
|
|
$historyMethod->getEndLine() - $historyMethod->getStartLine() + 1
|
|
)) : '';
|
|
patientPermissionExpect(
|
|
str_contains($historySource, "PatientAiReport::where('patient_id', \$patientId)")
|
|
&& str_contains($historySource, "self::decodeJsonArray(\$row['source_diagnosis_ids_json'] ?? '')")
|
|
&& str_contains($historySource, "array_filter(\$sourceDiagnosisIds")
|
|
&& str_contains($historySource, '!isset($authorized[$id])')
|
|
&& str_contains($historySource, "\$sourceDiagnosisIds = [(int) \$row['diagnosis_id']]")
|
|
&& !str_contains($historySource, "->whereIn('diagnosis_id', \$diagnosisIds)"),
|
|
'history requires every source diagnosis to remain authorized, with legacy diagnosis fallback only'
|
|
);
|
|
|
|
$baseRow = [
|
|
'patient_id' => 77,
|
|
'model_key' => 'qwen',
|
|
'model_name' => 'server-model',
|
|
'model_label' => 'Qwen',
|
|
'report_json' => '{"diagnosis":"诊断","risk_assessment":[],"treatment_advice":"建议"}',
|
|
'source_summary_json' => '{"diagnosis_count":2}',
|
|
'source_hash' => str_repeat('a', 64),
|
|
'prompt_version' => 'patient-longitudinal-report-v1',
|
|
'generated_at' => 1786665600,
|
|
'created_at' => 1786665600,
|
|
];
|
|
PatientAiReportHistoryQueryDouble::$rows = [
|
|
$baseRow + ['id' => 1, 'diagnosis_id' => 12, 'source_diagnosis_ids_json' => '[11,12]'],
|
|
$baseRow + ['id' => 2, 'diagnosis_id' => 12, 'source_diagnosis_ids_json' => '[11,99]'],
|
|
$baseRow + ['id' => 3, 'diagnosis_id' => 12, 'source_diagnosis_ids_json' => ''],
|
|
$baseRow + ['id' => 4, 'diagnosis_id' => null, 'source_diagnosis_ids_json' => '[]'],
|
|
];
|
|
$history = $historyMethod->invoke(null, 77, [11, 12], 1);
|
|
patientPermissionExpect(
|
|
array_column($history['reports'], 'id') === [1, 3],
|
|
'history executable filter keeps complete authorized and legacy rows but hides partial or missing source sets'
|
|
);
|
|
patientPermissionExpect(
|
|
$history['generated_report']['id'] === 1 && $history['report']['id'] === 1,
|
|
'generated report selection still works after complete-source authorization filtering'
|
|
);
|
|
patientPermissionExpect(
|
|
str_contains($source, "self::setError('患者不存在或无权访问')"),
|
|
'missing and unauthorized patients share a non-enumerating error'
|
|
);
|
|
|
|
$migration = file_get_contents(dirname(__DIR__) . '/database/migrations/2026_08_14_patient_ai_report.sql');
|
|
patientPermissionExpect(is_string($migration), 'permission migration is readable');
|
|
patientPermissionExpect(
|
|
substr_count($migration, "WHERE `perms` = 'tcm.diagnosis/patientAiReports'") >= 2,
|
|
'read permission registration is idempotent and addressable'
|
|
);
|
|
patientPermissionExpect(
|
|
substr_count($migration, "WHERE `perms` = 'tcm.diagnosis/generatePatientAiReport'") >= 2,
|
|
'generate permission registration is idempotent and addressable'
|
|
);
|
|
|
|
echo "Patient AI report permission scope: OK\n";
|