Files
Dune/tests/discovery-storm-recovery.test.ts
T
Quintine f0d3d9c464 Prototype Jacurutu income, Testing Station and Shrine effects
Connect automatic unambiguous Jacurutu physical-force income, a sole-occupant
Testing Station storm choice and Shrine reciprocal physical card roles to
engine actions, player controls, legal AI and saved continuation. Preserve
Weather Control exclusion, exact card/force custody, private hand boundaries
and committed effects after restart. Shared role checks cover existing ordinary
and special Karama paths; reserved cards cannot enter another pending use.

Keep Orgiz, mixed-force Jacurutu allocation and contested Cistern/Testing
Station benefits explicitly pending. These are bounded prototypes; complete
Discovery/Advanced/expansion and publication gates remain closed.

Validation: types, lint and 4,345 offline tests passed (32 added), production
build passed, and 40 HTTP tests passed. Independent cross-reviews repaired
reserved-card deadlock, physical-card selection, alternate-role permission,
storm source/token binding and public-entitlement bot ordering. The first broad
run found five failures; focused repairs preceded the stable repeated check.

Browser A5EJQ2W4 used genuine Discovery setup/reveal/free entry with targeted QA
positioning. A human increased storm movement from two to three sectors,
moving sector 6 to 9. Refresh restored station forces and the private Snooper.
This was targeted control/recovery evidence, not an unmodified full game.
All 187 opening saved rooms remained unchanged; 31 QA/HTTP rooms were added
(218 total). Reused the running server; no restart or save reset.

Source-bound report:
/home/quintine/.local/share/dune/checkpoints/20260913-discovery-location-effects/verification-final/report.json
Source fingerprint: beecb85ab7ec39319115389c115434d161591e8549802c542a6343a0fc19bea6
Durations: check 140540ms; build 6741ms; integration 24956ms.
Aggregate goal counter 63,116,444 to 64,295,091 at final verification:
1,178,647 tokens before final Git/push overhead for three bounded functions.
Standing authorization: Push all from now on.
2026-09-13 14:50:09 +10:00

147 lines
5.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { unitStore } from './fixture-nexus-room-store';
import {
discoveryStormFixture,
confirmDiscoveryStorm,
} from './fixture-discovery-storm';
import { discoveryStormActions } from '../game/discovery-storm-options';
import {
createStormSource,
discoveryStormSignature,
} from '../game/discovery-storm';
import { DiscoveryStormDecision } from '../components/discovery-storm';
import { normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
import type { RoomsClock } from '../db/rooms';
const clock: RoomsClock = { now: () => 10000, sleep: async () => {} };
void test('restored SQLite station choice keeps private controls and competing submissions adjust the storm only once', async () => {
const store = unitStore();
try {
const created = await store.rooms.createRoom(
'Station recovery',
'atreides',
false,
[],
);
const code = created.view.code,
sessions = [created.token];
for (const faction of ['guild', 'fremen'] as const)
sessions.push(
(await store.rooms.joinRoom(code, faction, faction)).token!,
);
const auths = await Promise.all(
sessions.map((token) => store.rooms.authenticate(code, token)),
);
const ids = auths.map((auth) => auth.playerId) as [string, string, string];
const game = confirmDiscoveryStorm(discoveryStormFixture(false, ids));
game.code = code;
store.sqlite
.prepare('UPDATE rooms SET state = ?, version = ? WHERE code = ?')
.run(JSON.stringify(game), game.version, code);
const seats = store.sqlite
.prepare('SELECT * FROM seats ORDER BY player_id')
.all();
for (const [i, auth] of auths.entries()) {
const view = await store.restart().readSeatView(code, auth);
assert.deepEqual(view, viewGame(game, auth.playerId));
assert.equal(view.ecologicalStorm !== null, i === 0);
assert.equal(JSON.stringify(view).includes('stormMovementSource'), false);
}
const action = discoveryStormActions(viewGame(game, ids[0])).find(
(a) => a.delta === 1,
)!;
let arrivals = 0,
release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
store.hooks.beforeWrite = async () => {
if (++arrivals === 2) release();
await gate;
};
const timer = setTimeout(release, 2000);
try {
const results = await Promise.allSettled(
[0, 1].map(() =>
store.restart().act(code, auths[0], game.version, action, clock),
),
);
assert.equal(arrivals, 2);
assert.equal(results.filter((r) => r.status === 'fulfilled').length, 1);
} finally {
clearTimeout(timer);
delete store.hooks.beforeWrite;
}
const done = await store.restart().readRoom(code);
assert.equal(done.version, game.version + 1);
assert.equal(done.storm, 9);
assert.equal(done.ecologicalStorm!.stage, 'complete');
assert.equal(
done.log.filter((e) => e.text.includes('increased storm movement'))
.length,
1,
);
await assert.rejects(
store.restart().act(code, auths[0], done.version, action, clock),
);
assert.deepEqual(await store.restart().readRoom(code), done);
assert.deepEqual(
store.sqlite.prepare('SELECT * FROM seats ORDER BY player_id').all(),
seats,
);
} finally {
store.sqlite.close();
}
});
void test('station controls show all legal distances only to the owner and disable while busy or on autopilot', () => {
const g = confirmDiscoveryStorm(discoveryStormFixture());
const render = (id: string, busy = false) =>
renderToStaticMarkup(
createElement(DiscoveryStormDecision, {
game: viewGame(g, id),
busy,
act() {},
}),
);
const markup = render('a');
for (const label of [
'Decrease: 1 sector',
'Keep: 2 sectors',
'Increase: 3 sectors',
])
assert.ok(markup.includes(label));
assert.equal(render('g'), '');
assert.equal((render('a', true).match(/\sdisabled=""/g) ?? []).length, 3);
g.players[0].autopilot = 'Easy';
assert.equal((render('a').match(/\sdisabled=""/g) ?? []).length, 3);
});
void test('source turn, source phase and exact station token are bound across restored decisions', () => {
const source = confirmDiscoveryStorm(discoveryStormFixture());
for (const mutate of [
(g: Game) => {
g.stormMovementSource = createStormSource(g.turn - 1, 'dials', 2);
},
(g: Game) => {
g.phase = 1;
},
(g: Game) => {
g.ecologicalStorm!.token = g.discoveries!.tokens.find(
(t) => t.face !== 'ecological-testing-station',
)!.id;
g.ecologicalStorm!.signature = discoveryStormSignature(
g.ecologicalStorm!,
);
},
]) {
const g = structuredClone(source);
mutate(g);
const before = structuredClone(g);
assert.throws(() => viewGame(g, 'a'));
assert.throws(() => normalizeAutomaticGame(g));
assert.deepEqual(g, before);
}
});