Report database size and make four genuine suites reproducible
Operations now report the SQLite database size and page count, read through PRAGMA page_count/page_size in the same authority-checked batch as the lifecycle counters with non-numeric values clamped to zero; the page renders both and states that free disk space is still unknown. replaceMoritaniAssassinationTraitors draws the first distinct physical Traitor Card from the reserve instead of the reserve's first entry, because the face-up set-aside card can still sit in that list: re-drawing it produced a receipt whose replacement equalled the revealed card, which the assassination integrity check rejects mid-Mentat. With no distinct card left the step is blocked with an explicit reason instead of committing an invalid receipt. Four tests were made reproducible after measuring their real failure rates, because a suite that fails for random reasons cannot act as a checkpoint gate: the Discovery typed Spice Blow test now removes other cards for the same territory so one blow deposits the printed amount (0/20 after, ~20% before); the Discovery E2 skills deal is pinned with the new shared tests/deterministic-random.ts helper so a Lasgun-shield explosion cannot pre-empt its skill consumer (0/12 after, ~20% before); the Ecaz Occupy Ix/CHOAM policy check accepts any legal continuation and still asserts the phase it reaches (0/30 after, ~10% before); and the mixed-E3 Moritani Cunning test keeps the random storm off red_chasm's single sector (0/25 after, ~20% before). Offline suite 7,801/7,801 with typecheck and lint clean. No game, seat, saved state or deployment changes.
This commit is contained in:
1 parent
8bea4c1a0c
commit
e493bcf58e
12 files changed
+128
-13
No files matched your search
@@ -28,12 +28,16 @@ void test('owner operations sample counts saved states and backup provenance wit
|
||||
store.sqlite.prepare('INSERT INTO admin_room_backup_downloads(download_id,operation_id,actor_admin_id,room_code,created_at) VALUES(?,?,?,?,?)').run(randomUUID(), backupId, owner.id, 'OPERAABC', now);
|
||||
const before = store.sqlite.prepare('SELECT code,state,version FROM rooms ORDER BY code').all();
|
||||
const result = await readAdminOperations(store.database, identity, now);
|
||||
assert.deepEqual(result, {
|
||||
const { databasePages, databaseBytes, ...counters } = result;
|
||||
assert.deepEqual(counters, {
|
||||
observedAt: now, rooms: 2, removedRooms: 1, archivedRooms: 1,
|
||||
pausedRooms: 1, closedRooms: 1, activeSeats: 1, unreadableRooms: 1,
|
||||
lastRoomChange: 9200, backupSnapshots: 1, backupBytes: 14, backupDownloads: 1,
|
||||
stalledRooms: 0, oldestStalledChange: null,
|
||||
});
|
||||
const pageSize = store.sqlite.prepare('PRAGMA page_size').get() as { page_size: number };
|
||||
assert.ok(databasePages > 0);
|
||||
assert.equal(databaseBytes, databasePages * pageSize.page_size);
|
||||
assert.equal(JSON.stringify(result).includes('secret'), false);
|
||||
assert.deepEqual(store.sqlite.prepare('SELECT code,state,version FROM rooms ORDER BY code').all(), before);
|
||||
assert.deepEqual(await readAdminIntegrity(store.database, identity, now),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TestContext } from 'node:test';
|
||||
|
||||
/** A deterministic byte source for `crypto.getRandomValues`.
|
||||
*
|
||||
* Tests that build genuine games otherwise inherit the process's real entropy:
|
||||
* a random storm can place a fixture's chosen sector in the storm, and a random
|
||||
* deal can seal a Lasgun-shield explosion or empty the Traitor Deck reserve.
|
||||
* Pinning the source makes those tests reproducible without weakening what they
|
||||
* assert. `t.mock` restores the original at the end of the test. */
|
||||
export function pinDeterministicRandom(t: TestContext, seed = 0x2f6e2b1): void {
|
||||
let state = seed;
|
||||
t.mock.method(crypto, 'getRandomValues', <T extends ArrayBufferView | null>(array: T): T => {
|
||||
if (array) {
|
||||
const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
state = (state * 1103515245 + 12345) & 0x7fffffff;
|
||||
bytes[i] = (state >>> 16) & 0xff;
|
||||
}
|
||||
}
|
||||
return array;
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { pinDeterministicRandom } from './deterministic-random';
|
||||
import { applyAction, handLimit, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
@@ -296,7 +297,11 @@ for (const advanced of [false, true]) for (const marker of [0, 3, 5] as const) {
|
||||
});
|
||||
}
|
||||
|
||||
void test('four minimal policies resolve actual nested marker plans into physical revelation and a completed casualty/skill consumer', () => {
|
||||
void test('four minimal policies resolve actual nested marker plans into physical revelation and a completed casualty/skill consumer', (t) => {
|
||||
// The deal is pinned: an unseeded Lasgun-shield pair explodes, destroys both
|
||||
// armies and legitimately leaves no winner, which would pre-empt the casualty
|
||||
// and skill consumer this test exists to exercise.
|
||||
pinDeterministicRandom(t);
|
||||
const f = createDiscoveryNativeE2SkillsFixture({ advanced: true, tech: true, strongholds: true });
|
||||
const battle = prepareDiscoveryNativeE2SkillsMarkerBattle(f, { marker: 5, reserveCap: 3 });
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
@@ -319,7 +324,8 @@ void test('four minimal policies resolve actual nested marker plans into physica
|
||||
assert.equal(player(game, f.richese).forces[battle.key], 3);
|
||||
assert.equal(player(game, f.richese).reserves, 0);
|
||||
const done = finishDiscoveryNativeE2SkillsBattle(game);
|
||||
assert.ok(done.lastBattleContext?.winner === f.richese || done.lastBattleContext?.winner === f.guild);
|
||||
assert.ok(done.lastBattleContext?.winner === f.richese || done.lastBattleContext?.winner === f.guild,
|
||||
`The non-exploding marker battle must resolve to a real winner, not ${done.lastBattleContext?.result}.`);
|
||||
assert.equal(done.battle, null);
|
||||
assert.equal(player(done, f.richese).noField!.deployed, null);
|
||||
assert.equal(player(done, f.richese).noField!.lastShipped, battle.token);
|
||||
|
||||
@@ -275,7 +275,11 @@ void test('original E1 Discovery Spice Blow producer destroys actual ordinary/st
|
||||
const printed = DISCOVERY_SPICE_CARDS.find(card => card.sector !== game.storm)!;
|
||||
const index = game.spiceDeck.findIndex(card => 'territory' in card && card.discovery === printed.discovery);
|
||||
assert.ok(index >= 0);
|
||||
game.spiceDeck.unshift(game.spiceDeck.splice(index, 1)[0]);
|
||||
const [chosen] = game.spiceDeck.splice(index, 1);
|
||||
game.spiceDeck.unshift(chosen);
|
||||
// One blow only: another card for the same territory would stack a second
|
||||
// deposit and make the printed amount depend on the random storm.
|
||||
game.spiceDeck = game.spiceDeck.filter(card => card === chosen || !('territory' in card && card.territory === printed.territory));
|
||||
const key = `${printed.territory}:${printed.sector}`;
|
||||
nativeTypedPlace(game, 'i', key, 1, 1);
|
||||
nativeTypedPlace(game, 'f', key, 1, advanced ? 1 : 0);
|
||||
|
||||
@@ -146,7 +146,11 @@ void test('All four native minimal policies can continue after Ecaz-led Ix subst
|
||||
const player = view.players.find(p => p.id === actor);
|
||||
assert.ok(player);
|
||||
player.bot = difficulty;
|
||||
const action = botActions(view).find(candidate => candidate.type === 'ready');
|
||||
// "Continue" is the contract: a random deal can leave the actor already in
|
||||
// Collection with only an optional card play, so accept any legal policy
|
||||
// action and keep asserting the phase the continuation reaches.
|
||||
const choices = botActions(view);
|
||||
const action = choices.find(candidate => candidate.type === 'ready') ?? choices[0];
|
||||
assert.ok(action, `${difficulty} must be able to continue the actual post-Ix phase.`);
|
||||
let next = applyAction(game, actor, action);
|
||||
while (next.phaseOpening) {
|
||||
|
||||
@@ -240,6 +240,9 @@ void test('supply-only native Cunning and the real Terror arrival retain the phy
|
||||
assert.ok(f.firstMentat && f.alliance);
|
||||
// The native Cunning is offered only inside its own quiet window.
|
||||
let g = advance(reload(f.game), s => viewGame(s, f.moritani).nexusMoritani?.blocked === null, f.actions);
|
||||
// red_chasm has one sector; a random storm can cover it and reject the
|
||||
// committed shipment this test needs, so keep the storm off the destination.
|
||||
g.storm = 0;
|
||||
const request = cunning(g, f.moritani, 'robbery', 'red_chasm');
|
||||
rejects(g, f.native, request);
|
||||
rejects(g, f.moritani, { ...request, nexus: 'stale-cunning' });
|
||||
|
||||
Reference in new issue
Block a user