更新
This commit is contained in:
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user