Files
zyt/server/tests/DifyMissingAttachmentFallbackTest.php
2026-10-23 13:57:11 +08:00

100 lines
7.2 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['missingFallbackConfig']; }
final class DifyRequiredAttachmentUpload {
public static function prepare(string $endpoint, string $key, string $user, array $files, int $timeout,
?callable $abort = null, array $options = []): array {
$skips = []; $wire = []; $indices = [];
foreach ($files as $position => $file) {
$missing = $GLOBALS['missingFallbackScenario'] === 'all-missing' || $position === 0;
if ($missing) {
$skips[] = ['index' => $position + 1, 'kind' => $file['type'], 'reason' => 'not_found'];
($options['on_upload'])(['index' => $position + 1, 'status' => 'skipped', 'reason' => 'not_found']);
} else {
$wire[] = ['type' => $file['type'], 'transfer_method' => 'local_file', 'upload_file_id' => 'synthetic-id'];
$indices[] = $position + 1;
($options['on_upload'])(['index' => $position + 1, 'status' => 'uploaded']);
}
}
return ['ok' => true, 'files' => $wire, 'uploaded_indices' => $indices, 'skipped' => $skips];
}
}
function curl_init(): \stdClass { return new \stdClass(); }
function curl_setopt_array(\stdClass $handle, array $options): bool {
if (!str_starts_with($options[CURLOPT_URL], 'https://fixture.invalid/v1/')) throw new \RuntimeException('Fixture destinations only');
$handle->options = $options; return true;
}
function curl_exec(\stdClass $handle): string|bool {
$payload = json_decode($handle->options[CURLOPT_POSTFIELDS], true);
$GLOBALS['missingFallbackRequests'][] = $payload;
$dify = str_ends_with($handle->options[CURLOPT_URL], '/chat-messages');
$handle->code = $dify ? 404 : 200;
($handle->options[CURLOPT_HEADERFUNCTION])($handle, 'HTTP/1.1 ' . $handle->code . " Synthetic\r\n");
if (isset($handle->options[CURLOPT_WRITEFUNCTION])) {
$write = $handle->options[CURLOPT_WRITEFUNCTION];
if ($dify) $write($handle, '{"code":"not_found"}');
else {
$write($handle, 'data: ' . json_encode(['choices' => [['delta' => ['content' => 'synthetic answer']]]]) . "\n\n");
$write($handle, "data: [DONE]\n\n");
}
return true;
}
return $dify ? '{"code":"not_found"}' : '{"choices":[{"message":{"content":"synthetic answer"}}]}';
}
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;
function missingFallbackExpect(bool $ok, string $message): void { if (!$ok) throw new RuntimeException($message); }
$missingFallbackConfig = ['enable' => true, 'base_url' => 'https://fixture.invalid/v1',
'timeout' => 5, 'required_attachment_transport' => 'upload',
'models' => ['qwen' => ['name' => 'synthetic', 'api_key' => 'fixture-only']]];
foreach ([false, true] as $stream) foreach (['all-missing', 'missing-image-available-document',
'missing-image-available-image', 'missing-document-available-image'] as $missingFallbackScenario) {
$missingFallbackRequests = []; $events = [];
$files = $missingFallbackScenario === 'all-missing'
? [['type' => 'document', 'url' => 'https://fixture.invalid/missing.pdf'], ['type' => 'audio', 'url' => 'https://fixture.invalid/missing.wav']]
: [['type' => $missingFallbackScenario === 'missing-document-available-image' ? 'document' : 'image', 'url' => 'https://fixture.invalid/missing'],
['type' => $missingFallbackScenario === 'missing-image-available-document' ? 'document' : 'image', 'url' => 'https://fixture.invalid/available']];
$options = ['required_files' => true, 'knowledge_mode' => 'evidence',
'on_progress' => static function (array $event) use (&$events): void { $events[] = $event; }];
$result = $stream ? DifyChatService::streamChat('qwen', [], 'complete synthetic clinical text', 'synthetic-user',
static fn (string $text): bool => true, null, $files, $options)
: DifyChatService::chat('qwen', [], 'complete synthetic clinical text', 'synthetic-user', $files, $options);
$expectedMissing = $missingFallbackScenario === 'all-missing' ? 2 : 1;
missingFallbackExpect($result['skipped_attachment_count'] === $expectedMissing && $result['attachment_count'] === 2,
'protocol fallback keeps original source and confirmed missing counts');
missingFallbackExpect($result['items'][0]['status'] === 'skipped' && $result['items'][0]['reason'] === 'not_found',
'known missing item survives every fallback outcome');
if ($missingFallbackScenario === 'missing-image-available-document') {
missingFallbackExpect(!empty($result['ok']) && $result['rejected_attachment_count'] === 1
&& count($missingFallbackRequests) === 6 && $result['submitted_attachment_count'] === 0
&& $result['attachment_transfer'] === 'uploaded_files'
&& $result['items'][1]['status'] === 'rejected' && $result['items'][1]['reason'] === 'file_type',
'unsupported document is isolated as a truthful unread gap while the confirmed missing source stays skipped');
$last = $missingFallbackRequests[count($missingFallbackRequests) - 1];
missingFallbackExpect(str_contains(json_encode($last), 'complete synthetic clinical text')
&& !str_contains(json_encode($last), 'https://fixture.invalid/missing'), 'final original body never resurrects known missing URLs');
continue;
}
missingFallbackExpect(!empty($result['ok']) && count($missingFallbackRequests) === 2
&& $result['submitted_attachment_count'] === 2 - $expectedMissing, 'text or available images complete through protocol fallback');
$wire = json_encode($missingFallbackRequests[1], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
missingFallbackExpect(!str_contains($wire, 'https://fixture.invalid/missing')
&& !str_contains($wire, 'ATTACHMENTS_NOT_INLINE') && str_contains($wire, 'complete synthetic clinical text')
&& substr_count($wire, '【附件可用性说明】') === 1, 'fallback builds from available sources without resurrecting dead references');
missingFallbackExpect(($missingFallbackScenario === 'all-missing') === !str_contains($wire, 'https://fixture.invalid/available'),
'available images are preserved while all-missing requests send only text');
}
echo "Missing attachment protocol fallback / truthful receipts / no revived references / blocking and stream: OK\n";
}