96 lines
3.8 KiB
PHP
96 lines
3.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\common\service {
|
|
function curl_init(): \stdClass
|
|
{
|
|
return (object) ['options' => [], '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";
|
|
}
|