Files
Dune/tools/admin-seat-ai-verification.mjs
T
Quintine 2cddf03932 Prototype paused admin participant AI support
Enable an existing AI difficulty for a human seat through admin controls without granting private access or advancing paused play. Preserve custody, credentials, takeback and separate paced resume. Bind atomic writes and outgoing-grant revocation to live authority, room/pause versions and active seat access with durable exact receipts.

Validation: types/lint, 5408 offline tests, build and 52 HTTP tests passed on source fingerprint 2157de0f4bb0b74d9760d52102601c52344cf674dee8ccd1d559f0a246a1d4ff; 22 authenticated admin HTTP groups, independent SQL/privacy/UI review, desktop/mobile and refresh checks. All 2098 baseline rooms and 4455 seats preserved. Migration 0015 is additive. Positive production admin QA awaits authorized access; full admin/rules scope remains unfinished.
2026-09-26 14:04:17 +10:00

82 lines
5.9 KiB
JavaScript

// All mutations use a newly created QA room. Never accepts an existing human room.
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
export async function verifyParticipantAi(request, adminCookie, report) {
const made = await request('/api/rooms', { name: 'Participant AI acceptance QA', faction: 'atreides', advanced: false, expansions: [] });
assert.equal(made.status, 201);
const code = made.data.code, host = made.cookie, path = '/api/rooms/' + code, adminPath = '/api/admin/rooms/' + code;
report.participantAiRoom = code;
const joined = await request(path, { type: 'join', name: 'Participant AI guest QA', faction: 'emperor' });
assert.equal(joined.status, 200);
const guest = joined.cookie;
async function action(cookie, action) {
const view = await request(path, undefined, cookie); assert.equal(view.status, 200);
return request(path, { version: view.data.version, action }, cookie);
}
async function pause(paused) {
const current = await request(adminPath + '/control', undefined, adminCookie); assert.equal(current.status, 200);
const result = await request(adminPath + '/control', { operationId: randomUUID(), expectedRevision: current.data.revision, paused, joinLocked: false, reason: 'Participant AI acceptance QA' }, adminCookie);
assert.equal(result.status, 200);
}
let failure;
try {
for (const cookie of [host, guest]) assert.equal((await action(cookie, { type: 'ready' })).status, 200);
assert.equal((await action(host, { type: 'start' })).status, 200);
const running = await request(adminPath + '/seat-ai', undefined, adminCookie); assert.equal(running.status, 200);
assert.equal(running.data.editable, false);
const early = { operationId: randomUUID(), expectedVersion: running.data.version, expectedControlRevision: running.data.controlRevision, target: made.data.me, difficulty: 'Easy', reason: 'Participant AI acceptance QA' };
assert.equal((await request(adminPath + '/seat-ai', early, adminCookie)).status, 409);
await pause(true);
const ready = await request(adminPath + '/seat-ai', undefined, adminCookie); assert.equal(ready.status, 200); assert.equal(ready.data.editable, true);
const input = { ...early, operationId: randomUUID(), expectedVersion: ready.data.version, expectedControlRevision: ready.data.controlRevision };
for (const denied of [
await request(adminPath + '/seat-ai', input, host),
await request(adminPath + '/seat-ai', input, adminCookie, 'https://foreign.invalid'),
await request(adminPath + '/seat-ai', input, adminCookie, undefined, randomUUID()),
]) { assert.ok([401, 403].includes(denied.status)); assert.equal(denied.rawCookie, null); }
const before = (await request(path, undefined, host)).data;
const changed = await request(adminPath + '/seat-ai', input, adminCookie);
assert.equal(changed.status, 200); assert.equal(changed.rawCookie, null); assert.equal(changed.data.replayed, false);
assert.equal(changed.data.appliedVersion, input.expectedVersion + 1); assert.equal(changed.data.room.paused, true);
assert.deepEqual(Object.keys(changed.data.room.players[0]).sort(), ['control','difficulty','eligible','faction','id','name']);
const frozen = (await request(path, undefined, host)).data;
assert.equal(frozen.me, before.me); assert.equal(frozen.roomControl.paused, true);
assert.deepEqual(frozen.players, before.players.map(player => player.id === frozen.me ? { ...player, autopilot: 'Easy' } : player));
assert.equal((await request(path, undefined, guest)).data.me, joined.data.me);
await delay(2200);
assert.deepEqual((await request(path, undefined, host)).data, frozen, 'Paused AI must not advance');
const replay = await request(adminPath + '/seat-ai', input, adminCookie); assert.equal(replay.status, 200); assert.equal(replay.data.replayed, true);
assert.deepEqual((await request(path, undefined, host)).data, frozen);
report.checks.push('Participant AI requires paused room and live authority; enable preserves private seats and cannot advance paused play or duplicate its operation');
const takeback = await action(host, { type: 'setAutopilot', difficulty: null }); assert.equal(takeback.status, 200);
assert.equal(takeback.data.players.find(player => player.id === takeback.data.me).autopilot, undefined);
assert.equal(takeback.data.roomControl.paused, true);
const afterTakeback = await request(adminPath + '/seat-ai', input, adminCookie); assert.equal(afterTakeback.status, 200); assert.equal(afterTakeback.data.replayed, true);
assert.deepEqual((await request(path, undefined, host)).data, takeback.data);
await pause(false);
const resumed = (await request(path, undefined, host)).data;
assert.equal(resumed.roomControl.paused, false);
assert.equal((await request(adminPath + '/seat-ai', input, adminCookie)).data.replayed, true);
assert.deepEqual((await request(path, undefined, host)).data, resumed);
report.checks.push('Human takeback works while paused; exact participant retry after takeback and resume cannot re-enable AI');
} catch (error) {
report.failureRequest ??= report.lastRequest;
report.participantAiFailure = 'Participant AI acceptance failed before cleanup.';
failure = error;
} finally {
try {
const view = await request(path, undefined, host); assert.equal(view.status, 200);
if (view.data.players.find(player => player.id === view.data.me)?.autopilot)
assert.equal((await action(host, { type: 'setAutopilot', difficulty: null })).status, 200);
const current = await request(adminPath + '/control', undefined, adminCookie); assert.equal(current.status, 200);
if (current.data.paused) await pause(false);
} catch (error) {
report.participantAiCleanupFailure = { message: 'The QA room cleanup could not be confirmed.', request: report.lastRequest };
failure ??= error;
}
}
if (failure) throw failure;
}