first commit
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<?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";
|
||||
Reference in New Issue
Block a user