88 lines
4.9 KiB
PHP
88 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\service\AssistantRequestControl;
|
|
use app\adminapi\http\middleware\AuthMiddleware;
|
|
|
|
function cancelExpect(bool $ok, string $message): void {
|
|
if (!$ok) throw new RuntimeException($message);
|
|
}
|
|
|
|
if (($argv[1] ?? '') === 'worker') {
|
|
$control = new AssistantRequestControl(7, 11, $argv[4], $argv[3]);
|
|
if ($argv[2] === 'cancel') echo json_encode($control->requestCancel());
|
|
else echo json_encode(['began' => $control->begin()]);
|
|
exit;
|
|
}
|
|
|
|
$directory = sys_get_temp_dir() . '/zyt-assistant-control-' . bin2hex(random_bytes(8));
|
|
$id = '12345678-1234-4234-8234-123456789abc';
|
|
$make = static fn (int $admin = 7, int $diagnosis = 11): AssistantRequestControl => new AssistantRequestControl($admin, $diagnosis, $id, $directory);
|
|
try {
|
|
$control = $make();
|
|
$tombstone = $control->requestCancel();
|
|
cancelExpect($tombstone['status'] === 'cancel_requested' && !$tombstone['transport_closed'] && !$tombstone['upstream_stop_confirmed'], 'early cancel is only requested');
|
|
cancelExpect($control->begin() && $control->shouldAbort(), 'prepare preserves cancellation that arrived before begin');
|
|
cancelExpect(!$control->begin(), 'same request id cannot reenter or resubmit');
|
|
cancelExpect(!$make(8)->shouldAbort() && !$make(7, 12)->shouldAbort(), 'admin and canonical diagnosis isolate cancellation');
|
|
$control->bindTask('qwen', 'synthetic-user', 'synthetic-task');
|
|
$calls = 0;
|
|
$stop = static function (array $task) use (&$calls): bool { ++$calls; return $task['task_id'] === 'synthetic-task'; };
|
|
cancelExpect($control->requestCancel($stop)['upstream_stop_confirmed'], 'bound task can be stopped by a different control instance');
|
|
$make()->requestCancel($stop);
|
|
cancelExpect($calls === 1, 'confirmed stop is idempotent');
|
|
$control->finish('cancelled');
|
|
cancelExpect($control->publicState()['transport_closed'], 'owning worker alone confirms transfer has ended');
|
|
cancelExpect(!str_contains(json_encode($control->publicState()), 'synthetic-task'), 'public status excludes task identity');
|
|
|
|
$id2 = '22345678-1234-4234-8234-123456789abc';
|
|
$control2 = new AssistantRequestControl(7, 11, $id2, $directory);
|
|
cancelExpect($control2->begin(), 'fresh request begins');
|
|
$run = static function (string $mode, string $requestId) use ($directory): array {
|
|
$pipes = [];
|
|
$process = proc_open([PHP_BINARY, __FILE__, 'worker', $mode, $directory, $requestId],
|
|
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
|
|
if (!is_resource($process)) throw new RuntimeException('Cannot start independent PHP worker');
|
|
fclose($pipes[0]);
|
|
return [$process, $pipes];
|
|
};
|
|
[$process, $pipes] = $run('cancel', $id2);
|
|
$status = json_decode(stream_get_contents($pipes[1]), true);
|
|
$stderr = stream_get_contents($pipes[2]);
|
|
fclose($pipes[1]); fclose($pipes[2]);
|
|
cancelExpect(proc_close($process) === 0 && $stderr === '' && $status['cancel_requested'], 'independent PHP process accepts cancellation');
|
|
cancelExpect($control2->shouldAbort(), 'original process observes cancellation through flock state');
|
|
|
|
$id3 = '32345678-1234-4234-8234-123456789abc';
|
|
$workers = [];
|
|
for ($i = 0; $i < 4; ++$i) $workers[] = $run('begin', $id3);
|
|
$began = 0;
|
|
foreach ($workers as [$process, $pipes]) {
|
|
$result = json_decode(stream_get_contents($pipes[1]), true);
|
|
$stderr = stream_get_contents($pipes[2]);
|
|
fclose($pipes[1]); fclose($pipes[2]);
|
|
cancelExpect(proc_close($process) === 0 && $stderr === '', 'racing worker terminates cleanly');
|
|
$began += (int) $result['began'];
|
|
}
|
|
cancelExpect($began === 1, 'flock allows exactly one of four independent workers to begin');
|
|
|
|
$path = $directory . '/' . hash('sha256', '7:11:' . $id3) . '.json';
|
|
file_put_contents($path, json_encode(['begun' => true, 'expires_at' => time() - 1]));
|
|
touch($path, time() - 700);
|
|
new AssistantRequestControl(7, 11, $id3, $directory);
|
|
cancelExpect(!is_file($path), 'expired request files are reclaimed under a directory guard');
|
|
|
|
$alias = new ReflectionMethod(AuthMiddleware::class, 'matchPermissionAlias');
|
|
cancelExpect($alias->invoke(new AuthMiddleware(), 'tcm.diagnosis/cancelaiassistant', ['tcm.diagnosis/aiassistant']), 'cancel action uses existing assistant menu permission');
|
|
cancelExpect(!$alias->invoke(new AuthMiddleware(), 'tcm.diagnosis/cancelaiassistant', []), 'cancel alias denies ungranted administrator');
|
|
cancelExpect(!AssistantRequestControl::validRequestId('../secret') && !AssistantRequestControl::validRequestId('arbitrary'), 'only UUID identities are accepted');
|
|
echo "Diagnosis AI assistant cancellation / independent Windows PHP workers: OK\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);
|
|
}
|