Files
zyt/server/tests/DifyAttemptReadinessTest.php
2026-10-27 14:03:35 +08:00

78 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service {
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
function config(string $name): array { return $GLOBALS['readinessConfig']; }
function curl_init(): \stdClass { return (object) ['errno' => 0]; }
function curl_setopt_array(\stdClass $handle, array $options): bool { $handle->options = $options; return true; }
function curl_exec(\stdClass $handle): string|bool {
$url = $handle->options[CURLOPT_URL];
if (str_ends_with($url, '/stop')) { ++$GLOBALS['readinessStops']; return '{"result":"success"}'; }
++$GLOBALS['readinessSends'];
$GLOBALS['readinessReady'] = false;
if ($GLOBALS['readinessMode'] === 'blocking') {
$handle->httpCode = 404;
return '{"code":"not_found","message":"Not Found"}';
}
$header = $handle->options[CURLOPT_HEADERFUNCTION];
$write = $handle->options[CURLOPT_WRITEFUNCTION];
$header($handle, "HTTP/1.1 200 OK\r\n");
$write($handle, "data: {\"event\":\"workflow_started\",\"task_id\":\"synthetic-unconfirmed\"}\n\n");
return true; // EOF without message_end/workflow terminal; never a real network call.
}
function curl_errno(\stdClass $handle): int { return $handle->errno; }
function curl_getinfo(\stdClass $handle, int $option): int { return $handle->httpCode ?? 200; }
function curl_close(\stdClass $handle): void {}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\DifyChatService;
use app\adminapi\service\AssistantRequestControl;
$assertions = 0;
function readinessCheck(bool $ok, string $message): void {
++$GLOBALS['assertions']; if (!$ok) throw new RuntimeException($message);
}
$readinessConfig = ['enable' => true, 'base_url' => 'https://synthetic.invalid/v1', 'timeout' => 5,
'models' => ['qwen' => ['name' => 'synthetic-qwen', 'api_key' => 'offline-only'],
'openai' => ['name' => 'synthetic-openai', 'api_key' => 'offline-only']]];
$directory = sys_get_temp_dir() . '/zyt-readiness-' . bin2hex(random_bytes(8));
try {
foreach (['qwen', 'openai'] as $profile) {
foreach (['stream', 'blocking'] as $readinessMode) {
$readinessSends = 0; $readinessReady = true; $readinessStops = 0;
$control = new AssistantRequestControl(7, 11, '12345678-1234-4234-8234-' . sprintf('%012d', $assertions), $directory);
readinessCheck($control->begin(), 'isolated control begins');
$options = ['knowledge_mode' => 'evidence',
'can_start_attempt' => static fn (): bool => $GLOBALS['readinessReady'] && $control->canStartAttempt(),
'on_task' => static function (string $task) use ($control, $profile): void { $control->bindTask($profile, 'synthetic-user', $task); },
'on_transfer_end' => static function (string $task) use ($control): void { $control->clearTask($task); }];
$result = $readinessMode === 'stream'
? DifyChatService::streamChat($profile, [], 'synthetic', 'synthetic-user', static fn ($text): bool => true, null, [], $options)
: DifyChatService::chat($profile, [], 'synthetic', 'synthetic-user', [], $options);
readinessCheck(($result['error_code'] ?? '') === 'UPSTREAM_CLEANUP_PENDING' && $readinessSends === 1,
'An internal protocol retry cannot start another generation while cleanup is pending.');
readinessCheck(DifyChatService::stopTask($profile, 'synthetic-unconfirmed', 'synthetic-user') && $readinessStops === 1,
'The generation gate never blocks the stop endpoint.');
if ($readinessMode === 'stream') readinessCheck(!$control->canStartAttempt(), 'EOF never clears an unconfirmed remote identity.');
}
$control = new AssistantRequestControl(7, 11, '22345678-1234-4234-8234-' . sprintf('%012d', $assertions), $directory);
readinessCheck($control->begin(), 'receipt control begins');
$control->bindTask($profile, 'synthetic-user', 'old-task'); $control->signalValidationFailure();
$control->markStop(true, 'old-task');
try { $control->bindTask($profile, 'synthetic-user', 'race-task'); } catch (RuntimeException $expected) {}
$control->markTerminal('old-task', 'stopped'); $control->clearTask('old-task');
$state = $control->publicState();
readinessCheck(!$state['upstream_terminal_confirmed'] && !$state['upstream_stop_confirmed']
&& $state['upstream_pending_task_count'] === 1 && !$control->canStartAttempt(),
'A pending quarantined task cannot be hidden by a previous task terminal.');
readinessCheck(!str_contains(json_encode($state), 'race-task'), 'Public receipts never expose task identities.');
}
echo "Dify per-attempt readiness / cleanup receipt: {$assertions} assertions passed (pure cURL stubs).\n";
} finally {
foreach (glob($directory . '/*') ?: [] as $path) unlink($path);
if (is_file($directory . '/.gc.lock')) unlink($directory . '/.gc.lock');
if (is_dir($directory)) rmdir($directory);
}
}