325 lines
23 KiB
PHP
325 lines
23 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Standalone synthetic test: no framework bootstrap, runtime config, socket,
|
|
// credential, model, database, request-control file or external service is used.
|
|
namespace app\common\service {
|
|
function hrtime(bool $asNumber = false) { return $GLOBALS['runStatusStub']['now_ns']; }
|
|
function config(string $name) {
|
|
$stub =& $GLOBALS['runStatusStub'];
|
|
++$stub['config_calls'];
|
|
if ($name !== 'prescription_ai') throw new \RuntimeException('Unexpected config read');
|
|
$stub['now_ns'] += $stub['config_delay_ms'] * 1000000;
|
|
if ($stub['config_throw']) throw new \RuntimeException($stub['sensitive']);
|
|
return $stub['config'];
|
|
}
|
|
function curl_init() {
|
|
$stub =& $GLOBALS['runStatusStub'];
|
|
++$stub['init_calls'];
|
|
$stub['now_ns'] += $stub['init_delay_ms'] * 1000000;
|
|
if ($stub['init_throw']) throw new \RuntimeException($stub['sensitive']);
|
|
if ($stub['init_false']) return false;
|
|
return (object) ['errno' => $stub['errno']];
|
|
}
|
|
function curl_setopt_array(\stdClass $handle, array $options): bool {
|
|
$stub =& $GLOBALS['runStatusStub'];
|
|
++$stub['setopt_calls'];
|
|
$stub['options'] = $handle->options = $options;
|
|
$stub['now_ns'] += $stub['setopt_delay_ms'] * 1000000;
|
|
if ($stub['setopt_throw']) throw new \RuntimeException($stub['sensitive']);
|
|
return !$stub['setopt_false'];
|
|
}
|
|
function curl_exec(\stdClass $handle): bool {
|
|
$stub =& $GLOBALS['runStatusStub'];
|
|
++$stub['exec_calls'];
|
|
$stub['now_ns'] += $stub['exec_delay_ms'] * 1000000;
|
|
if ($stub['exec_throw']) throw new \RuntimeException($stub['sensitive']);
|
|
if ($stub['progress']) {
|
|
$option = defined('CURLOPT_XFERINFOFUNCTION') ? CURLOPT_XFERINFOFUNCTION : CURLOPT_PROGRESSFUNCTION;
|
|
$stub['progress_result'] = ($handle->options[$option])($handle, 0, 0, 0, 0);
|
|
if ($stub['progress_result'] !== 0) { $handle->errno = CURLE_ABORTED_BY_CALLBACK; return false; }
|
|
}
|
|
$chunks = $stub['chunks'] ?? [$stub['body']];
|
|
foreach ($chunks as $index => $chunk) {
|
|
$stub['now_ns'] += ($stub['chunk_delay_ms'][$index] ?? 0) * 1000000;
|
|
$written = ($handle->options[CURLOPT_WRITEFUNCTION])($handle, $chunk);
|
|
$stub['write_results'][] = $written;
|
|
if ($written !== strlen($chunk)) { $handle->errno = CURLE_WRITE_ERROR; return false; }
|
|
}
|
|
$stub['now_ns'] += $stub['after_body_ms'] * 1000000;
|
|
return !$stub['exec_false'];
|
|
}
|
|
function curl_errno(\stdClass $handle): int { return $handle->errno; }
|
|
function curl_getinfo(\stdClass $handle, int $option): int {
|
|
if ($option !== CURLINFO_RESPONSE_CODE) throw new \RuntimeException('Unexpected cURL info read');
|
|
$GLOBALS['runStatusStub']['now_ns'] += $GLOBALS['runStatusStub']['info_delay_ms'] * 1000000;
|
|
return $GLOBALS['runStatusStub']['http'];
|
|
}
|
|
function curl_error(\stdClass $handle): string {
|
|
++$GLOBALS['runStatusStub']['error_reads'];
|
|
throw new \RuntimeException('Raw cURL error must never be read');
|
|
}
|
|
function curl_close(\stdClass $handle): void {
|
|
++$GLOBALS['runStatusStub']['close_calls'];
|
|
$GLOBALS['runStatusStub']['now_ns'] += $GLOBALS['runStatusStub']['close_delay_ms'] * 1000000;
|
|
if ($GLOBALS['runStatusStub']['close_throw']) throw new \RuntimeException($GLOBALS['runStatusStub']['sensitive']);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
require dirname(__DIR__) . '/app/common/service/DifyWorkflowRunStatus.php';
|
|
use app\common\service\DifyWorkflowRunStatus;
|
|
|
|
$checks = 0;
|
|
$cases = 0;
|
|
$identity = ['profile' => 'qwen', 'user' => 'synthetic-private-user', 'task_id' => 'synthetic-task-001',
|
|
'workflow_run_id' => 'synthetic-run-001', 'generation' => 1];
|
|
|
|
function runStatusExpect(bool $condition, string $message): void {
|
|
++$GLOBALS['checks'];
|
|
if (!$condition) throw new RuntimeException($message);
|
|
}
|
|
function runStatusReset(array $changes = []): void {
|
|
$GLOBALS['runStatusStub'] = array_replace([
|
|
'now_ns' => 10000000000,
|
|
'config' => ['enable' => true, 'base_url' => 'https://status-synthetic.invalid/v1/chat-messages',
|
|
'models' => ['qwen' => ['api_key' => 'qwen-fixture-only-key'], 'openai' => ['api_key' => 'openai-fixture-only-key']]],
|
|
'sensitive' => 'PRIVATE-PATIENT-OUTPUT-ERROR-CREDENTIAL',
|
|
'body' => json_encode(['id' => 'synthetic-run-001', 'status' => 'succeeded',
|
|
'inputs' => ['patient' => 'PRIVATE-PATIENT-OUTPUT-ERROR-CREDENTIAL'],
|
|
'outputs' => ['answer' => 'PRIVATE-PATIENT-OUTPUT-ERROR-CREDENTIAL'],
|
|
'error' => 'PRIVATE-PATIENT-OUTPUT-ERROR-CREDENTIAL'], JSON_THROW_ON_ERROR),
|
|
'http' => 200, 'errno' => 0, 'chunks' => null, 'write_results' => [],
|
|
'config_calls' => 0, 'init_calls' => 0, 'setopt_calls' => 0, 'exec_calls' => 0,
|
|
'close_calls' => 0, 'error_reads' => 0, 'options' => [],
|
|
'config_delay_ms' => 0, 'init_delay_ms' => 0, 'setopt_delay_ms' => 0, 'exec_delay_ms' => 0,
|
|
'chunk_delay_ms' => [], 'after_body_ms' => 0, 'info_delay_ms' => 0, 'close_delay_ms' => 0,
|
|
'config_throw' => false, 'init_false' => false, 'init_throw' => false,
|
|
'setopt_false' => false, 'setopt_throw' => false, 'exec_false' => false,
|
|
'exec_throw' => false, 'close_throw' => false, 'progress' => false,
|
|
], $changes);
|
|
}
|
|
function runStatusLookup(?array $identity = null, int $timeoutMs = 1000): array {
|
|
++$GLOBALS['cases'];
|
|
ob_start();
|
|
try { $result = DifyWorkflowRunStatus::lookup($identity ?? $GLOBALS['identity'], $timeoutMs); }
|
|
finally { $output = ob_get_clean(); }
|
|
runStatusExpect($output === '', 'adapter emits no body, error or private data');
|
|
$stub = $GLOBALS['runStatusStub'];
|
|
runStatusExpect($stub['error_reads'] === 0, 'raw provider transport error is never read');
|
|
runStatusExpect($stub['exec_calls'] <= 1 && $stub['init_calls'] <= 1, 'no retry or endpoint/protocol fallback');
|
|
if ($stub['init_calls'] === 1 && !$stub['init_false'] && !$stub['init_throw']) {
|
|
runStatusExpect($stub['close_calls'] === 1, 'every initialized handle closes exactly once');
|
|
}
|
|
$encoded = json_encode($result, JSON_THROW_ON_ERROR);
|
|
foreach ([$stub['sensitive'], 'status-synthetic.invalid', 'fixture-only-key', 'synthetic-private-user',
|
|
'synthetic-task-001', 'inputs', 'outputs', 'Authorization', 'api_key'] as $private) {
|
|
runStatusExpect(!str_contains($encoded, $private), 'result excludes sensitive or unrelated metadata');
|
|
}
|
|
if (isset($stub['options'][CURLOPT_WRITEFUNCTION])) {
|
|
$variables = (new ReflectionFunction($stub['options'][CURLOPT_WRITEFUNCTION]))->getStaticVariables();
|
|
runStatusExpect($variables['body'] === '', 'bounded sensitive response buffer is discarded on every result');
|
|
}
|
|
return $result;
|
|
}
|
|
function runStatusError(array $result, string $code): void {
|
|
runStatusExpect($result === ['ok' => false, 'error_code' => $code], 'failure returns only expected static code');
|
|
}
|
|
function runStatusSuccess(array $result, string $status = 'succeeded', string $run = 'synthetic-run-001'): void {
|
|
runStatusExpect($result === ['ok' => true, 'workflow_run_id' => $run, 'status' => $status], 'success returns only exact private run/status');
|
|
}
|
|
function runStatusTransportOptions(int $timeoutMs = 1000, int $protocol = CURLPROTO_HTTPS): void {
|
|
$stub = $GLOBALS['runStatusStub']; $options = $stub['options'];
|
|
runStatusExpect($stub['config_calls'] === 1 && $stub['exec_calls'] === 1, 'one server profile read and one synthetic request');
|
|
foreach ([CURLOPT_HTTPGET => true, CURLOPT_RETURNTRANSFER => false, CURLOPT_HEADER => false, CURLOPT_VERBOSE => false,
|
|
CURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0, CURLOPT_PROXY => '', CURLOPT_NOPROXY => '*',
|
|
CURLOPT_PROTOCOLS => $protocol, CURLOPT_REDIR_PROTOCOLS => $protocol, CURLOPT_NETRC => CURL_NETRC_IGNORED,
|
|
CURLOPT_UNRESTRICTED_AUTH => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
|
|
CURLOPT_TIMEOUT_MS => $timeoutMs, CURLOPT_CONNECTTIMEOUT_MS => $timeoutMs, CURLOPT_NOSIGNAL => true,
|
|
CURLOPT_NOPROGRESS => false] as $name => $expected) {
|
|
runStatusExpect(($options[$name] ?? null) === $expected, 'GET/direct/TLS/protocol/deadline option retained');
|
|
}
|
|
foreach ([CURLOPT_POST, CURLOPT_POSTFIELDS, CURLOPT_CUSTOMREQUEST, CURLOPT_USERPWD] as $forbidden) {
|
|
runStatusExpect(!array_key_exists($forbidden, $options), 'no inference/stop/body/alternate credential option');
|
|
}
|
|
}
|
|
|
|
foreach (['qwen', 'openai'] as $profile) {
|
|
runStatusReset(); $target = array_replace($identity, ['profile' => $profile]);
|
|
runStatusSuccess(runStatusLookup($target)); runStatusTransportOptions();
|
|
runStatusExpect($runStatusStub['options'][CURLOPT_URL] === 'https://status-synthetic.invalid/v1/workflows/run/synthetic-run-001', 'configured chat endpoint becomes exact run GET');
|
|
runStatusExpect($runStatusStub['options'][CURLOPT_HTTPHEADER] === ['Authorization: Bearer ' . $profile . '-fixture-only-key', 'Accept: application/json'], 'only original configured profile key is used');
|
|
}
|
|
foreach (['https://status-synthetic.invalid', 'https://status-synthetic.invalid/',
|
|
'https://status-synthetic.invalid/v1', 'https://status-synthetic.invalid/v1/',
|
|
'https://status-synthetic.invalid/v1/chat-messages/', 'https://status-synthetic.invalid/prefix/v1/chat-messages',
|
|
'https://status-synthetic.invalid:8443/v1/chat-messages', 'http://status-synthetic.invalid/v1/chat-messages'] as $base) {
|
|
runStatusReset(); $runStatusStub['config']['base_url'] = $base;
|
|
runStatusSuccess(runStatusLookup());
|
|
$root = rtrim($base, '/');
|
|
if (str_ends_with($root, '/chat-messages')) $root = substr($root, 0, -strlen('/chat-messages'));
|
|
runStatusExpect($runStatusStub['options'][CURLOPT_URL] === $root . '/workflows/run/synthetic-run-001', 'one configured root is preserved');
|
|
runStatusTransportOptions(1000, str_starts_with($base, 'https:') ? CURLPROTO_HTTPS : CURLPROTO_HTTP);
|
|
}
|
|
runStatusReset();
|
|
runStatusSuccess(runStatusLookup($identity + ['url' => 'https://untrusted.invalid/', 'base_url' => 'https://untrusted.invalid/',
|
|
'api_key' => 'untrusted-key', 'ca_file' => 'untrusted-file']));
|
|
runStatusExpect($runStatusStub['options'][CURLOPT_URL] === 'https://status-synthetic.invalid/v1/workflows/run/synthetic-run-001'
|
|
&& !str_contains(implode(',', $runStatusStub['options'][CURLOPT_HTTPHEADER]), 'untrusted')
|
|
&& !array_key_exists(CURLOPT_CAINFO, $runStatusStub['options']), 'identity cannot select a destination, credential or CA file');
|
|
runStatusReset(); runStatusSuccess(runStatusLookup(array_replace($identity, ['user' => ''])));
|
|
foreach (['task_id', 'workflow_run_id'] as $field) {
|
|
foreach (['a', str_repeat('x', 128), 'AZaz09_-'] as $valid) {
|
|
runStatusReset(); $target = array_replace($identity, [$field => $valid]);
|
|
if ($field === 'workflow_run_id') $runStatusStub['body'] = json_encode(['id' => $valid, 'status' => 'stopped']);
|
|
runStatusSuccess(runStatusLookup($target), $field === 'workflow_run_id' ? 'stopped' : 'succeeded', $target['workflow_run_id']);
|
|
}
|
|
}
|
|
|
|
$invalidIdentities = [[]];
|
|
foreach (array_keys($identity) as $field) { $missing = $identity; unset($missing[$field]); $invalidIdentities[] = $missing; }
|
|
foreach (['task_id', 'workflow_run_id'] as $field) foreach (['', str_repeat('x', 129), 'a/b', '../run', 'https://untrusted.invalid',
|
|
'run%2Fother', "run\n", "run\0", 'run?x', 'run#x', 123, [], new stdClass()] as $bad) {
|
|
$invalidIdentities[] = array_replace($identity, [$field => $bad]);
|
|
}
|
|
foreach (['', 'QWEN', 'other', null, []] as $bad) $invalidIdentities[] = array_replace($identity, ['profile' => $bad]);
|
|
foreach ([0, -1, '1', 1.0, true, null] as $bad) $invalidIdentities[] = array_replace($identity, ['generation' => $bad]);
|
|
foreach ([str_repeat('x', 129), "user\n", "user\0", 1, [], null] as $bad) $invalidIdentities[] = array_replace($identity, ['user' => $bad]);
|
|
foreach ($invalidIdentities as $target) {
|
|
runStatusReset(); runStatusError(runStatusLookup($target), 'RUN_STATUS_IDENTITY_INVALID');
|
|
runStatusExpect($runStatusStub['config_calls'] === 0 && $runStatusStub['init_calls'] === 0, 'invalid original identity performs no config or transport work');
|
|
}
|
|
foreach ([0, -1, PHP_INT_MIN] as $timeout) {
|
|
runStatusReset(); runStatusError(runStatusLookup(null, $timeout), 'RUN_STATUS_TIMEOUT');
|
|
runStatusExpect($runStatusStub['config_calls'] === 0 && $runStatusStub['init_calls'] === 0, 'expired budget performs no config or transport work');
|
|
}
|
|
|
|
foreach ([null, false, 'bad', new stdClass()] as $bad) {
|
|
runStatusReset(['config' => $bad]); runStatusError(runStatusLookup(), 'RUN_STATUS_CONFIG_INVALID');
|
|
runStatusExpect($runStatusStub['init_calls'] === 0, 'invalid config performs no transport');
|
|
}
|
|
foreach ([false, null, 0, 'true'] as $bad) {
|
|
runStatusReset(); $runStatusStub['config']['enable'] = $bad;
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_CONFIG_DISABLED');
|
|
runStatusExpect($runStatusStub['init_calls'] === 0, 'disabled config performs no transport');
|
|
}
|
|
foreach ([[], null, 'bad', new stdClass(), ['qwen' => null], ['qwen' => 'bad'], ['qwen' => new stdClass()]] as $bad) {
|
|
runStatusReset(); $runStatusStub['config']['models'] = $bad;
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_CONFIG_INVALID');
|
|
runStatusExpect($runStatusStub['init_calls'] === 0, 'profile has no fallback key');
|
|
}
|
|
foreach (['', "bad\r\nAuthorization: leak", ' padded ', "bad\tkey", "bad\0key", str_repeat('k', 4097), null, 123, [], new stdClass()] as $bad) {
|
|
runStatusReset(); $runStatusStub['config']['models']['qwen']['api_key'] = $bad;
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_CONFIG_INVALID');
|
|
runStatusExpect($runStatusStub['init_calls'] === 0, 'malformed credential fails before cURL');
|
|
}
|
|
foreach (['', 'https://status-synthetic.invalid/v1/chat/completions', 'https://status-synthetic.invalid/v1/chat/completions/',
|
|
'https://status-synthetic.invalid/v1/CHAT/COMPLETIONS', 'https://status-synthetic.invalid/v1/chat/completions/other',
|
|
'https://status-synthetic.invalid/v1?x=1', 'https://status-synthetic.invalid/v1?',
|
|
'https://status-synthetic.invalid/v1#fragment', 'https://status-synthetic.invalid/v1#',
|
|
'https://name@status-synthetic.invalid/v1', 'https://name:pass@status-synthetic.invalid/v1',
|
|
'ftp://status-synthetic.invalid/v1', 'file:///tmp/status', '//status-synthetic.invalid/v1',
|
|
'http://', 'https://status-synthetic.invalid:999999/v1', 'https://status-synthetic.invalid/v1%2fchat-messages',
|
|
'https://status-synthetic.invalid/v1/../other', 'https://status-synthetic.invalid/v1\\other',
|
|
"https://status-synthetic.invalid/v1\n", "https://status-synthetic.invalid/v1\0", ' https://status-synthetic.invalid/v1',
|
|
'https://status-synthetic.invalid/v1 ', str_repeat('x', 2049), null, 1, [], new stdClass()] as $bad) {
|
|
runStatusReset(); $runStatusStub['config']['base_url'] = $bad;
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_CONFIG_INVALID');
|
|
runStatusExpect($runStatusStub['init_calls'] === 0, 'invalid/non-Dify destination performs no transport');
|
|
}
|
|
foreach ([['ca_file' => __DIR__ . '/definitely-absent-status-ca.pem'], ['ca_file' => __DIR__], ['ca_file' => "bad\0path"],
|
|
['ca_file' => []], ['ca_file' => null], 'bad'] as $bad) {
|
|
runStatusReset(); $runStatusStub['config']['required_attachment_upload'] = $bad;
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_CONFIG_INVALID');
|
|
runStatusExpect($runStatusStub['init_calls'] === 0, 'invalid server CA fails before transport');
|
|
}
|
|
runStatusReset(); $runStatusStub['config']['required_attachment_upload'] = ['ca_file' => __FILE__];
|
|
runStatusSuccess(runStatusLookup()); runStatusTransportOptions();
|
|
runStatusExpect($runStatusStub['options'][CURLOPT_CAINFO] === __FILE__, 'existing server CA option is forwarded with peer/hostname verification');
|
|
|
|
foreach (['scheduled', 'running', 'succeeded', 'failed', 'stopped', 'partial-succeeded', 'paused'] as $status) {
|
|
runStatusReset(['body' => json_encode(['id' => $identity['workflow_run_id'], 'status' => $status,
|
|
'task_id' => $identity['task_id'], 'user' => $identity['user'], 'profile' => $identity['profile'],
|
|
'workflow_run_id' => $identity['workflow_run_id'], 'inputs' => ['secret' => 'PRIVATE-PATIENT-OUTPUT-ERROR-CREDENTIAL']])]);
|
|
runStatusSuccess(runStatusLookup(), $status);
|
|
}
|
|
foreach (['', 'cancelled', 'Canceled', 'success', 'SUCCEEDED', 'unknown', 'stopped ', null, 1, [], ['status' => 'stopped']] as $bad) {
|
|
runStatusReset(['body' => json_encode(['id' => $identity['workflow_run_id'], 'status' => $bad, 'error' => 'PRIVATE-PATIENT-OUTPUT-ERROR-CREDENTIAL'])]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_STATUS_UNCONFIRMED');
|
|
}
|
|
runStatusReset(['body' => json_encode(['id' => $identity['workflow_run_id'], 'result' => 'success'])]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_STATUS_UNCONFIRMED');
|
|
foreach (['', 'synthetic-run-other', strtoupper($identity['workflow_run_id']), null, [], 123] as $bad) {
|
|
runStatusReset(['body' => json_encode(['id' => $bad, 'status' => 'stopped'])]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_IDENTITY_MISMATCH');
|
|
}
|
|
foreach (['{}', '[]', 'null', 'true', '123', '"succeeded"', '{"status":"stopped"}',
|
|
'{"workflow_id":"synthetic-run-001","status":"stopped"}',
|
|
'{"data":{"id":"synthetic-run-001","status":"stopped"}}'] as $body) {
|
|
runStatusReset(['body' => $body]); runStatusError(runStatusLookup(), 'RUN_STATUS_IDENTITY_MISMATCH');
|
|
}
|
|
foreach (['task_id', 'user', 'profile', 'workflow_run_id'] as $field) foreach (['foreign', null, []] as $bad) {
|
|
runStatusReset(['body' => json_encode(['id' => $identity['workflow_run_id'], 'status' => 'stopped', $field => $bad])]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_IDENTITY_MISMATCH');
|
|
}
|
|
foreach (['', '{broken', "{\"id\":\"\xff\"}", str_repeat('[', 65) . '0' . str_repeat(']', 65)] as $body) {
|
|
runStatusReset(['body' => $body]); runStatusError(runStatusLookup(), 'RUN_STATUS_RESPONSE_INVALID');
|
|
}
|
|
foreach ([0, 201, 204, 301, 302, 303, 307, 308, 400, 401, 403, 404, 429, 500, 503] as $http) {
|
|
runStatusReset(['http' => $http]); runStatusError(runStatusLookup(), 'RUN_STATUS_HTTP_ERROR');
|
|
runStatusTransportOptions();
|
|
}
|
|
$peerVerificationError = defined('CURLE_PEER_FAILED_VERIFICATION') ? constant('CURLE_PEER_FAILED_VERIFICATION') : CURLE_SSL_CACERT;
|
|
foreach ([CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT, CURLE_SSL_CONNECT_ERROR,
|
|
$peerVerificationError, CURLE_SSL_CACERT_BADFILE, CURLE_RECV_ERROR, CURLE_WRITE_ERROR] as $errno) {
|
|
runStatusReset(['errno' => $errno]); runStatusError(runStatusLookup(), 'RUN_STATUS_TRANSPORT_ERROR');
|
|
runStatusTransportOptions();
|
|
}
|
|
runStatusReset(['errno' => CURLE_OPERATION_TIMEDOUT, 'http' => 200]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_TIMEOUT');
|
|
foreach (['config_throw' => 'RUN_STATUS_CONFIG_UNAVAILABLE', 'init_false' => 'RUN_STATUS_TRANSPORT_ERROR',
|
|
'init_throw' => 'RUN_STATUS_TRANSPORT_ERROR', 'setopt_false' => 'RUN_STATUS_TRANSPORT_ERROR',
|
|
'setopt_throw' => 'RUN_STATUS_TRANSPORT_ERROR', 'exec_false' => 'RUN_STATUS_TRANSPORT_ERROR',
|
|
'exec_throw' => 'RUN_STATUS_TRANSPORT_ERROR'] as $flag => $code) {
|
|
runStatusReset([$flag => true]); runStatusError(runStatusLookup(), $code);
|
|
}
|
|
runStatusReset(['close_throw' => true]); runStatusSuccess(runStatusLookup());
|
|
|
|
$small = json_encode(['id' => $identity['workflow_run_id'], 'status' => 'succeeded', 'outputs' => ''], JSON_THROW_ON_ERROR);
|
|
$limitBody = substr($small, 0, -2) . str_repeat('x', 1048576 - strlen($small)) . '"}';
|
|
runStatusExpect(strlen($limitBody) === 1048576, 'synthetic exact body boundary is one MiB');
|
|
runStatusReset(['body' => $limitBody]); runStatusSuccess(runStatusLookup());
|
|
runStatusReset(['chunks' => [substr($limitBody, 0, 1048566), substr($limitBody, 1048566)]]);
|
|
runStatusSuccess(runStatusLookup());
|
|
foreach ([[$limitBody . ' '], [$limitBody, ' '], [str_repeat('x', 1048577)]] as $chunks) {
|
|
runStatusReset(['chunks' => $chunks]); runStatusError(runStatusLookup(), 'RUN_STATUS_RESPONSE_TOO_LARGE');
|
|
runStatusExpect(end($runStatusStub['write_results']) === 0, 'oversize body aborts cURL before accumulating beyond one MiB');
|
|
}
|
|
|
|
foreach ([1, 137, 713, 1000, 1500, PHP_INT_MAX] as $timeout) {
|
|
runStatusReset(); runStatusSuccess(runStatusLookup(null, $timeout));
|
|
runStatusTransportOptions(min(1000, $timeout));
|
|
}
|
|
runStatusReset(['config_delay_ms' => 100, 'init_delay_ms' => 50]);
|
|
runStatusSuccess(runStatusLookup()); runStatusTransportOptions(850);
|
|
foreach (['config_delay_ms', 'init_delay_ms', 'setopt_delay_ms', 'exec_delay_ms', 'after_body_ms', 'info_delay_ms', 'close_delay_ms'] as $stage) {
|
|
runStatusReset([$stage => 1000]); runStatusError(runStatusLookup(), 'RUN_STATUS_TIMEOUT');
|
|
if (in_array($stage, ['config_delay_ms', 'init_delay_ms', 'setopt_delay_ms'], true)) {
|
|
runStatusExpect($runStatusStub['exec_calls'] === 0, 'setup expiration performs no GET');
|
|
}
|
|
}
|
|
runStatusReset(['exec_delay_ms' => 999]); runStatusSuccess(runStatusLookup());
|
|
runStatusReset(['exec_delay_ms' => 1000, 'progress' => true]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_TIMEOUT');
|
|
runStatusExpect($runStatusStub['progress_result'] === 1 && $runStatusStub['write_results'] === [], 'progress callback aborts at exact monotonic deadline');
|
|
runStatusReset(['chunks' => ['{"id":', '"synthetic-run-001","status":"stopped"}'], 'chunk_delay_ms' => [100, 900]]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_TIMEOUT');
|
|
runStatusExpect($runStatusStub['write_results'] === [6, 0], 'late body chunk cannot supply a terminal');
|
|
runStatusReset(['config_delay_ms' => 999, 'init_delay_ms' => 1]);
|
|
runStatusError(runStatusLookup(), 'RUN_STATUS_TIMEOUT');
|
|
runStatusExpect($runStatusStub['exec_calls'] === 0, 'remaining fraction never becomes a fresh timeout');
|
|
|
|
echo 'Dify workflow exact-run status adapter: OK (' . $cases . ' synthetic cases, ' . $checks . " assertions; zero network/config-file/model/DB/control operations)\n";
|
|
}
|