73 lines
5.3 KiB
PHP
73 lines
5.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic;
|
|
use app\adminapi\service\AiRequiredSource;
|
|
use app\adminapi\service\AssistantSseProtocol;
|
|
use app\adminapi\controller\tcm\DiagnosisController;
|
|
|
|
function errorReceiptExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
|
|
$format = new ReflectionMethod(DiagnosisAiLogic::class, 'formatAssistantResult');
|
|
$sections = AiRequiredSource::manifest([]);
|
|
foreach ($sections as &$section) { $section['status'] = 'failed'; $section['label'] = '不得回传的姓名'; }
|
|
unset($section);
|
|
$result = ['ok' => false, 'error_code' => 'REQUIRED_FILES_REJECTED', 'error' => '合成附件失败',
|
|
'required_sections' => $sections, 'source_summary' => ['patient_name' => '不得回传的姓名', 'url' => 'https://private.invalid', 'required_sections' => $sections],
|
|
'items' => [['kind' => 'image', 'status' => 'failed', 'label' => '不得回传的姓名', 'url' => 'https://private.invalid/image.png', 'upload_file_id' => 'private-id'],
|
|
['kind' => 'video', 'status' => 'submitted', 'label' => 'private-video']],
|
|
'attachment_count' => 1, 'attachment_transfer' => 'uploaded_files', 'transfer_manifest_final' => true,
|
|
'upstream_http_status' => 400, 'upstream_message' => 'provider private message', 'api_key' => 'private-api-key'];
|
|
errorReceiptExpect($format->invoke(null, [], $result) === null, 'real formatter retains failure');
|
|
$meta = DiagnosisAiLogic::getAssistantErrorMeta();
|
|
errorReceiptExpect(count($meta['required_sections']) === 13 && $meta['items'][0]['status'] === 'failed'
|
|
&& count($meta['items']) === 1 && $meta['attachment_transfer'] === 'uploaded_files', 'safe complete final receipt survives failure');
|
|
$encoded = json_encode($meta, JSON_UNESCAPED_UNICODE);
|
|
foreach (['不得回传的姓名', 'private.invalid', 'private-id', 'private-api-key', 'provider private message', 'private-video'] as $private) {
|
|
errorReceiptExpect(!str_contains($encoded, $private), 'error metadata excludes private fields');
|
|
}
|
|
$protocol = new AssistantSseProtocol(); $protocol->encode('start', []);
|
|
$frames = [];
|
|
$emit = static function (string $event, array $payload) use ($protocol, &$frames): bool {
|
|
$frames[] = $protocol->encode($event, $payload); return true;
|
|
};
|
|
$controller = (new ReflectionClass(DiagnosisController::class))->newInstanceWithoutConstructor();
|
|
$finish = new ReflectionMethod(DiagnosisController::class, 'finishAssistantError');
|
|
$finish->invoke($controller, $emit, null, false);
|
|
$frame = end($frames);
|
|
errorReceiptExpect(str_contains($frame, 'REQUIRED_FILES_REJECTED') && str_contains($frame, '"transfer_manifest_final":true')
|
|
&& str_contains($frame, '"status":"failed"'), 'real controller SSE error carries authoritative failure receipt');
|
|
$frames = []; $cancelProtocol = new AssistantSseProtocol(); $cancelProtocol->encode('start', []);
|
|
$cancelEmit = static function (string $event, array $payload) use ($cancelProtocol, &$frames): bool {
|
|
$frames[] = $cancelProtocol->encode($event, $payload); return true;
|
|
};
|
|
$finish->invoke($controller, $cancelEmit, null, true);
|
|
errorReceiptExpect(!str_contains(end($frames), 'required_sections') && str_contains(end($frames), 'AI_ASSISTANT_CANCELLED'),
|
|
'cancel terminal does not reuse unrelated failed receipt');
|
|
$format->invoke(null, [], ['ok' => true, 'content' => '合成回复']);
|
|
errorReceiptExpect(DiagnosisAiLogic::getAssistantErrorMeta() === [], 'success clears stale error metadata');
|
|
$skippedResult = $result;
|
|
$skippedResult['items'] = [
|
|
['kind' => 'image', 'status' => 'skipped', 'reason' => 'not_found', 'url' => 'https://private.invalid/missing'],
|
|
['kind' => 'image', 'status' => 'failed', 'reason' => 'private-error-body'],
|
|
];
|
|
$skippedResult['attachment_count'] = 2;
|
|
$skippedResult['submitted_attachment_count'] = 0;
|
|
$skippedResult['skipped_attachment_count'] = 1;
|
|
$format->invoke(null, [], $skippedResult);
|
|
$safeSkipped = DiagnosisAiLogic::getAssistantErrorMeta();
|
|
errorReceiptExpect($safeSkipped['items'][0]['status'] === 'skipped' && $safeSkipped['items'][0]['reason'] === 'not_found'
|
|
&& $safeSkipped['items'][1]['status'] === 'failed' && !isset($safeSkipped['items'][1]['reason'])
|
|
&& $safeSkipped['submitted_attachment_count'] === 0 && $safeSkipped['skipped_attachment_count'] === 1,
|
|
'final errors preserve safe missing-source facts and separate counters');
|
|
errorReceiptExpect(!str_contains(json_encode($safeSkipped), 'private.invalid')
|
|
&& !str_contains(json_encode($safeSkipped), 'private-error-body'), 'skipped error metadata never includes resource URLs or private reasons');
|
|
$success = $format->invoke(null, [], ['ok' => true, 'content' => '合成回复', 'transfer_manifest_final' => true,
|
|
'required_sections' => AiRequiredSource::manifest([]), 'items' => [['kind' => 'image', 'status' => 'skipped', 'reason' => 'not_found']],
|
|
'attachment_count' => 1, 'submitted_attachment_count' => 0, 'skipped_attachment_count' => 1,
|
|
'attachment_transfer' => 'uploaded_files']);
|
|
errorReceiptExpect($success['answer'] === '合成回复' && $success['items'][0]['status'] === 'skipped'
|
|
&& $success['submitted_attachment_count'] === 0 && $success['skipped_attachment_count'] === 1,
|
|
'a text answer with all attachments missing keeps its successful final receipt');
|
|
echo "Assistant final error receipt / real controller SSE / private metadata filter / cancellation isolation: OK\n";
|