This commit is contained in:
Your Name
2026-09-10 15:19:17 +08:00
parent 27fbef9321
commit 36975c6c1b
487 changed files with 15696 additions and 78 deletions
@@ -2,6 +2,38 @@
declare(strict_types=1);
namespace app\common\service {
// Exercise chat() offline without loading runtime configuration or opening a connection.
function config(string $name): array
{
return $GLOBALS['upstreamTestConfig'];
}
function curl_init(): \stdClass
{
return new \stdClass();
}
function curl_setopt_array(\stdClass $handle, array $options): bool
{
$handle->url = $options[CURLOPT_URL];
$GLOBALS['upstreamTestRequests'][] = ['url' => $handle->url, 'payload' => json_decode($options[CURLOPT_POSTFIELDS], true)];
return true;
}
function curl_exec(\stdClass $handle): string
{
return json_encode(str_ends_with($handle->url, '/chat-messages')
? ['answer' => 'offline reply'] : ['choices' => [['message' => ['content' => 'offline reply']]]]);
}
function curl_errno(\stdClass $handle): int { return 0; }
function curl_getinfo(\stdClass $handle, int $option): int { return 200; }
function curl_close(\stdClass $handle): void {}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\DifyChatService;
@@ -163,6 +195,56 @@ expectSame(
'non-http attachments are still rejected outright'
);
$duplicateFiles = [
['file_id' => 'file:1', 'source_ids' => ['source:1'], 'type' => 'image', 'url' => 'https://cdn.example.test/shared.jpg'],
['file_id' => 'file:2', 'source_ids' => ['source:2'], 'type' => 'image', 'url' => 'https://cdn.example.test/shared.jpg'],
['file_id' => 'file:3', 'source_ids' => ['source:3'], 'type' => 'image', 'url' => 'https://cdn.example.test/other.jpg'],
];
expectSame(2, count(callPrivate('normalizeFiles', [$duplicateFiles, 3])['kept']), 'default normalization still deduplicates shared URLs');
$upstreamTestConfig = ['enable' => true, 'base_url' => '', 'timeout' => 30, 'max_files' => 3,
'models' => ['qwen' => ['name' => 'offline-model', 'api_key' => 'offline-fixture']]];
foreach (['dify' => 'chat-messages', 'openai' => 'chat/completions'] as $protocol => $endpoint) {
$upstreamTestConfig['base_url'] = 'https://ai.example.test/v1/' . $endpoint;
$upstreamTestRequests = [];
$strictResult = DifyChatService::chat('qwen', [], 'offline query', 'offline-user', $duplicateFiles, ['strict_files' => true]);
expectSame(true, $strictResult['ok'], 'strict ' . $protocol . ' accepts distinct logical attachments sharing a URL');
expectSame(1, count($upstreamTestRequests), 'strict ' . $protocol . ' submits the complete batch once');
$payload = $upstreamTestRequests[0]['payload'];
$wireUrls = $protocol === 'dify' ? array_column($payload['files'], 'url')
: array_column(array_column(array_slice($payload['messages'][0]['content'], 1), 'image_url'), 'url');
expectSame(array_column($duplicateFiles, 'url'), $wireUrls, 'strict ' . $protocol . ' transmits every attachment in manifest order');
expectSame(count($wireUrls), $strictResult['transmitted_file_count'], 'strict ' . $protocol . ' acknowledgment matches actual wire attachment count');
expectSame($protocol, $strictResult['attachment_transport'], 'strict response identifies the actual attachment protocol');
$upstreamTestRequests = [];
$ordinaryResult = DifyChatService::chat('qwen', [], 'offline query', 'offline-user', $duplicateFiles);
expectSame(true, $ordinaryResult['ok'], 'ordinary ' . $protocol . ' chat remains successful');
$payload = $upstreamTestRequests[0]['payload'];
$wireUrls = $protocol === 'dify' ? array_column($payload['files'], 'url')
: array_column(array_column(array_slice($payload['messages'][0]['content'], 1), 'image_url'), 'url');
expectSame(array_values(array_unique(array_column($duplicateFiles, 'url'))), $wireUrls, 'ordinary ' . $protocol . ' still deduplicates URLs');
}
foreach ([
['type' => 'image', 'url' => 'ftp://cdn.example.test/invalid.jpg'],
['type' => 'image', 'url' => 'https://user@cdn.example.test/invalid.jpg'],
['type' => 'image', 'url' => "https://cdn.example.test/invalid\n.jpg"],
['type' => 'unknown', 'url' => 'https://cdn.example.test/invalid.jpg'],
null,
] as $invalidFile) {
$upstreamTestRequests = [];
$invalidFiles = [$duplicateFiles[0], $duplicateFiles[1], $invalidFile];
$invalidResult = DifyChatService::chat('qwen', [], 'offline query', 'offline-user', $invalidFiles, ['strict_files' => true]);
expectSame('STRICT_FILES_INVALID_OR_LIMIT', $invalidResult['error_code'] ?? '', 'strict duplicate preservation never bypasses attachment validation');
expectSame([], $upstreamTestRequests, 'invalid strict batches are rejected before transport');
}
foreach ([2, 0] as $limit) {
$upstreamTestConfig['max_files'] = $limit;
$upstreamTestRequests = [];
$limitedResult = DifyChatService::chat('qwen', [], 'offline query', 'offline-user', $duplicateFiles, ['strict_files' => true]);
expectSame('STRICT_FILES_INVALID_OR_LIMIT', $limitedResult['error_code'] ?? '', 'strict limits count logical attachments even when URLs repeat');
expectSame([], $upstreamTestRequests, 'over-limit strict batches are never partially transmitted');
}
// 被截断的附件必须出现在提示词清单里,否则模型会把“没看到”当成“没有”。
$cappedSpecs = callPrivate('buildRequestSpecs', [
'https://ai.example.test/v1/chat-messages',
@@ -332,3 +414,5 @@ expectSame(false, callPrivate('isValidTimeout', [0]), 'zero timeout');
expectSame(false, callPrivate('isValidTimeout', [301]), 'excessive timeout');
echo "Prescription AI upstream contract: OK\n";
}