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'); echo "Assistant final error receipt / real controller SSE / private metadata filter / cancellation isolation: OK\n";