Files
xuetang/server/tests/DiagnosisAiAnalysisContractTest.php
2026-09-08 11:40:15 +08:00

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