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

144 lines
12 KiB
PHP

<?php
declare(strict_types=1);
// Actual transport with literal canonical controller closures and deterministic
// in-process I/O only. No sockets, real config, auth, bootstrap or model.
namespace app\adminapi\service {
function hrtime(bool $number = false) { return \app\common\service\hrtime($number); }
function usleep(int $us): void { $GLOBALS['r26bwNs'] += $us * 1000; }
}
namespace think\facade { class Log { public static function warning(...$unused): void {} } }
namespace app\common\service {
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
function hrtime(bool $number = false) { $n = $GLOBALS['r26bwNs']; return $number ? $n : [intdiv($n, 1000000000), $n % 1000000000]; }
function config(string $unused): array { return ['enable' => true, 'base_url' => 'https://independent.invalid/v1/chat-messages',
'timeout' => 90, 'models' => ['qwen' => ['name' => 'independent', 'api_key' => 'private-independent-key']]]; }
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): bool {
$generation = ++$GLOBALS['r26bwPosts']; $GLOBALS['r26bwInCurl'] = true;
$GLOBALS['r26bwCaps'][] = $handle->options[CURLOPT_TIMEOUT_MS];
try {
($handle->options[CURLOPT_HEADERFUNCTION])($handle, "HTTP/1.1 200 OK\r\n");
$frames = [['event' => 'workflow_started', 'task_id' => 'private-wire-task-' . $generation, 'workflow_run_id' => 'private-wire-run-' . $generation],
['event' => 'message', 'task_id' => 'private-wire-task-' . $generation, 'answer' => "临床甲\r\n乙。"],
['event' => 'workflow_finished', 'task_id' => 'private-wire-task-' . $generation, 'workflow_run_id' => 'private-wire-run-' . $generation,
'data' => ['status' => $GLOBALS['r26bwProducer'][$generation]]], ['event' => 'message_end']];
if (!empty($GLOBALS['r26bwMissingIdentity'])) {
$frames[0] = ['event' => 'node_started'];
unset($frames[1]['task_id']);
// A terminal-only pair must remain incapable of establishing
// ownership. The submitted transfer itself still happened.
}
foreach ($frames as $frame) {
$GLOBALS['r26bwNs'] += 1000000;
($handle->options[CURLOPT_WRITEFUNCTION])($handle, 'data: ' . json_encode($frame, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n\n");
}
return true;
} finally { $GLOBALS['r26bwInCurl'] = false; }
}
function curl_errno(\stdClass $handle): int { return 0; }
function curl_getinfo(\stdClass $handle, int $option): int|float { return $option === CURLINFO_HTTP_CODE ? 200 : 0.001; }
function curl_close(\stdClass $handle): void { $GLOBALS['r26bwClosed'] = $GLOBALS['r26bwPosts']; }
class DifyWorkflowRunStatus {
public static function lookup(array $id, int $timeoutMs): array {
++$GLOBALS['r26bwGets'];
\r26bwExpect(!$GLOBALS['r26bwInCurl'] && $GLOBALS['r26bwClosed'] === $GLOBALS['r26bwPosts'], 'GET only after this actual handle close and outside libcurl callbacks');
\r26bwExpect(count($GLOBALS['r26bwTiming']) === $GLOBALS['r26bwPosts'], 'transport timing published before generation verifier I/O');
$g = $id['generation'];
\r26bwExpect($id === ['profile' => 'qwen', 'user' => 'private-wire-user', 'task_id' => 'private-wire-task-' . $g,
'workflow_run_id' => 'private-wire-run-' . $g, 'generation' => $g], 'actual canonical EOF verifies own immutable generation pair');
\r26bwExpect($timeoutMs > 0 && $timeoutMs <= 1000, 'canonical EOF passes bounded I/O time');
$GLOBALS['r26bwNs'] += 300000000;
return ['ok' => true, 'workflow_run_id' => $id['workflow_run_id'], 'status' => $GLOBALS['r26bwPersisted'][$g]];
}
}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\service\AssistantRequestControl as Control;
use app\common\service\DifyChatService;
$checks = 0; $failures = []; $observations = [];
function r26bwExpect(bool $ok, string $label): void { ++$GLOBALS['checks']; if (!$ok) $GLOBALS['failures'][] = $label; }
$source = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/tcm/DiagnosisController.php');
r26bwExpect(hash('sha256', $source) === '83d47bf0ffb47c0f06a0cd69db910d28c0e790ccbfb5e5d3e5c931bf84f72568', 'exact canonical controller pin');
$closures = [];
foreach (['on_submission', 'on_task', 'on_terminal', 'on_transfer_end', 'can_start_attempt', 'on_timeout'] as $key) {
$pattern = in_array($key, ['on_submission', 'on_task'], true) ? '/\$options\[\'' . preg_quote($key, '/') . '\'\] = (static function.*?\n });/s'
: '/\'' . preg_quote($key, '/') . '\' => (static function.*?\n }),/s';
if (preg_match_all($pattern, $source, $matches) !== 1) throw new RuntimeException('Canonical closure missing or ambiguous');
$closures[$key] = $matches[1][0];
}
$directory = sys_get_temp_dir() . '/r26b-independent-wiring-' . bin2hex(random_bytes(8));
try {
foreach (['running_blocks', 'different_terminal_then_successor', 'verification_consumes_deadline', 'submitted_without_original_identity'] as $case => $label) {
$GLOBALS['r26bwNs'] = 100000000000; $GLOBALS['r26bwPosts'] = $GLOBALS['r26bwGets'] = $GLOBALS['r26bwClosed'] = 0;
$GLOBALS['r26bwInCurl'] = false; $GLOBALS['r26bwCaps'] = $GLOBALS['r26bwTiming'] = [];
$GLOBALS['r26bwMissingIdentity'] = $case === 3;
$GLOBALS['r26bwProducer'] = [1 => 'stopped', 2 => 'succeeded'];
$GLOBALS['r26bwPersisted'] = [1 => $case === 0 ? 'running' : 'failed', 2 => 'succeeded'];
$uuid = sprintf('c4567890-1234-4234-8234-%012d', $case + 1);
$control = new Control(17, 11, $uuid, $directory); r26bwExpect($control->begin(), 'fresh wiring request begins ' . $label);
$profile = 'qwen'; $upstreamUser = 'private-wire-user'; $options = [];
foreach ($closures as $key => $literal) $options[$key] = eval('namespace app\\adminapi\\controller\\tcm; use app\\common\\service\\DifyWorkflowRunStatus; return ' . $literal . ';');
$options['on_upstream_timing'] = static function ($timing): void {
r26bwExpect(!$GLOBALS['r26bwInCurl'] && $GLOBALS['r26bwClosed'] === $GLOBALS['r26bwPosts'], 'timing publication follows curl close');
$snapshot = $timing->snapshot();
$GLOBALS['r26bwTiming'][] = ['at_ns' => $GLOBALS['r26bwNs'], 'duration_ms' => $snapshot['duration_ms']];
};
$options['request_deadline_ns'] = 100000000000 + ($case === 2 ? 200000000 : 90000000000);
$options['timeout_ms'] = 90000;
$received = '';
$call = static function () use (&$received, &$options): array {
return DifyChatService::streamChat('qwen', [], 'private-wire-query', 'private-wire-user',
static function (string $bytes) use (&$received): bool { $received .= $bytes; return true; }, static fn (): bool => false, [], $options);
};
$first = $call(); $firstAfter = $GLOBALS['r26bwNs'];
r26bwExpect($received === "临床甲\r\n乙。" && !empty($first['ok']), 'actual transport preserves exact original clinical bytes ' . $label);
r26bwExpect($GLOBALS['r26bwTiming'][0] === ['at_ns' => 100004000000, 'duration_ms' => 4]
&& ($case === 3 ? $firstAfter === 100004000000 : $firstAfter >= 100304000000), 'lookup delay excluded from transport duration and observed origin ' . $label);
$canStartAfterFirst = $control->canStartAttempt();
if ($case === 3) r26bwExpect(!$canStartAfterFirst, 'submitted transfer with no original bound identity cannot authorize another generation');
$second = $call();
if ($case === 0) {
r26bwExpect(($second['error_code'] ?? '') === 'UPSTREAM_CLEANUP_PENDING' && $GLOBALS['r26bwPosts'] === 1 && $GLOBALS['r26bwGets'] === 2,
'real stopped-running EOF blocks successor without another model transfer or lookup');
r26bwExpect($control->publicState()['lifecycle_conflict'] && $control->publicState()['upstream_pending_task_count'] === 1, 'actual running conflict retained');
} elseif ($case === 1) {
r26bwExpect(!empty($second['ok']) && $GLOBALS['r26bwPosts'] === 2 && $GLOBALS['r26bwGets'] === 2, 'independently terminal predecessor permits exactly next generation');
r26bwExpect($GLOBALS['r26bwCaps'][0] === 92500 && $GLOBALS['r26bwCaps'][1] < 92500 && $GLOBALS['r26bwCaps'][1] > 90000,
'later actual transport receives only original remaining request generation budget plus existing drain');
$path = $directory . '/' . hash('sha256', '17:11:' . $uuid) . '.json';
$s = json_decode(file_get_contents($path), true, 64, JSON_THROW_ON_ERROR);
r26bwExpect(count($s['completed_generations']) === 2 && $s['completed_generations'][1]['producer_terminal_status'] === 'stopped'
&& $s['completed_generations'][1]['persisted_run_status'] === 'failed', 'different-terminal archive retained through real subsequent generation');
r26bwExpect($control->publicState()['lifecycle_conflict'] && $control->publicState()['upstream_pending_task_count'] === 0, 'prior conflict retained separately from now resolved lifecycle');
} elseif ($case === 2) {
r26bwExpect(($second['error_code'] ?? '') === 'UPSTREAM_TIMEOUT' && $GLOBALS['r26bwPosts'] === 1 && $GLOBALS['r26bwGets'] === 1,
'verification uses elapsed wall time and expired original request deadline cannot start successor');
r26bwExpect($control->publicState()['timeout_reached'] && !$control->publicState()['cancel_requested'], 'deadline remains ordinary timeout intent');
} else {
r26bwExpect(($second['error_code'] ?? '') === 'UPSTREAM_CLEANUP_PENDING' && $GLOBALS['r26bwPosts'] === 1 && $GLOBALS['r26bwGets'] === 0,
'unidentified submitted generation blocks successor without guessing a run or making a GET');
}
$safe = json_encode($control->publicState(), JSON_THROW_ON_ERROR);
foreach (['private-wire-', 'c4567890-', 'independent.invalid'] as $private) r26bwExpect(!str_contains($safe, $private), 'canonical safe state excludes private scope ' . $label);
$observations[] = ['label' => $label, 'synthetic_model_transfer_count' => $GLOBALS['r26bwPosts'],
'synthetic_status_get_count' => $GLOBALS['r26bwGets'], 'can_start_after_first_transfer' => $canStartAfterFirst,
'second_transport_ok' => ($second['ok'] ?? false) === true, 'second_error_code' => $second['error_code'] ?? null,
'state' => $control->publicState()];
}
r26bwExpect(!class_exists('think\\App', false), 'no framework bootstrap loaded by composed offline wiring');
echo json_encode(['scope' => 'r26b repaired actual transport/canonical EOF timing and handoff review', 'assertions' => $checks,
'failures' => $failures, 'observations' => $observations, 'network_calls' => 0, 'model_calls' => 0], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n";
} finally {
$real = realpath($directory); $temp = realpath(sys_get_temp_dir());
if ($real !== false) {
if ($temp === false || !str_starts_with(str_replace('\\', '/', $real), rtrim(str_replace('\\', '/', $temp), '/') . '/r26b-independent-wiring-')) throw new RuntimeException('Synthetic cleanup boundary');
foreach (new DirectoryIterator($real) as $entry) { if ($entry->isDot()) continue; if ($entry->isLink() || !$entry->isFile()) throw new RuntimeException('Unexpected synthetic entry'); unlink($entry->getPathname()); }
rmdir($real);
}
}
exit($failures === [] ? 0 : 1);
}