Files
zyt/server/tests/DifyRequiredSourcesLoopbackTest.php
T
2026-10-23 10:17:09 +08:00

84 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace think\facade {
class Log { public static function warning(string $message): void {} }
}
namespace app\common\service {
function config(string $name): array { return $GLOBALS['loopbackRequiredConfig']; }
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\DifyChatService;
function loopbackRequiredExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
if (($argv[1] ?? '') === 'server') {
$listener = stream_socket_server('tcp://127.0.0.1:0', $error, $message);
if ($listener === false) exit(2);
echo json_encode(['address' => stream_socket_get_name($listener, false)]) . "\n";
flush();
$client = stream_socket_accept($listener, 5);
if ($client === false) { fclose($listener); exit(3); }
stream_set_timeout($client, 5);
$wire = '';
while (!str_contains($wire, "\r\n\r\n")) {
$chunk = fread($client, 8192);
if ($chunk === '' || $chunk === false) break;
$wire .= $chunk;
}
[$headers, $body] = array_pad(explode("\r\n\r\n", $wire, 2), 2, '');
preg_match('/\r\nContent-Length:\s*(\d+)/i', $headers, $length);
$expected = (int) ($length[1] ?? 0);
while (strlen($body) < $expected) {
$chunk = fread($client, $expected - strlen($body));
if ($chunk === '' || $chunk === false) break;
$body .= $chunk;
}
$payload = json_decode($body, true);
$stream = ($payload['response_mode'] ?? '') === 'streaming' || !empty($payload['stream']);
$status = ($argv[2] ?? '') === 'reject' ? 413 : 200;
$response = $status === 413 ? '{"code":"invalid_param"}'
: ($stream ? "data: {\"event\":\"message\",\"answer\":\"synthetic-answer\"}\n\ndata: {\"event\":\"message_end\"}\n\n" : '{"answer":"synthetic-answer"}');
fwrite($client, 'HTTP/1.1 ' . $status . " Synthetic\r\nContent-Type: " . ($stream ? 'text/event-stream' : 'application/json')
. "\r\nContent-Length: " . strlen($response) . "\r\nConnection: close\r\n\r\n" . $response);
fclose($client); fclose($listener);
echo json_encode(['payload' => $payload, 'body_bytes' => strlen($body), 'expected_body_bytes' => $expected], JSON_UNESCAPED_SLASHES) . "\n";
exit;
}
$files = [];
for ($index = 1; $index <= 8; ++$index) $files[] = ['type' => $index <= 4 ? 'image' : 'document',
'url' => 'https://synthetic.invalid/required-' . $index . ($index <= 4 ? '.png' : '.pdf')];
$files[] = ['type' => 'video', 'url' => 'https://synthetic.invalid/not-transmitted.mp4'];
$sections = [['key' => 'chat', 'label' => '聊天', 'count' => 0, 'availability' => 'empty', 'status' => 'prepared']];
foreach (['blocking', 'streaming', 'reject'] as $scenario) {
$process = proc_open([PHP_BINARY, __FILE__, 'server', $scenario],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, null, null, ['bypass_shell' => true]);
loopbackRequiredExpect(is_resource($process), 'loopback peer starts');
$ready = json_decode((string) fgets($pipes[1]), true);
loopbackRequiredExpect(is_string($ready['address'] ?? null), 'loopback peer supplies ephemeral port');
$loopbackRequiredConfig = ['enable' => true, 'base_url' => 'http://' . $ready['address'] . '/v1/chat-messages',
'timeout' => 5, 'max_files' => 3, 'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'loopback-fixture-only']]];
$options = ['required_files' => true, 'required_sections' => $sections, 'knowledge_mode' => 'evidence'];
$result = $scenario === 'streaming'
? DifyChatService::streamChat('qwen', [], '合成资料;聊天:无', 'synthetic-user', static fn (string $delta): bool => true, null, $files, $options)
: DifyChatService::chat('qwen', [], '合成资料;聊天:无', 'synthetic-user', $files, $options);
$captured = json_decode((string) fgets($pipes[1]), true);
fclose($pipes[0]); fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]); fclose($pipes[2]);
$exit = proc_close($process);
loopbackRequiredExpect($exit === 0 && $stderr === '', 'loopback peer exits cleanly');
loopbackRequiredExpect($captured['body_bytes'] === $captured['expected_body_bytes']
&& count($captured['payload']['files']) === 8, 'actual HTTP body carries all eight required attachments with complete Content-Length');
loopbackRequiredExpect(!str_contains(json_encode($captured['payload']), 'not-transmitted.mp4')
&& str_contains($captured['payload']['query'], '聊天:无'), 'actual HTTP wire contains explicit empty text and no video');
if ($scenario === 'reject') loopbackRequiredExpect(($result['error_code'] ?? '') === 'REQUIRED_FILES_REJECTED', 'real 413 terminates as required data failure');
else loopbackRequiredExpect(!empty($result['ok']) && $result['attachment_count'] === 8
&& $result['required_sections'][0]['status'] === 'submitted', 'actual successful HTTP delivery has final complete manifest');
}
echo "Dify required sources actual loopback HTTP blocking / streaming / hard-limit rejection: OK\n";
}