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

74 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
// Real monotonic clock + actual unlocked sleep; synthetic temporary control and
// status callbacks only. No app bootstrap, network, model, DB or real requests.
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\service\AssistantRequestControl as Control;
$checks = 0;
$expect = static function (bool $ok, string $message) use (&$checks): void {
++$checks; if (!$ok) throw new RuntimeException($message);
};
$directory = sys_get_temp_dir() . '/zyt-real-delay-terminal-' . bin2hex(random_bytes(8));
$uuid = 'b1324567-1234-4234-8234-123456789012';
$control = new Control(7, 11, $uuid, $directory);
$path = $directory . '/' . hash('sha256', '7:11:' . $uuid) . '.json';
try {
$expect($control->begin(), 'new synthetic control begins');
$token = $control->beginSubmission('openai', 'synthetic-real-delay-user');
$control->bindTask('openai', 'synthetic-real-delay-user', 'synthetic-real-delay-task', 'synthetic-real-delay-run', $token);
$control->signalCancel(); $control->markStop(true, 'synthetic-real-delay-task');
$control->markTerminal('synthetic-real-delay-task', 'failed', 'synthetic-real-delay-run');
$times = $timeouts = [];
$started = hrtime(true) / 1e9;
$result = $control->closeSubmittedGeneration($token, 'synthetic-real-delay-task', 'unconfirmed',
static function (array $identity, int $ms) use (&$times, &$timeouts, $started, $directory, $path, $expect, $uuid): array {
$elapsed = hrtime(true) / 1e9 - $started;
$times[] = $elapsed; $timeouts[] = $ms;
foreach ([$directory . '/.gc.lock', $path] as $file) {
$handle = fopen($file, 'c+'); $acquired = flock($handle, LOCK_EX | LOCK_NB);
$expect($acquired, 'no file or GC lock is held during real-clock lookup');
if ($acquired) flock($handle, LOCK_UN); fclose($handle);
}
$peer = new Control(7, 11, $uuid, $directory);
$expect($peer->publicState()['cancel_requested'], 'independent worker reads cancellation during cleanup');
return ['ok' => true, 'workflow_run_id' => $identity['workflow_run_id'],
'status' => $elapsed >= 10.1 ? 'stopped' : 'running'];
});
$elapsed = hrtime(true) / 1e9 - $started;
$state = json_decode((string) file_get_contents($path), true, 32, JSON_THROW_ON_ERROR);
$public = $control->publicState();
$expect(count($times) === 2 && $times[0] < 0.25 && $times[1] >= 10.99 && $elapsed < 12.25,
'actual real-clock wait spans bounded teardown with exactly two reads and no added window');
$expect($timeouts[0] === 1000 && $timeouts[1] > 0 && $timeouts[1] <= 1000, 'both actual I/O allocations are at most one second');
$expect($result['confirmed'] && $public['upstream_pending_task_count'] === 0 && !$control->canStartAttempt(),
'later independent stopped clears only cleanup and never bypasses user cancellation');
$expect($public['producer_terminal_status'] === 'failed' && $public['persisted_run_status'] === 'stopped' && $public['lifecycle_conflict'],
'real-delay cleanup preserves producer/persisted disagreement without relabelling');
$archive = array_values($state['completed_generations'])[0];
$expect(abs($archive['verification_deadline_at'] - $archive['verification_started_at'] - 12.0) < 0.000001,
'real-clock generation retains its frozen twelve-second deadline');
$again = $control->closeSubmittedGeneration($token, 'synthetic-real-delay-task', 'unconfirmed', static function (): array {
throw new RuntimeException('completed generation must never dispatch another lookup');
});
$expect(!$again['pending'] && count($times) === 2, 'repeated close never starts another verification window');
echo "Real-delay terminal confirmation: {$checks} assertions passed; elapsed_ms=" . (int) floor($elapsed * 1000)
. "; no network/model/DB.\n";
} finally {
$resolved = realpath($directory); $temp = realpath(sys_get_temp_dir());
if ($resolved !== false) {
if ($temp === false || !str_starts_with(str_replace('\\', '/', $resolved), rtrim(str_replace('\\', '/', $temp), '/') . '/zyt-real-delay-terminal-')) {
throw new RuntimeException('Synthetic cleanup boundary mismatch');
}
foreach (new DirectoryIterator($resolved) as $file) {
if ($file->isDot()) continue;
if ($file->isLink() || !$file->isFile()) throw new RuntimeException('Unexpected synthetic cleanup entry');
unlink($file->getPathname());
}
rmdir($resolved);
}
}