Files
zyt/server/tests/DiagnosisAiAssistantCancellationTest.php
2026-10-23 09:26:49 +08:00

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);
}