177 lines
6.6 KiB
PHP
177 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic;
|
|
use app\adminapi\validate\tcm\DiagnosisValidate;
|
|
use think\helper\Str;
|
|
|
|
function analysisContractExpect(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
fwrite(STDERR, "FAIL: {$message}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
$logicReflection = new ReflectionClass(DiagnosisAiLogic::class);
|
|
analysisContractExpect(
|
|
$logicReflection->getConstant('PERMISSION_ANALYSIS') === 'tcm.diagnosis/aianalysis',
|
|
'logic enforces the exact normalized aiAnalysis permission'
|
|
);
|
|
analysisContractExpect(
|
|
strtolower(Str::camel('tcm.diagnosis/aiAnalysis')) === 'tcm.diagnosis/aianalysis',
|
|
'middleware normalization matches the logic permission'
|
|
);
|
|
$hasPermission = $logicReflection->getMethod('hasPermission');
|
|
analysisContractExpect(
|
|
$hasPermission->invoke(null, 0, ['root' => 1], 'tcm.diagnosis/aianalysis') === true,
|
|
'super administrator remains compatible without a role-menu row'
|
|
);
|
|
|
|
$analysisMethod = $logicReflection->getMethod('analysis');
|
|
$analysisParameters = $analysisMethod->getParameters();
|
|
analysisContractExpect(count($analysisParameters) === 4, 'analysis accepts an optional model key');
|
|
analysisContractExpect(
|
|
$analysisParameters[3]->getName() === 'modelKey'
|
|
&& $analysisParameters[3]->isDefaultValueAvailable()
|
|
&& $analysisParameters[3]->getDefaultValue() === 'qwen',
|
|
'internal legacy calls also default to qwen'
|
|
);
|
|
$logicLines = file($logicReflection->getFileName());
|
|
analysisContractExpect(is_array($logicLines), 'logic source is readable');
|
|
$logicSource = implode('', $logicLines);
|
|
$analysisSource = implode('', array_slice(
|
|
$logicLines,
|
|
$analysisMethod->getStartLine() - 1,
|
|
$analysisMethod->getEndLine() - $analysisMethod->getStartLine() + 1
|
|
));
|
|
analysisContractExpect(
|
|
substr_count($analysisSource, 'DifyChatService::chat(') === 1,
|
|
'one analysis request performs exactly one upstream chat call'
|
|
);
|
|
foreach ([
|
|
"'diagnosis_advice'",
|
|
"'risk_assessment'",
|
|
"'treatment_advice'",
|
|
] as $field) {
|
|
analysisContractExpect(str_contains($logicSource, $field), "response contains {$field}");
|
|
}
|
|
foreach ([
|
|
"'model_key'",
|
|
"'model_label'",
|
|
"'model_name'",
|
|
"'generated_at'",
|
|
] as $field) {
|
|
analysisContractExpect(str_contains($analysisSource, $field), "response contains {$field}");
|
|
}
|
|
|
|
$controller = file_get_contents(
|
|
dirname(__DIR__) . '/app/adminapi/controller/tcm/DiagnosisController.php'
|
|
);
|
|
analysisContractExpect(is_string($controller), 'controller source is readable');
|
|
analysisContractExpect(
|
|
str_contains($controller, 'public function aiAnalysis()'),
|
|
'POST action name is aiAnalysis'
|
|
);
|
|
analysisContractExpect(
|
|
str_contains($controller, "goCheck('aiAnalysis')"),
|
|
'aiAnalysis uses its strict validation scene'
|
|
);
|
|
analysisContractExpect(
|
|
str_contains($controller, 'DiagnosisAiLogic::analysis('),
|
|
'controller delegates to structured analysis logic'
|
|
);
|
|
analysisContractExpect(
|
|
str_contains($controller, "\$params['model'] ?? 'qwen'"),
|
|
'legacy requests without model default to qwen at the endpoint boundary'
|
|
);
|
|
|
|
$validator = new DiagnosisValidate();
|
|
$payloadCheck = (new ReflectionClass($validator))->getMethod('checkAiAnalysisPayload');
|
|
analysisContractExpect(
|
|
$payloadCheck->invoke($validator, 7, '', ['id' => 7]) === true,
|
|
'request accepts exactly id'
|
|
);
|
|
analysisContractExpect(
|
|
$payloadCheck->invoke($validator, 7, '', ['id' => 7, 'model' => 'qwen']) === true,
|
|
'request accepts explicit qwen model key'
|
|
);
|
|
analysisContractExpect(
|
|
$payloadCheck->invoke($validator, 7, '', ['id' => 7, 'model' => 'openai']) === true,
|
|
'request accepts explicit openai model key'
|
|
);
|
|
foreach (['provider', 'profile', 'key', 'api_key', 'base_url', 'prompt'] as $forbiddenField) {
|
|
analysisContractExpect(
|
|
$payloadCheck->invoke(
|
|
$validator,
|
|
7,
|
|
'',
|
|
['id' => 7, 'model' => 'qwen', $forbiddenField => 'client-controlled']
|
|
) !== true,
|
|
"request rejects forbidden {$forbiddenField} field"
|
|
);
|
|
}
|
|
foreach (['', 'QWEN', ' qwen', 'gpt-5.6-sol', 'other'] as $invalidModel) {
|
|
analysisContractExpect(
|
|
$payloadCheck->invoke($validator, 7, '', ['id' => 7, 'model' => $invalidModel]) !== true,
|
|
"request rejects non-whitelisted model value {$invalidModel}"
|
|
);
|
|
}
|
|
foreach ([null, 0, true, []] as $invalidModelType) {
|
|
analysisContractExpect(
|
|
$payloadCheck->invoke($validator, 7, '', ['id' => 7, 'model' => $invalidModelType]) !== true,
|
|
'request rejects non-string model value of type ' . get_debug_type($invalidModelType)
|
|
);
|
|
}
|
|
$validScene = (new DiagnosisValidate())->scene('aiAnalysis');
|
|
analysisContractExpect($validScene->check(['id' => 7]), 'full validation scene accepts an integer id');
|
|
$qwenScene = (new DiagnosisValidate())->scene('aiAnalysis');
|
|
analysisContractExpect(
|
|
$qwenScene->check(['id' => 7, 'model' => 'qwen']),
|
|
'full validation scene accepts qwen'
|
|
);
|
|
$openAiScene = (new DiagnosisValidate())->scene('aiAnalysis');
|
|
analysisContractExpect(
|
|
$openAiScene->check(['id' => 7, 'model' => 'openai']),
|
|
'full validation scene accepts openai'
|
|
);
|
|
$invalidScene = (new DiagnosisValidate())->scene('aiAnalysis');
|
|
analysisContractExpect(
|
|
!$invalidScene->check(['id' => 7, 'model' => 'QWEN']),
|
|
'full validation scene enforces exact lowercase model keys'
|
|
);
|
|
foreach ([null, 0, true, []] as $invalidModelType) {
|
|
$typedInvalidScene = (new DiagnosisValidate())->scene('aiAnalysis');
|
|
analysisContractExpect(
|
|
!$typedInvalidScene->check(['id' => 7, 'model' => $invalidModelType]),
|
|
'full validation scene rejects model type ' . get_debug_type($invalidModelType)
|
|
);
|
|
}
|
|
$forbiddenScene = (new DiagnosisValidate())->scene('aiAnalysis');
|
|
analysisContractExpect(
|
|
!$forbiddenScene->check(['id' => 7, 'model' => 'qwen', 'base_url' => 'https://client.invalid']),
|
|
'full validation scene rejects client upstream configuration'
|
|
);
|
|
|
|
$migration = file_get_contents(
|
|
dirname(__DIR__) . '/sql/1.9.20260813/add_diagnosis_ai_report.sql'
|
|
);
|
|
analysisContractExpect(is_string($migration), 'permission migration is readable');
|
|
analysisContractExpect(
|
|
str_contains($migration, "'tcm.diagnosis/aiAnalysis'"),
|
|
'exact action permission is registered'
|
|
);
|
|
analysisContractExpect(
|
|
str_contains($migration, "WHERE NOT EXISTS (\n SELECT 1 FROM `zyt_system_menu`\n WHERE `perms` = 'tcm.diagnosis/aiAnalysis'"),
|
|
'permission insertion is idempotent'
|
|
);
|
|
analysisContractExpect(
|
|
str_contains($migration, '@diagnosis_ai_analysis_menu_id'),
|
|
'eligible roles receive the exact permission node'
|
|
);
|
|
|
|
echo "Diagnosis AI analysis contract: OK\n";
|