78 lines
5.2 KiB
PHP
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);
|
|
}
|
|
}
|