Make native fixtures deterministic and expose verifier failures safely

This commit is contained in:
Quintine committed 2026-10-05 02:07:51 +11:00
1 parent 2d24f43721
commit 8b6bf38560
4 files changed
+79 -14

No files matched your search

+22 -3
View File
@@ -1,6 +1,6 @@
# Current development status
Updated 4 October 2026. **The full twelve-faction goal is unfinished. Basic
Updated 5 October 2026. **The full twelve-faction goal is unfinished. Basic
is playable and the six classic factions have a user-authorized Advanced preview.
Expansion starts and full rules/publication acceptance remain gated.**
@@ -16,6 +16,25 @@ open mode gates.
## Current checkpoint and work
**Paired checkpoint publication blocked:** code
**2d24f4372196515b4b4b07555e5af518a209e18b** is pushed.
[CI37208188809](https://github.com/Quintine/Dune/actions/runs/37208188809) /
container111453717886 failed isolated storage/HTTP step7; publication step9
was correctly skipped. Public job/annotations show only exit1; detailed logs
require unavailable authenticated access. Investigation uses the original
proxy-free `deploy/serve.mjs` with Node22 and a new private QA store, never an
existing game database or unsafe POST retry. Original owner/operator login,
foreign-origin rejection,25 administrator HTTP checks and the exact55/55 HTTP
integration cases pass there. Original room/seat/admin witnesses survive
process replacement; HTTPS external-origin login/Secure cookies, creation,
join/discussion and four foreign-mutation rejections also pass. This is
proxy-free Node22 evidence, not a passing Docker/CI claim. The failed job's
specific boundary is still unknown. `tools/verify-container.mjs` now reports
redacted failing stages and up to three failing integration names as public
CI annotations while retaining nonzero failure and the publication gate.
Local fail-closed diagnostic smoke emits the missing-Docker stage and exit1.
No successful publication or deployed claim is made for this checkpoint.
**Paired E1/E2 Nexus / Skills / Tech / Strongholds — bounded Development Verified:**
both Ixians+Tleilaxu OR CHOAM+Richese plus classics, one original family47/
35+cache deck/all14/all12, selected unused Tech3..6 Basic/Advanced and/or
@@ -27,8 +46,8 @@ rejects and four legal policies pass. Thirty-four genuine2..6 games complete
16,094 accepted actions/no rejection,420 JSON continuations and22 actual
closing choices on unchanged tree
`472b5705ca5376fb95f8e6ff6f40192d9f1745d7d08c41efdef2b3e53783139a`.
An older native marker fixture's unseeded scalar actions are now controlled;
all nine native cases and the frozen175-case union pass afterward.
The older native fixture now controls the original initializer remainder and
later scalar actions; final59 module/native cases, types/lint/build pass.
Original backed-up CLI **PPL5CNEB / TZRU5GYP v10/setup** retains actual
offers/hands/IDs after a scoped original14 shuffle. Human390px **PPL5CNEB
v28/Collection** copies held Arrakeen into HMS, uses full Suboids and trained
+3 -3
View File
@@ -53,9 +53,9 @@ games complete16,094 accepted actions/no rejection,420 JSON continuations and
22 actual closing choices. Natural Cunning combinations are not claimed.
Reports under `/tmp/dune-paired-nexus-skills-{tech,stronghold,both}-games-20261005`
capture unchanged tree `472b5705ca5376fb95f8e6ff6f40192d9f1745d7d08c41efdef2b3e53783139a`.
An older native marker fixture's scalar action entropy is controlled after an
intermittent missing-marker assertion; its nine cases and final175 union pass.
No production marker behavior or unresolved ruling is changed for that fixture.
An older native fixture's initializer remainder and scalar actions are now
controlled after intermittent marker/held-owner assertions; final59 module/
native cases, types/lint/build pass. No production behavior or ruling changes.
Original backed-up CLI **PPL5CNEB / TZRU5GYP v10/setup** preserves first
offers/hands/IDs after only the original14 pre-deal shuffle. Controlled390px
+9 -5
View File
@@ -100,8 +100,8 @@ export function nextNativeSkillsStrongholdStep(game: Game): StrongholdFactionsNa
return game.players.some(p => p.faction === 'richese')
? nextRicheseStrongholdsNativeStep(game) : nextStrongholdFactionsNativeStep(game);
}
function act(game: Game, next: StrongholdFactionsNativeStep, actions?: StrongholdFactionsNativeStep[]): Game {
actions?.push(structuredClone(next));
/** Scope the original initializer remainder and later native shuffles alike. */
function nativeScalarEntropy<T>(run: () => T): T {
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
const native = crypto.getRandomValues.bind(crypto);
crypto.getRandomValues = <V extends ArrayBufferView | null>(array: V): V => {
@@ -109,12 +109,16 @@ function act(game: Game, next: StrongholdFactionsNativeStep, actions?: Stronghol
else Reflect.apply(native, crypto, [array]);
return array;
};
try { return applyAction(game, next.actor, next.action); }
try { return run(); }
finally {
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
else Reflect.deleteProperty(crypto, 'getRandomValues');
}
}
function act(game: Game, next: StrongholdFactionsNativeStep, actions?: StrongholdFactionsNativeStep[]): Game {
actions?.push(structuredClone(next));
return nativeScalarEntropy(() => applyAction(game, next.actor, next.action));
}
export function advanceNativeSkillsStronghold(state: Game, until: (g: Game) => boolean,
actions?: StrongholdFactionsNativeStep[], firstDial = 0): Game {
let game = state;
@@ -209,9 +213,9 @@ export function createNativeSkillsStrongholdFixture(options: NativeSkillsStrongh
if (options.optionalTech && !game.techTokens) game = applyAction(game, game.host, { type: 'techTokens', enabled: true });
if (game.techTokens) assert.ok(game.players.length >= 3);
const initial = structuredClone(game), actions: StrongholdFactionsNativeStep[] = [],
staging: string[] = ['Scoped original scalar action entropy keeps native source programs reproducible; UUID byte arrays remain native.'];
staging: string[] = ['Scoped original initializer and scalar action entropy keeps native source programs reproducible; UUID byte arrays remain native.'];
const native: AdvancedNativeSkillsOptions = { family, skillOwner: ownerFaction, requestedSkill: 'suk-graduate', initial: game };
game = initializeAdvancedNativeSkillsSetup(native);
game = nativeScalarEntropy(() => initializeAdvancedNativeSkillsSetup(native));
const offered = structuredClone(game);
for (let i = 0; game.status === 'setup' && i < 200; i++) {
if (game.setupStage === 'leaderSkills') {
+45 -3
View File
@@ -1,6 +1,6 @@
// Runs only against a disposable container and volume created by this command.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { execFileSync, spawnSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
@@ -15,6 +15,13 @@ const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8' }).t
let base;
const adminFiles = mkdtempSync(joinPath(tmpdir(), 'dune-admin-container-'));
let adminCookie;
let stage = 'isolated volume and cold start';
function annotateFailure(message) {
if (process.env.GITHUB_ACTIONS !== 'true') return;
const safe = String(message).replaceAll(/dune-admin\.[0-9a-f-]{36}\.[0-9a-f]{64}/g, '[REDACTED ADMIN KEY]')
.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A');
console.error(`::error title=Container verification - ${stage}::${safe}`);
}
async function ready() {
for (let attempt = 0; attempt < 90; attempt++) {
try {
@@ -42,12 +49,14 @@ try {
docker('volume', 'create', volume);
start();
await ready();
stage = 'first owner bootstrap and login';
const bootstrapKey = checkBootstrap(1);
const ownerLogin = await fetch(`${base}/api/admin/session`, {
method: 'POST', headers: { 'content-type': 'application/json', origin: base },
body: JSON.stringify({ action: 'login', key: bootstrapKey }),
});
assert.equal(ownerLogin.status, 200, 'Bootstrapped owner must be able to sign in');
stage = 'operator provisioning and login';
execFileSync('node', ['tools/admin-access.mjs', '--name', 'Container QA', '--role', 'operator', '--out', joinPath(adminFiles, 'key')]);
execFileSync('docker', ['exec', '-i', name, 'node', '-e', "require('node:fs').writeFileSync('/tmp/dune-admin-provision.sql', require('node:fs').readFileSync(0), {mode:0o600})"], {
input: readFileSync(joinPath(adminFiles, 'key/provision.sql')), // Container USER owns this private file.
@@ -61,6 +70,7 @@ try {
assert.equal(adminLogin.status, 200);
adminCookie = adminLogin.headers.get('set-cookie')?.split(';')[0];
assert.ok(adminCookie);
stage = 'mapped origin and room creation';
const headers = { 'content-type': 'application/json', origin: base };
const body = JSON.stringify({ name: 'Container verification', faction: 'atreides', advanced: false, expansions: [] });
const create = await fetch(`${base}/api/rooms`, { method: 'POST', headers, body });
@@ -81,23 +91,44 @@ try {
assert.equal(admin.status, 200, 'Administrator session must survive replacement');
}
}
stage = 'container restart and saved witnesses';
docker('restart', '--time', '60', name);
// Docker may assign a different ephemeral host port after a restart.
base = `http://${docker('port', name, '3000/tcp')}`;
await ready();
assert.equal(checkBootstrap(1), bootstrapKey, 'Restart must not replace the owner key');
await restored();
stage = 'container replacement and bootstrap custody';
docker('stop', '--time', '60', name);
docker('rm', name);
start();
await ready();
await restored();
checkBootstrap(0);
stage = 'administrator HTTP acceptance';
execFileSync('node', ['tools/verify-admin.mjs', '--url', base, '--key-file', joinPath(adminFiles, 'key/access-key.txt'), '--qa-account', adminKey.split('.')[1], '--out', joinPath(adminFiles, 'http')], { stdio: 'inherit' });
adminCookie = undefined; // The acceptance test intentionally signs out all sessions.
execFileSync('npm', ['run', 'test:integration'], {
stdio: 'inherit', env: { ...process.env, DUNE_TEST_URL: base },
stage = 'HTTP integration';
const integration = spawnSync('npm', ['run', 'test:integration'], {
encoding: 'utf8', maxBuffer: 16 * 1024 * 1024,
env: { ...process.env, DUNE_TEST_URL: base },
});
if (integration.stdout) process.stdout.write(integration.stdout);
if (integration.stderr) process.stderr.write(integration.stderr);
if (integration.error) throw integration.error;
if (integration.status !== 0) {
const details = `${integration.stdout ?? ''}\n${integration.stderr ?? ''}`;
const failures = details.match(/^\s*\u2716 [^\r\n]+/gm) ?? [];
const reported = new Set();
for (const failure of failures) {
const name = failure.trim();
if (name.includes('failing tests:') || reported.has(name)) continue;
reported.add(name); annotateFailure(name);
if (reported.size === 3) break;
}
throw new Error(`HTTP integration exited with ${integration.signal ?? integration.status}`);
}
stage = 'HTTPS proxy replacement and original witnesses';
docker('stop', '--time', '60', name);
docker('rm', name);
const publicOrigin = 'https://dune.example.test';
@@ -105,12 +136,14 @@ try {
await ready();
await restored();
checkBootstrap(0);
stage = 'HTTPS external-origin login and cookies';
const proxyAdmin = await fetch(`${base}/api/admin/session`, {
method: 'POST', headers: { 'content-type': 'application/json', origin: publicOrigin },
body: JSON.stringify({ action: 'login', key: adminKey }),
});
assert.equal(proxyAdmin.status, 200);
assert.ok(proxyAdmin.headers.get('set-cookie')?.includes('; Secure'));
stage = 'HTTPS external-origin room, join and discussion';
const proxyCreate = await fetch(`${base}/api/rooms`, {
method: 'POST', headers: { ...headers, origin: publicOrigin }, body,
});
@@ -129,6 +162,7 @@ try {
body: JSON.stringify({ id: randomUUID(), recipientId: null, text: 'Proxy verification' }),
});
assert.equal(message.status, 200, 'Table discussion must work through HTTPS proxy');
stage = 'foreign-origin mutation rejection';
for (const path of ['/api/rooms', `/api/rooms/${proxyRoom.code}`, `/api/rooms/${proxyRoom.code}/control`, `/api/rooms/${proxyRoom.code}/messages`]) {
const rejected = await fetch(base + path, {
method: 'POST', headers: { ...headers, origin: 'https://foreign.invalid' }, body,
@@ -137,6 +171,14 @@ try {
}
console.log('Container origin, restart, replacement, saved seat and HTTP integration checks passed.');
} catch (error) {
annotateFailure(String(error?.message ?? error).split('\n')[0]);
if (stage === 'administrator HTTP acceptance') {
try {
const report = JSON.parse(readFileSync(joinPath(adminFiles, 'http/report.json'), 'utf8'));
const request = report.failureRequest ?? report.lastRequest;
if (request) annotateFailure(`${request.path} returned ${request.status}; ${report.failureLocation ?? 'see the failed acceptance check'}`);
} catch { /* Early failures may not create the private report; the original error remains fatal. */ }
}
try { console.error(docker('logs', name).replaceAll(/dune-admin\.[0-9a-f-]{36}\.[0-9a-f]{64}/g, '[REDACTED ADMIN KEY]')); } catch { /* May not exist yet. */ }
throw error;
} finally {