import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createTurnExchange } from '../src/shared/peer-protocol.js'; import type { PeerMessage } from '../src/shared/p2p-types.js'; import type { ActionId, Pair } from '../src/shared/types.js'; function pair() { const toA: PeerMessage[] = [], toB: PeerMessage[] = [], resolvedA: Pair[] = [], resolvedB: Pair[] = []; const base = { matchId: 'match', epoch: 0, turnId: 1, energy: [0, 0] as Pair }; const a = createTurnExchange({ ...base, slot: 0, send: m => toB.push(m), onResolved: r => resolvedA.push(r) }); const b = createTurnExchange({ ...base, slot: 1, send: m => toA.push(m), onResolved: r => resolvedB.push(r) }); const pump = async () => { for (let i = 0; i < 20 && (toA.length || toB.length); i++) { while (toA.length) await a.receive(toA.shift()!); while (toB.length) await b.receive(toB.shift()!); } }; return { a, b, toA, toB, resolvedA, resolvedB, pump }; } test('choices stay secret until both commitments and resolve identically exactly once', async () => { const h = pair(); await h.a.commit('charge'); assert.equal(h.toB.length, 1); assert.equal(h.toB[0].type, 'turn:commit'); assert.equal('action' in h.toB[0], false); await h.b.receive(h.toB.shift()!); assert.equal(h.toA.length, 0); await h.b.commit('guard'); const commit = h.toA[0]; await h.pump(); assert.deepEqual(h.resolvedA, [['charge', 'guard']]); assert.deepEqual(h.resolvedB, h.resolvedA); await h.a.receive(commit); assert.equal(h.resolvedA.length, 1); }); test('missing input is committed as null and is distinct from missing network packets', async () => { const h = pair(); await h.a.commit(null); await h.b.commit('charge'); await h.pump(); assert.deepEqual(h.resolvedA, [[null, 'charge']]); const silent = pair(); await silent.a.commit(null); assert.equal(silent.resolvedA.length, 0); }); test('tampered reveal and changed commitment are rejected; stale epochs never resolve', async () => { const h = pair(); await h.a.commit('charge'); await h.b.commit('guard'); const original = h.toA[0]; await h.a.receive({ ...original, connectionEpoch: 99 }); assert.equal(h.toB.length, 1); await h.a.receive(h.toA.shift()!); await h.b.receive(h.toB.shift()!); const reveal = h.toA.find(m => m.type === 'turn:reveal')!; await assert.rejects(h.a.receive({ ...reveal, action: 'charge' } as PeerMessage), /COMMITMENT_MISMATCH/); await assert.rejects(h.a.receive({ ...original, digest: '0'.repeat(64) } as PeerMessage), /COMMITMENT_CHANGED/); assert.equal(h.resolvedA.length, 0); });