Files
zyt/server/tests/DifyRequiredUploadTransportTest.php
T
2026-10-23 10:40:43 +08:00

88 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
namespace think\facade {
class Log { public static function warning(string $message): void {} public static function write(string $message, string $level): void {} }
}
namespace app\common\service {
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
function config(string $name): array { return $GLOBALS['uploadTransportConfig']; }
final class DifyRequiredAttachmentUpload {
public static function prepare(string $endpoint, string $key, string $user, array $files, int $timeout,
?callable $shouldAbort = null, array $options = []): array {
$GLOBALS['uploadTransportStages'][] = compact('endpoint', 'user', 'files', 'timeout');
if ($GLOBALS['uploadTransportScenario'] === 'cancel') return ['ok' => false, 'error_code' => 'AI_ASSISTANT_CANCELLED', 'error' => '合成取消'];
if ($GLOBALS['uploadTransportScenario'] === 'upload-failed') return ['ok' => false, 'error_code' => 'REQUIRED_FILES_UPLOAD_FAILED', 'error' => '合成上传失败'];
$result = [];
foreach ($files as $index => $file) {
foreach (['uploading', 'uploaded'] as $status) {
if (is_callable($options['on_upload'] ?? null)) ($options['on_upload'])(['index' => $index + 1, 'total' => count($files), 'status' => $status]);
}
$result[] = ['type' => $file['type'], 'transfer_method' => 'local_file', 'upload_file_id' => 'fixture-upload-' . ($index + 1)];
}
return ['ok' => true, 'files' => $result, 'attachment_transport' => 'uploaded_files'];
}
}
function curl_init(): \stdClass { return new \stdClass(); }
function curl_setopt_array(\stdClass $handle, array $options): bool { $handle->options = $options; return true; }
function curl_exec(\stdClass $handle): string|bool {
$payload = json_decode($handle->options[CURLOPT_POSTFIELDS], true);
$GLOBALS['uploadTransportRequests'][] = $payload;
$stream = ($payload['response_mode'] ?? '') === 'streaming';
$handle->code = $GLOBALS['uploadTransportScenario'] === 'inputs' && !empty($payload['inputs']) ? 400 : 200;
($handle->options[CURLOPT_HEADERFUNCTION])($handle, 'HTTP/1.1 ' . $handle->code . " Synthetic\r\n");
if ($handle->code !== 200) {
$body = json_encode(['code' => 'invalid_param', 'message' => 'synthetic_undeclared_input input variable invalid']);
if ($stream) { ($handle->options[CURLOPT_WRITEFUNCTION])($handle, $body); return true; }
return $body;
}
if (!$stream) return json_encode(['answer' => '合成回答']);
$write = $handle->options[CURLOPT_WRITEFUNCTION];
$write($handle, 'data: ' . json_encode(['event' => 'message', 'answer' => '合成回答']) . "\n\n");
$write($handle, "data: {\"event\":\"message_end\"}\n\n");
return true;
}
function curl_errno(\stdClass $handle): int { return 0; }
function curl_getinfo(\stdClass $handle, int $option): int { return $handle->code; }
function curl_close(\stdClass $handle): void {}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\DifyChatService;
use app\adminapi\service\AiRequiredSource;
function uploadTransportExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
$uploadTransportConfig = ['enable' => true, 'base_url' => 'https://synthetic.invalid/v1/chat-messages',
'timeout' => 5, 'required_attachment_transport' => 'upload',
'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'fixture-only']]];
$files = [['type' => 'image', 'url' => 'https://synthetic.invalid/one.png'],
['type' => 'image', 'url' => 'https://synthetic.invalid/two.png'],
['type' => 'document', 'url' => 'https://synthetic.invalid/three.pdf']];
$options = ['required_files' => true, 'knowledge_mode' => 'evidence', 'required_sections' => AiRequiredSource::manifest([])];
foreach ([false, true] as $stream) foreach (['success', 'inputs', 'upload-failed', 'cancel'] as $scenario) {
$uploadTransportScenario = $scenario; $uploadTransportStages = []; $uploadTransportRequests = []; $events = [];
$opts = $options + ['on_progress' => static function (array $event) use (&$events): void { $events[] = $event; }];
$result = $stream ? DifyChatService::streamChat('qwen', ['synthetic_undeclared_input' => 'value'], '合成资料', 'fixture-same-user',
static fn (string $text): bool => true, null, $files, $opts)
: DifyChatService::chat('qwen', ['synthetic_undeclared_input' => 'value'], '合成资料', 'fixture-same-user', $files, $opts);
uploadTransportExpect(count($uploadTransportStages) === 1, 'all required files are staged once per request');
uploadTransportExpect($uploadTransportStages[0]['user'] === 'fixture-same-user' && count($uploadTransportStages[0]['files']) === 3,
'upload and chat share end user and complete source set');
if (in_array($scenario, ['upload-failed', 'cancel'], true)) {
uploadTransportExpect(empty($result['ok']) && $uploadTransportRequests === [], 'failed or cancelled upload never starts model generation');
uploadTransportExpect($result['error_code'] === ($scenario === 'cancel' ? 'AI_ASSISTANT_CANCELLED' : 'REQUIRED_FILES_UPLOAD_FAILED'),
'upload failure retains actual cause');
continue;
}
uploadTransportExpect(!empty($result['ok']) && $result['attachment_transfer'] === 'uploaded_files', 'final receipt identifies actual uploads');
uploadTransportExpect(count($uploadTransportRequests) === ($scenario === 'inputs' ? 2 : 1), 'bounded input compatibility remains intact');
foreach ($uploadTransportRequests as $payload) {
uploadTransportExpect($payload['user'] === 'fixture-same-user' && count($payload['files']) === 3, 'every generation attempt retains all uploaded files');
uploadTransportExpect(array_column($payload['files'], 'transfer_method') === ['local_file', 'local_file', 'local_file'], 'Dify gets upload IDs, not resource URLs');
uploadTransportExpect(!array_key_exists('url', $payload['files'][0]) && $payload['files'][0]['upload_file_id'] === 'fixture-upload-1', 'original URL never leaks into uploaded wire files');
}
uploadTransportExpect(count(array_filter($events, static fn (array $event): bool => ($event['status'] ?? '') === 'uploading')) >= 3,
'safe per-file upload progress reaches the desktop');
}
echo "Dify required upload transport / complete IDs / same user / input retry reuse / failure / cancel: OK\n";
}