[], 'errno' => 0]; } function curl_setopt_array(\stdClass $handle, array $options): bool { $handle->options = $options; return true; } function curl_exec(\stdClass $handle): string|bool { $GLOBALS['streamDeliveryExecuting'] = true; $options = $handle->options; $options[CURLOPT_HEADERFUNCTION]($handle, 'HTTP/1.1 ' . $GLOBALS['streamDeliveryStatus'] . " Fixture\r\n"); foreach ($GLOBALS['streamDeliveryChunks'] as [$chunk, $expectedDeltas]) { $written = $options[CURLOPT_WRITEFUNCTION]($handle, $chunk); \deliveryExpect(count($GLOBALS['streamDeliveryDeltas']) === $expectedDeltas, 'each complete upstream frame reaches the consumer before the next network read'); if ($written !== strlen($chunk)) { $handle->errno = CURLE_WRITE_ERROR; break; } } $GLOBALS['streamDeliveryExecuting'] = false; return $handle->errno === 0 ? '' : false; } function curl_errno(\stdClass $handle): int { return $handle->errno; } function curl_getinfo(\stdClass $handle, int $option): int { return $GLOBALS['streamDeliveryStatus']; } function curl_close(\stdClass $handle): void {} } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; use app\common\service\DifyChatService; function deliveryExpect(bool $condition, string $message): void { if (!$condition) { fwrite(STDERR, "FAIL: {$message}\n"); exit(1); } } function deliverFixture(string $protocol, array $chunks, int $status = 200, bool $accept = true): array { $GLOBALS['streamDeliveryStatus'] = $status; $GLOBALS['streamDeliveryChunks'] = $chunks; $GLOBALS['streamDeliveryDeltas'] = []; $method = (new ReflectionClass(DifyChatService::class))->getMethod('sendStreamRequest'); return $method->invoke(null, $protocol, 'https://offline.example.test', [], 'offline-fixture', 30, static function (string $delta) use ($accept): bool { deliveryExpect($GLOBALS['streamDeliveryExecuting'], 'delta callback runs during curl_exec, before transfer completion'); $GLOBALS['streamDeliveryDeltas'][] = $delta; return $accept; }, null); } $dify = deliverFixture('dify', [ ["data: {\"event\":\"message\",\"answer\":\"首\"}\r\n", 0], ["\r\n", 1], ["data: {\"event\":\"message\",\r\ndata: \"answer\":\"字\"}\r\n\r\n", 2], ["data: {\"event\":\"message_end\"}\r\n\r\n", 2], ]); deliveryExpect($dify['content'] === '首字' && $dify['finished'], 'Dify streams complete frames in order'); $openAi = deliverFixture('openai', [ ["data: {\"choices\":[{\"delta\":{\"content\":\"A\"}}]}\n\n", 1], ["data: {\"choices\":[{\"delta\":{\"content\":\"B\"}}]}\n\n", 2], ["data: [DONE]\n\n", 2], ]); deliveryExpect($openAi['content'] === 'AB' && $openAi['finished'], 'OpenAI streams complete frames in order'); $rejected = deliverFixture('dify', [ ["data: {\"event\":\"message\",\"answer\":\"private upstream error\"}\n\n", 0], ], 401); deliveryExpect(!$rejected['emitted'] && $rejected['content'] === '', 'HTTP errors never become downstream text'); $aborted = deliverFixture('dify', [ ["data: {\"event\":\"message\",\"answer\":\"first\"}\n\n", 1], ["data: {\"event\":\"message\",\"answer\":\"late\"}\n\n", 2], ], 200, false); deliveryExpect($aborted['callback_error'] && $GLOBALS['streamDeliveryDeltas'] === ['first'], 'consumer refusal stops the current transfer before another delta'); echo "Dify chat stream delivery: OK\n"; }