diablo2-web/tests/net.test.ts

1669 lines
96 KiB
TypeScript

import { describe, test, expect as vitestExpect } from 'vitest'
import * as fs from 'fs'
/**
* Network checks: wire protocol, transports, and two peers playing over a socket.
*
* The claim being tested is not "messages encode" — it is that two independently
* simulated worlds, driven only by messages that crossed a real byte pipe, stay
* bit-identical; that a peer which goes quiet *stalls* the world instead of
* corrupting it; and that a peer whose input is altered in flight is *detected*.
*
* The last section runs the same session code over a real TCP socket and real
* WebSocket framing (through the relay in `scripts/net-relay.ts`), because an
* in-memory pipe cannot prove that the browser transport works: it never frames,
* never splits a message across TCP segments, and never delivers asynchronously.
*
* Usage: node scripts/verify-net.ts
*/
import { addPlayer, createWorld, damageMonster, spawnMonsters, tickCombat, tickCombatMulti } from '../src/game/combat.ts'
import type { CombatOptions, CombatWorld, MonsterStats } from '../src/game/combat.ts'
import { Rng } from '../src/game/rng.ts'
import { LockstepSession } from '../src/net/lockstep.ts'
import type { InputFrame, LockstepSimulation } from '../src/net/lockstep.ts'
import { NetplaySession } from '../src/net/netplay.ts'
import { decodeMessage, encodeMessage, NO_ACK, ProtocolError } from '../src/net/protocol.ts'
import type { NetMessage } from '../src/net/protocol.ts'
import { MemoryHub, memoryTransportPair, socketTransport, MemoryTransport } from '../src/net/transport.ts'
import type { Transport } from '../src/net/transport.ts'
import { parseTable } from '../src/game/tables.ts'
import { monsterStatsFromTable } from '../src/game/combat.ts'
import { startRelay } from '../scripts/net-relay.ts'
const isSkip = false && !fs.existsSync('samples/d2');
const _results: any[] = [];
let suiteCompleted = false;
let problems: string[] = [];
let checks: any[] = [];
function expect(condition: boolean, description: string) { _results.push({cond: condition, desc: description}); if (!condition) problems.push(description); }
function check(nameOrOk: any, okOrMessage: any, detail?: string) {
if (typeof nameOrOk === 'string') {
_results.push({cond: okOrMessage, desc: nameOrOk, detail}); if (!okOrMessage) checks.push({name: nameOrOk, ok: okOrMessage});
} else {
_results.push({cond: nameOrOk, desc: okOrMessage}); if (!nameOrOk) checks.push({name: okOrMessage, ok: nameOrOk});
}
}
if (!isSkip) {
const problems: string[] = []
let checks = 0
/**
* Assert one condition.
*
* @param condition - the condition to hold.
* @param description - what it means.
*/
function __expect_disabled(condition: boolean, description: string): void {
checks += 1
if (!condition) problems.push(description)
}
// --- the wire codec ----------------------------------------------------------
const messages: NetMessage[] = [
{ kind: 'hello', peer: 1, peers: 2, seed: 0xdeadbeef, ackTo: NO_ACK },
{ kind: 'hello', peer: 0, peers: 2, seed: 0xdeadbeef, ackTo: 1 },
{ kind: 'input', peer: 0, frame: { tick: 1234, movement: { x: -1, y: 0.5 }, attack: true, pickup: false, talk: true, skill: 3 } },
{ kind: 'hash', peer: 1, tick: 987654, hash: 0x89abcdef },
{ kind: 'bye', peer: 0 },
]
for (const message of messages) {
const round = decodeMessage(encodeMessage(message))
expect(round.kind === message.kind, 'message survives a round trip')
if (round.kind === 'input' && message.kind === 'input') {
expect(round.frame.tick === message.frame.tick, 'the tick round-trips')
expect(Math.abs(round.frame.movement.x - message.frame.movement.x) < 1e-3, 'the x movement round-trips as fixed point')
expect(Math.abs(round.frame.movement.y - message.frame.movement.y) < 1e-3, 'the y movement round-trips as fixed point')
expect(round.frame.attack && !round.frame.pickup && round.frame.talk, 'the control flags round-trip')
expect(round.frame.skill === 3, 'the skill slot round-trips')
}
if (round.kind === 'hello' && message.kind === 'hello') expect(round.seed === 0xdeadbeef, 'the seed round-trips')
if (round.kind === 'hash' && message.kind === 'hash') {
expect(round.tick === 987654 && round.hash === 0x89abcdef, 'the hash and tick round-trip')
}
}
// The layout is pinned, not just self-consistent: a peer running an older build
// must fail loudly rather than silently misread a field.
const pinned = encodeMessage({ kind: 'input', peer: 1, frame: { tick: 1, movement: { x: -1, y: 0 }, attack: true, pickup: false, talk: false, skill: 0 } })
expect(pinned.byteLength === 12, 'an input message is twelve bytes')
expect(pinned[0] === 2 && pinned[1] === 1, 'an input message starts with its type and peer')
expect(pinned[6] === 0x18 && pinned[7] === 0xfc, 'a movement of -1 is fixed point -1000, little-endian')
expect(pinned[10] === 1, 'only the attack flag is set')
expect(encodeMessage({ kind: 'hash', peer: 0, tick: 0, hash: 0 }).byteLength === 10, 'a hash message is ten bytes')
expect(encodeMessage({ kind: 'hello', peer: 0, peers: 1, seed: 0, ackTo: NO_ACK }).byteLength === 8, 'a hello message is eight bytes')
const acked = decodeMessage(encodeMessage({ kind: 'hello', peer: 0, peers: 4, seed: 7, ackTo: 3 }))
expect(acked.kind === 'hello' && acked.ackTo === 3, 'an acknowledgement names the peer it acknowledges')
const unacked = decodeMessage(encodeMessage({ kind: 'hello', peer: 0, peers: 4, seed: 7, ackTo: NO_ACK }))
expect(unacked.kind === 'hello' && unacked.ackTo === NO_ACK, 'a plain introduction is not mistaken for an acknowledgement')
const bad: [string, Uint8Array][] = [
['an empty message', new Uint8Array(0)],
['an unknown type', new Uint8Array([99, 0])],
['a truncated input', new Uint8Array([2, 0, 0, 0])],
['an over-long input', new Uint8Array(16)],
['a hello claiming zero peers', new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0])],
['a hello claiming nine peers', new Uint8Array([1, 0, 9, 0, 0, 0, 0, 0])],
['a truncated hash', new Uint8Array([3, 0, 0])],
]
for (const [label, bytes] of bad) {
let rejected = false
try {
decodeMessage(bytes)
} catch (error) {
rejected = error instanceof ProtocolError
}
expect(rejected, 'input is rejected before it reaches the simulation')
}
const movementOverflow = decodeMessage(encodeMessage({
kind: 'input', peer: 0, frame: { tick: 0, movement: { x: 1e9, y: -1e9 }, attack: false, pickup: false, talk: false, skill: 255 },
}))
expect(movementOverflow.kind === 'input' && movementOverflow.frame.movement.x <= 32767, 'an absurd movement is clamped, not wrapped')
// --- transports --------------------------------------------------------------
const [endA, endB] = memoryTransportPair()
let receivedAtB = 0
endB.onMessage(() => { receivedAtB += 1 })
expect(endA.open && endB.open, 'a fresh memory pair is open at both ends')
endA.send(new Uint8Array([1, 2, 3]))
expect(receivedAtB === 0, 'a memory message is queued, not delivered on send')
endA.flush()
expect(receivedAtB === 1, 'flushing delivers the queued message')
const oversizeRejected = ((): boolean => {
try {
endA.send(new Uint8Array(5000))
return false
} catch { return true }
})()
expect(oversizeRejected, 'an oversized message is refused at the transport')
let closed = 0
endA.onClose(() => { closed += 1 })
endB.close()
expect(closed === 1 && !endA.open && !endB.open, 'closing one end closes the pair')
endA.send(new Uint8Array([9]))
endA.flush()
expect(endA.dropped === 1, 'sending on a closed pair drops instead of throwing')
// --- two peers, one world, over a byte pipe ----------------------------------
const stats: MonsterStats[] = monsterStatsFromTable(parseTable([
'Id\tName\tHP\tDamage\tCooldownTicks\tReach\tAggroRadius\tSpeed\tXP',
// Fast enough to catch the player and weak enough that neither peer's player
// dies before the kills can be compared: the point of this table is contact.
'fallen\tFallen\t12\t1\t24\t36\t400\t220\t8',
'zombie\tZombie\t30\t1\t32\t40\t300\t210\t15',
].join('\n')))
const options: CombatOptions = {
playerSpeed: 180, playerReach: 48, playerCooldownTicks: 12, playerDamage: 6,
playerManaPerAttack: 2, respawnTicks: 40,
}
const xpTable: readonly number[] = [0, 0, 1000]
/** A world plus its random stream: everything a peer simulates. */
interface Game { world: ReturnType<typeof createWorld>; rng: Rng }
/**
* Build a fresh game, identical on both peers.
*
* @param seed - the world seed.
* @returns the game.
*/
function newGame(seed: number): Game {
const world = createWorld(0, 0)
// The monsters start close enough to reach the player inside the first few
// seconds: a network test where nothing ever fights would agree on nothing.
spawnMonsters(world, stats, 5, { x: 30, y: 0 }, 40, { overlap: () => 0 })
return { world, rng: new Rng(seed) }
}
/**
* The input script: a pure function of the tick and the peer, so there is nothing
* here that two peers could disagree about.
*
* @param peer - the peer index.
* @param tick - the tick.
* @returns movement and intent.
*/
function scriptedInput(peer: number, tick: number): { movement: { x: number; y: number }; attack: boolean; pickup: boolean; talk: boolean; skill: number } {
const phase = Math.floor((tick + peer * 7) / 25) % 4
const movement = phase === 0 ? { x: 1, y: 0 } : phase === 1 ? { x: 0, y: 1 } : phase === 2 ? { x: -1, y: 0 } : { x: 0, y: -1 }
return { movement, attack: (tick + peer) % 5 === 0, pickup: tick % 41 === 0, talk: false, skill: (tick + peer) % 4 }
}
/**
* Wrap a game as a lockstep simulation. Every peer applies *every* peer's input,
* and the digest covers the whole world, so a field missed by one peer's
* simulation shows up as a desync.
*
* @param game - the game to drive.
* @returns the simulation.
*/
function asSimulation(game: Game): LockstepSimulation {
return {
advance: (inputs) => {
for (const input of inputs) {
// Only movement and attack reach the combat tick; pickup and talk belong
// to the scene, so they are deliberately not part of the digest here.
tickCombat(game.world, { movement: input.movement, attack: input.attack }, options, { overlap: () => 0 }, xpTable)
if (input.attack && game.world.player.cooldown === 0) {
const swing = game.rng.next() // the drop stream is part of the world
game.world.monsters.forEach((monster, index) => {
if (monster.state === 'dead') return
if (Math.hypot(monster.x - game.world.player.x, monster.y - game.world.player.y) > 48) return
damageMonster(game.world, index, options.playerDamage + (swing > 0.5 ? 1 : 0))
})
}
}
},
hash: () => LockstepSession.digest(digest(game)),
}
}
/**
* Digest everything, so a divergence cannot hide in a field the hash forgets.
*
* @param game - the game.
* @returns a digest string.
*/
function digest(game: Game): string {
const round = (value: number): number => Math.round(value * 1000)
return JSON.stringify({
tick: game.world.tick,
kills: game.world.kills,
rng: game.rng.seed,
events: game.world.events.map(event => [event.kind, event.subjectId ?? '', round(event.x), round(event.y)]),
player: {
x: round(game.world.player.x), y: round(game.world.player.y),
hp: game.world.player.hp, mana: game.world.player.mana,
cooldown: game.world.player.cooldown, facing: game.world.player.facing,
alive: game.world.player.alive,
},
monsters: game.world.monsters.map(m => [round(m.x), round(m.y), m.hp, m.state, m.cooldown, m.hitFlash]),
})
}
/**
* A peer: its session and the game it drives.
*/
interface Peer { session: NetplaySession; game: Game }
/**
* Build one networked peer.
*
* @param index - the peer index.
* @param transport - its byte pipe.
* @param seed - the world seed.
* @returns the peer.
*/
function makePeer(index: number, transport: Transport, seed: number): Peer {
const game = newGame(seed)
const session = new NetplaySession(
{ peer: index, peers: 2, seed, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 40 },
asSimulation(game),
transport,
)
const peer: Peer = { session, game }
session.start()
return peer
}
// --- the quiet-peer case: a stall must not corrupt the world ------------------
const [pipeA, pipeB] = memoryTransportPair()
const peerA = makePeer(0, pipeA, 0x51ed)
const peerB = makePeer(1, pipeB, 0x51ed)
// The handshake is owed by `start` and delivered by the first pump: a session is
// normally created while its socket is still connecting, so sending on `start`
// would write into a socket that is not open yet.
expect(peerA.session.stats.helloSent === 0 && !peerA.session.stats.handshaked, 'nothing is sent before the session is pumped')
peerA.session.pump()
peerB.session.pump()
pipeA.flush()
expect(peerA.session.remoteSeed === 0x51ed && peerB.session.remoteSeed === 0x51ed, 'the handshake exchanges the world seed')
expect(peerA.session.stats.handshaked && peerB.session.stats.handshaked, 'both peers complete the handshake')
// The acknowledgements are still in flight; delivering them must end the
// handshake for good rather than leaving either peer repeating itself.
pipeA.flush()
expect(peerA.session.stats.acknowledged && peerB.session.stats.acknowledged, 'both peers learn that their own hello was heard')
for (let tick = 0; tick < 40; tick += 1) {
peerA.session.pump()
peerB.session.pump()
pipeA.flush()
}
expect(
peerA.session.stats.helloSent === 2 && peerB.session.stats.helloSent === 2,
'a completed handshake stops: one hello and one acknowledgement each',
)
const TICKS = 200
let stepsA = 0
let stepsB = 0
for (let tick = 0; tick < TICKS; tick += 1) {
const inputA = scriptedInput(0, tick)
const inputB = scriptedInput(1, tick)
peerA.session.setIntent(inputA)
peerB.session.setIntent(inputB)
if (peerA.session.pump().kind === 'stepped') stepsA += 1
if (peerB.session.pump().kind === 'stepped') stepsB += 1
// B's link is held for a stretch: A must stall, B must run on, and neither may
// end up with a different world.
pipeB.hold = tick >= 60 && tick < 90
pipeA.flush()
}
// Drain the backlog the held link accumulated.
for (let round = 0; round < 60; round += 1) {
peerA.session.setIntent({ movement: { x: 0, y: 0 }, attack: false })
peerB.session.setIntent({ movement: { x: 0, y: 0 }, attack: false })
peerA.session.pump()
peerB.session.pump()
pipeA.flush()
}
expect(peerA.session.desyncReport === null && peerB.session.desyncReport === null, 'a peer whose link stalls does not cause a desync')
expect(peerA.session.stats.hashesCompared === peerA.session.stats.hashesAgreed, 'every hash peer 0 could check agreed')
expect(peerA.session.stats.waiting > 0, 'the peer that waited counted the stalled ticks')
expect(peerA.session.stats.hashesCompared > 0 && peerB.session.stats.hashesCompared > 0, 'both peers compared state hashes')
expect(
peerB.session.stats.hashesCompared === peerB.session.stats.hashesAgreed,
'every hash that arrived at peer 1 agreed',
)
expect(peerA.session.stats.malformed === 0 && peerB.session.stats.malformed === 0, 'no message was malformed')
expect(peerA.session.stats.timedOut === false, 'a link that stalls for a moment is not declared lost')
expect(peerA.session.stats.sent > 100 && peerA.session.stats.received > 100, 'traffic actually crossed the pipe')
expect(stepsA > 100 && stepsB > 100, 'both peers ran the game, one eventually catching up')
expect(stepsB > stepsA, 'the peer whose link was open ran ahead while the other was starved')
// A peer that is starved stops producing input, so its partner runs only as far
// as the inputs it already holds and then waits too — the world as a whole is held
// back, which is the point of lockstep. The two ends therefore resume with a small
// constant lead rather than snapping level.
const lead = peerB.session.lockstep.tick - peerA.session.lockstep.tick
expect(lead >= 0 && lead <= 6, 'the two ends resume within a few ticks of each other')
// The starved peer still holds the other's backlog, so it can be brought level
// without any further traffic from the far end: pumping peer 0 alone (and holding
// its own sends) replays exactly the ticks it was missing.
for (let extra = 0; extra < 12 && peerA.session.lockstep.tick < peerB.session.lockstep.tick; extra += 1) {
peerA.session.setIntent(scriptedInput(0, peerA.session.lockstep.tick))
peerA.session.pump()
}
expect(
peerA.session.lockstep.tick === peerB.session.lockstep.tick,
'the starved peer can be brought level from the backlog it already holds',
)
expect(
digest(peerA.game) === digest(peerB.game),
'at the same tick, the peer that was starved holds exactly the other peer\'s world',
)
expect(peerA.game.world.kills === peerB.game.world.kills, 'both peers agree on the kill count')
expect(peerA.game.rng.seed === peerB.game.rng.seed, 'both peers agree on the random stream position')
expect(peerA.game.world.monsters.every((m, i) => m.hp === peerB.game.world.monsters[i]?.hp), 'both peers agree on monster health')
expect(peerA.game.world.kills > 0, 'the test actually produced kills to disagree about')
// The starved peer buffered the hashes that arrived for ticks it had not run yet,
// so once it catches up it checks them all: being behind costs nothing.
expect(peerA.session.stats.hashesIgnored === 0, 'a peer that fell behind still checks every hash it received')
expect(peerA.session.stats.hashesCompared > 100, 'catching up results in a real number of compared hashes')
expect(peerB.session.stats.hashesAgreed === peerB.session.stats.hashesCompared, 'every hash peer 1 could check agreed')
// --- co-op: two peers, one shared world, one fighter each ---------------------
/**
* Build a co-op world: two players, monsters between them.
*
* Both peers call this with the same seed and get the same world, which is the
* whole premise of lockstep — nothing about the world is sent, only what the
* players pressed.
*
* @returns the world.
*/
function newCoopWorld(): CombatWorld {
const world = createWorld(-40, 0)
addPlayer(world, 40, 0)
spawnMonsters(world, coopStats, 6, { x: 0, y: 0 }, 90, { overlap: () => 0 })
return world
}
/**
* Digest a co-op world.
*
* The player list is digested in peer order; `world.player` is a view alias and is
* deliberately absent, because which player is "local" must not affect the state
* two peers agree on.
*
* @param world - the world.
* @returns a digest string.
*/
function coopDigest(world: CombatWorld): string {
const round = (value: number): number => Math.round(value * 1000)
return JSON.stringify({
tick: world.tick,
kills: world.kills,
players: world.players.map(player => [round(player.x), round(player.y), player.hp, player.xp, player.level, player.alive]),
monsters: world.monsters.map(m => [round(m.x), round(m.y), m.hp, m.state]),
})
}
/**
* A co-op simulation: each peer's input drives its own fighter.
*
* @param world - the shared world.
* @returns the simulation.
*/
function coopSimulation(world: CombatWorld): LockstepSimulation {
return {
advance: (inputs) => {
tickCombatMulti(world, inputs.map(input => ({ movement: input.movement, attack: input.attack })), options, { overlap: () => 0 }, xpTable)
},
hash: () => LockstepSession.digest(coopDigest(world)),
}
}
const coopStats: MonsterStats[] = monsterStatsFromTable(parseTable([
'Id\tName\tHP\tDamage\tCooldownTicks\tReach\tAggroRadius\tSpeed\tXP',
'fallen\tFallen\t14\t1\t24\t36\t400\t220\t8',
].join('\n')))
const [coopPipeA, coopPipeB] = memoryTransportPair()
const coopWorldA = newCoopWorld()
const coopWorldB = newCoopWorld()
const coopA = new NetplaySession(
{ peer: 0, peers: 2, seed: 0x9a9a, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 },
coopSimulation(coopWorldA), coopPipeA,
)
const coopB = new NetplaySession(
{ peer: 1, peers: 2, seed: 0x9a9a, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 },
coopSimulation(coopWorldB), coopPipeB,
)
coopA.start()
coopB.start()
coopPipeA.flush()
for (let tick = 0; tick < 300; tick += 1) {
coopA.setIntent(scriptedInput(0, tick))
coopB.setIntent(scriptedInput(1, tick))
coopA.pump()
coopB.pump()
coopPipeA.flush()
}
expect(coopA.desyncReport === null && coopB.desyncReport === null, 'two players in one world do not desync')
expect(coopA.stats.hashesCompared > 200 && coopA.stats.hashesCompared === coopA.stats.hashesAgreed, 'every co-op hash peer 0 could check agreed')
expect(coopB.stats.hashesCompared > 200 && coopB.stats.hashesCompared === coopB.stats.hashesAgreed, 'every co-op hash peer 1 could check agreed')
expect(coopA.lockstep.tick >= 295 && coopB.lockstep.tick === coopA.lockstep.tick, 'both peers ran the whole co-op run, tick for tick together')
expect(coopDigest(coopWorldA) === coopDigest(coopWorldB), 'the two peers hold the same two-player world')
expect(coopWorldA.players.length === 2, 'the world really has two players in it')
expect(coopWorldA.kills > 0, 'the two players killed something together')
expect(coopWorldA.players[0]!.xp !== coopWorldA.players[1]!.xp, 'experience is credited to the peer that landed the killing blow')
expect(coopWorldA.players[0]!.x !== coopWorldB.players[0]!.x || coopWorldA.players[0]!.y === coopWorldB.players[0]!.y, 'player 0 moved under its own input')
expect(coopDigest(coopWorldA) !== coopDigest(newCoopWorld()), 'the co-op world actually changed while it ran')
// Monsters go for whoever is nearest, not for player 0 by seniority.
const bait = createWorld(-300, 0)
addPlayer(bait, 0, 0)
bait.monsters.push({
index: 0, stats: { ...coopStats[0]!, aggroRadius: 400, reach: 40, speed: 0 }, x: 6, y: 0,
hp: 50, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
})
const idle = { movement: { x: 0, y: 0 }, attack: false }
for (let tick = 0; tick < 40; tick += 1) tickCombatMulti(bait, [idle, idle], options, { overlap: () => 0 }, xpTable)
expect(bait.players[1]!.hp < bait.players[1]!.maxHp, 'a monster attacks the nearest player')
expect(bait.players[0]!.hp === bait.players[0]!.maxHp, 'a monster 300 pixels away from the other player leaves it alone')
// A dead peer must not stall the monsters or hide the living one.
const bereaved = createWorld(-30, 0)
addPlayer(bereaved, 30, 0)
bereaved.players[1]!.hp = 0
bereaved.players[1]!.alive = false
bereaved.players[1]!.respawnIn = 30
// The monster stands on top of the dead peer and within reach of the living one,
// so a monster that still targeted corpses would attack the wrong player.
bereaved.monsters.push({
index: 0, stats: { ...coopStats[0]!, aggroRadius: 400, reach: 80, speed: 0 }, x: 30, y: 0,
hp: 50, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
})
for (let tick = 0; tick < 10; tick += 1) tickCombatMulti(bereaved, [idle, idle], options, { overlap: () => 0 }, xpTable)
expect(bereaved.players[0]!.hp < bereaved.players[0]!.maxHp, 'with one peer dead the monsters hunt the other')
expect(!bereaved.players[1]!.alive && bereaved.players[1]!.hp === 0, 'a dead peer stays dead while its respawn timer runs')
// A hash whose tick has already fallen out of history is a different case: it
// cannot be checked at all, and saying so is the honest answer.
const [latePipeA, latePipeB] = memoryTransportPair()
const lateA = makePeer(0, latePipeA, 0x6666)
const lateB = makePeer(1, latePipeB, 0x6666)
latePipeA.flush()
for (let tick = 0; tick < 120; tick += 1) {
lateA.session.setIntent(scriptedInput(0, tick))
lateB.session.setIntent(scriptedInput(1, tick))
lateA.session.pump()
lateB.session.pump()
latePipeA.flush()
}
latePipeB.send(encodeMessage({ kind: 'hash', peer: 1, tick: 0, hash: 0x1234 }))
latePipeA.flush()
for (let extra = 0; extra < 5; extra += 1) { lateA.session.pump(); latePipeA.flush() }
expect(lateA.session.stats.hashesIgnored === 1, 'a hash for a tick that has left history is counted as uncheckable')
expect(lateA.session.desyncReport === null, 'an uncheckable hash is not mistaken for a mismatch')
expect(lateA.session.stats.hashesAgreed === lateA.session.stats.hashesCompared, 'a stale hash does not corrupt the agreement count')
// --- four peers, one world, one fighter each --------------------------------
/**
* Build a world with a given number of players, in peer order.
*
* @param count - how many players.
* @returns the world.
*/
function newHubWorld(count: number): CombatWorld {
const world = createWorld(-60, 0)
for (let index = 1; index < count; index += 1) addPlayer(world, -60 + index * 40, 0)
spawnMonsters(world, coopStats, 8, { x: 0, y: 0 }, 140, { overlap: () => 0 })
return world
}
const FOUR = 4
// The hub is grown rather than pre-sized: a peer that has not joined has no pipe
// at all, which is the case worth testing — a transport that merely exists
// handshakes immediately, because answering a hello is not a game action.
const hub = new MemoryHub()
const hubWorlds: CombatWorld[] = []
const hubSessions: NetplaySession[] = []
for (let index = 0; index < FOUR - 1; index += 1) {
const world = newHubWorld(FOUR)
hubWorlds.push(world)
const session = new NetplaySession(
{ peer: index, peers: FOUR, seed: 0x4bee, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 },
coopSimulation(world),
hub.attach(),
)
session.start()
hubSessions.push(session)
}
/**
* Pump one round: every running peer once, then deliver.
*
* @param tick - the scripted tick.
*/
function pumpRound(tick: number): void {
for (let index = 0; index < hubSessions.length; index += 1) {
hubSessions[index]!.setIntent(scriptedInput(index, tick))
hubSessions[index]!.pump()
}
hub.flush()
}
// Three peers are up; the fourth has not joined.
for (let tick = 0; tick < 60; tick += 1) pumpRound(tick)
expect(hubSessions[0]!.stats.peersHeard === 2, 'a session knows which of its peers it has heard from')
expect(hubSessions[0]!.stats.peersExpected === 3, 'a four-peer session expects three other peers')
expect(!hubSessions[0]!.stats.ready, 'a session expecting four peers is not ready with three')
expect(hubSessions[0]!.lockstep.tick === 0, 'with a peer still missing, nobody starts the game')
expect(hubSessions[0]!.stats.stepped === 0, 'and no input is sent into the void before everyone is known')
// The fourth peer joins late and nothing it missed was ever sent, because the
// others refused to start without it.
const lateWorld = newHubWorld(FOUR)
const lateSession = new NetplaySession(
{ peer: 3, peers: FOUR, seed: 0x4bee, inputDelayTicks: 3, hashInterval: 1, timeoutTicks: 400 },
coopSimulation(lateWorld),
hub.attach(),
)
lateSession.start()
hubWorlds.push(lateWorld)
hubSessions.push(lateSession)
for (let tick = 60; tick < 360; tick += 1) pumpRound(tick)
expect(hubSessions.every(session => session.stats.handshaked), 'all four peers complete the handshake')
expect(hubSessions.every(session => session.stats.acknowledged), 'every peer is acknowledged by all the others')
expect(hubSessions.every(session => session.stats.ready), 'every peer becomes ready once the fourth arrives')
expect(hubSessions.every(session => session.lockstep.tick >= 290), 'all four peers ran the whole session')
expect(
new Set(hubSessions.map(session => session.lockstep.tick)).size === 1,
'a late joiner runs level with the peers that waited for it, tick for tick',
)
expect(hubWorlds.every(world => world.players.length === FOUR), 'each world holds one fighter per peer')
const hubDigests = new Set(hubWorlds.map(world => coopDigest(world)))
expect(hubDigests.size === 1, 'all four independently simulated worlds are identical')
expect(hubSessions.every(session => session.desyncReport === null), 'four peers do not desync')
expect(
hubSessions.every(session => session.stats.hashesCompared > 200 && session.stats.hashesAgreed === session.stats.hashesCompared),
'every hash every peer could check agreed',
)
expect(
hubSessions.every(session => session.stats.malformed === 0),
'four peers exchange only well-formed messages',
)
expect(hubWorlds[0]!.kills > 0, 'four fighters killed something together')
expect(
hubWorlds[0]!.players.some((player, index) => index > 0 && player.x !== hubWorlds[0]!.players[0]!.x),
'the fighters stand where their own peer drove them',
)
expect(hubSessions[0]!.peerSeeds.length === 3, 'every peer seed is recorded, not just the first')
expect(hubSessions[0]!.peerSeeds.every(entry => entry.seed === 0x4bee), 'every peer reports the same world seed')
// One of the four goes quiet: the others must stall rather than run on without it.
const beforeQuiet = hubSessions.map(session => session.lockstep.tick)
hub.held.add(2)
for (let tick = 360; tick < 420; tick += 1) pumpRound(tick)
const stalledTicks = hubSessions.map((session, index) => session.lockstep.tick - beforeQuiet[index]!)
expect(hubSessions.every(session => session.desyncReport === null), 'a quiet peer in a four-peer game causes no desync')
expect(
hubSessions[0]!.lockstep.tick <= hubSessions[2]!.lockstep.tick,
'the peers that lost a partner ran no further than the one they lost',
)
expect(
hubSessions.slice(0, 3).every(session => session.stats.waiting > 10),
'the peers that lost a partner counted the ticks they could not run',
)
// Releasing the link leaves everyone the backlog they need to finish level.
hub.held.delete(2)
for (let tick = 420; tick < 520; tick += 1) pumpRound(tick)
const target = Math.max(...hubSessions.map(session => session.lockstep.tick))
for (let round = 0; round < 40; round += 1) {
for (let index = 0; index < FOUR; index += 1) {
const session = hubSessions[index]!
if (session.lockstep.tick >= target) continue
session.setIntent(scriptedInput(index, session.lockstep.tick))
session.pump()
}
hub.flush()
}
expect(
hubSessions.every(session => session.lockstep.tick === target),
'after the link recovers, every peer can be brought level from its own backlog',
)
expect(new Set(hubWorlds.map(world => coopDigest(world))).size === 1, 'the four worlds are still identical after a stall')
expect(stalledTicks[0]! < 60, 'the stall really did hold the other peers back')
// --- a peer that alters input in flight is detected --------------------------
/**
* A transport that rewrites the movement in the first input message it forwards.
*
* This is the shape of a real bug: not a malicious peer, but a corrupted or
* mismatched build. The field is chosen so the change *must* matter — a movement
* of half a step moves the player, whereas flipping the attack flag on a tick with
* nothing in reach changes nothing, and a tamper that changes nothing proves
* nothing. The first input message is the warm-up frame for tick 0, which the send
* cursor fills in so that the first ticks have inputs at all; the world must
* therefore diverge at tick 0.
*
* @param inner - the transport to wrap.
* @returns the tampering transport.
*/
function tamperingTransport(inner: MemoryTransport): Transport {
let tampered = false
return {
send: (data) => {
if (!tampered && data[0] === 2) {
tampered = true
const copy = new Uint8Array(data)
copy[6] = 500 & 0xff // movement x := 0.5, a value the script never sends
copy[7] = (500 >> 8) & 0xff
inner.send(copy)
return
}
inner.send(data)
},
onMessage: handler => { inner.onMessage(handler) },
onClose: handler => { inner.onClose(handler) },
close: () => { inner.close() },
get open(): boolean { return inner.open },
}
}
const [honestPipe, cheatPipe] = memoryTransportPair()
const honest = makePeer(0, honestPipe, 0x2222)
const cheat = makePeer(1, tamperingTransport(cheatPipe), 0x2222)
honestPipe.flush()
for (let tick = 0; tick < 40; tick += 1) {
honest.session.setIntent(scriptedInput(0, tick))
cheat.session.setIntent(scriptedInput(1, tick))
honest.session.pump()
cheat.session.pump()
honestPipe.flush()
}
const report = honest.session.desyncReport ?? cheat.session.desyncReport
expect(report !== null, 'a tampered input is detected as a desync')
expect(report !== null && report.tick === 0, 'the desync is reported at the tick the tampered input applied to')
expect(report !== null && report.local !== report.remote, 'the report carries both digests so the divergence is diagnosable')
expect(honest.session.stats.hashesCompared > 0, 'hashes were exchanged before the divergence was found')
// --- garbage on the wire is dropped, not fatal -------------------------------
const [cleanPipe, noisyPipe] = memoryTransportPair()
const calm = makePeer(0, cleanPipe, 0x3333)
const noisy = makePeer(1, noisyPipe, 0x3333)
// One end receives an unknown type, the other an empty frame: both directions of
// nonsense, delivered before either peer runs a tick.
cleanPipe.send(new Uint8Array([200, 1, 2, 3]))
noisyPipe.send(new Uint8Array(0))
cleanPipe.flush()
for (let tick = 0; tick < 20; tick += 1) {
calm.session.setIntent(scriptedInput(0, tick))
noisy.session.setIntent(scriptedInput(1, tick))
calm.session.pump()
noisy.session.pump()
cleanPipe.flush()
}
expect(noisy.session.stats.malformed === 1, 'an unknown message type is counted and dropped')
expect(calm.session.stats.malformed === 1, 'an empty frame is counted and dropped at the other end')
expect(noisy.session.stats.stepped > 10, 'the session keeps running after a malformed message')
expect(calm.session.desyncReport === null && noisy.session.desyncReport === null, 'garbage does not desync the game')
expect(digest(calm.game) === digest(noisy.game), 'a malformed message leaves both worlds identical')
// --- a silent peer is a timeout, not a desync --------------------------------
// Peer 0's outbound link is held, so peer 1 hears nothing at all: that is the
// "the other player vanished" case, and it must read as a loss, not a divergence.
const [lostPipe, gonePipe] = memoryTransportPair()
const heardFrom = makePeer(0, lostPipe, 0x4444)
const starved = makePeer(1, gonePipe, 0x4444)
lostPipe.flush()
lostPipe.hold = true
for (let tick = 0; tick < 60; tick += 1) {
heardFrom.session.setIntent(scriptedInput(0, tick))
starved.session.setIntent(scriptedInput(1, tick))
heardFrom.session.pump()
starved.session.pump()
lostPipe.flush()
}
expect(starved.session.stats.timedOut, 'a peer nothing has been heard from is declared lost')
expect(starved.session.stats.waiting > 20, 'the starved peer counted every tick it could not run')
expect(starved.session.lockstep.tick === 0, 'the starved peer never fabricated the input it was missing')
expect(starved.session.desyncReport === null && heardFrom.session.desyncReport === null, 'a lost peer is a timeout, never a desync')
// The other end does *not* give up: its peer is still retrying its handshake, so
// something is arriving. "No answer yet" and "no peer" are different states, and
// the session is expected to tell them apart.
expect(!heardFrom.session.stats.timedOut, 'a peer still trying to handshake is not declared lost')
expect(starved.session.stats.helloSent > 1, 'a peer whose handshake goes unanswered keeps retrying it')
// The other end hears those hellos and answers them, but its answers never
// arrive, so its own handshake is never acknowledged and it keeps retrying too:
// "no acknowledgement" is the condition for retrying, not "no answer".
expect(heardFrom.session.stats.helloSent > 1, 'a peer whose own hello was never acknowledged keeps retrying it')
expect(!heardFrom.session.stats.acknowledged, 'an unacknowledged handshake is never called complete')
expect(heardFrom.session.lockstep.tick === 0, 'neither peer runs a tick before the handshake completes')
// --- goodbye -----------------------------------------------------------------
const [byePipe] = memoryTransportPair()
const leaving = makePeer(0, byePipe, 0x5555)
leaving.session.pump()
byePipe.flush()
leaving.session.leave()
expect(!leaving.session.stats.connected, 'leaving closes the pipe')
expect(leaving.session.stats.sent === 2, 'leaving sends exactly the goodbye after the handshake')
// --- the same sessions over a real socket ------------------------------------
const relay = await startRelay(0)
const socketA = new WebSocket(relay.url)
const socketB = new WebSocket(relay.url)
await Promise.all([
new Promise<void>(resolve => { socketA.addEventListener('open', () => { resolve() }) }),
new Promise<void>(resolve => { socketB.addEventListener('open', () => { resolve() }) }),
])
expect(relay.accepted === 2, 'the relay accepted both peers')
const netA = makePeer(0, socketTransport(socketA as unknown as Parameters<typeof socketTransport>[0]), 0x7777)
const netB = makePeer(1, socketTransport(socketB as unknown as Parameters<typeof socketTransport>[0]), 0x7777)
const deadline = Date.now() + 4000
for (let tick = 0; tick < 150 && Date.now() < deadline; tick += 1) {
netA.session.setIntent(scriptedInput(0, tick))
netB.session.setIntent(scriptedInput(1, tick))
netA.session.pump()
netB.session.pump()
// Real sockets deliver on the event loop, so the session is pumped more than
// once per scripted tick: a peer that is ahead stalls until its peer catches up.
await new Promise(resolve => { setTimeout(resolve, 8) })
netA.session.pump()
netB.session.pump()
await new Promise(resolve => { setTimeout(resolve, 1) })
}
expect(netA.session.stats.handshaked && netB.session.stats.handshaked, 'the handshake crosses a real socket')
expect(relay.forwarded > 200, 'the relay forwarded traffic in both directions')
expect(netA.session.stats.stepped > 120 && netB.session.stats.stepped > 120, 'both peers ran the game over the socket')
expect(Math.abs(netA.session.stats.stepped - netB.session.stats.stepped) <= 1, 'the peers stay within one tick of each other')
expect(netA.session.desyncReport === null && netB.session.desyncReport === null, 'no desync over a real socket')
expect(
netA.session.stats.hashesCompared > 50 && netA.session.stats.hashesCompared === netA.session.stats.hashesAgreed,
'every hash that crossed the socket agreed',
)
expect(
netB.session.stats.hashesCompared > 50 && netB.session.stats.hashesCompared === netB.session.stats.hashesAgreed,
'every hash that crossed the socket agreed at the other peer',
)
expect(digest(netA.game) === digest(netB.game), 'two worlds fed by a real socket end up identical')
expect(netA.session.stats.malformed === 0 && netB.session.stats.malformed === 0, 'the socket transport framed every message correctly')
// Closing the socket must be observed by the session rather than hanging it.
netA.session.leave()
await new Promise(resolve => { setTimeout(resolve, 40) })
expect(!netB.session.stats.connected, 'a peer leaving over a socket is observed by the other end')
socketA.close()
socketB.close()
await relay.close()
expect(relay.clients === 0, 'the relay closes every client')
// --- three peers over real sockets -------------------------------------------
const relayThree = await startRelay(0)
const THREE = 3
const threeWorlds = [newHubWorld(THREE), newHubWorld(THREE), newHubWorld(THREE)]
const threeSessions: NetplaySession[] = []
for (let index = 0; index < THREE; index += 1) {
const socket = new WebSocket(relayThree.url)
await new Promise<void>(resolve => { socket.addEventListener('open', () => { resolve() }) })
const session = new NetplaySession(
{ peer: index, peers: THREE, seed: 0x3eed, inputDelayTicks: 4, hashInterval: 1, timeoutTicks: 400 },
coopSimulation(threeWorlds[index]!),
socketTransport(socket as unknown as Parameters<typeof socketTransport>[0]),
)
session.start()
threeSessions.push(session)
}
const threeSockets: WebSocket[] = []
expect(relayThree.accepted === THREE, 'the relay accepts all three peers')
const threeDeadline = Date.now() + 6000
for (let tick = 0; tick < 160 && Date.now() < threeDeadline; tick += 1) {
for (let index = 0; index < THREE; index += 1) {
threeSessions[index]!.setIntent(scriptedInput(index, tick))
threeSessions[index]!.pump()
}
await new Promise(resolve => { setTimeout(resolve, 7) })
for (let index = 0; index < THREE; index += 1) threeSessions[index]!.pump()
await new Promise(resolve => { setTimeout(resolve, 1) })
}
expect(threeSessions.every(session => session.stats.handshaked), 'three peers complete the handshake over real sockets')
expect(threeSessions.every(session => session.stats.acknowledged), 'three peers are acknowledged by each other')
expect(threeSessions.every(session => session.desyncReport === null), 'three peers over sockets do not desync')
expect(threeSessions.every(session => session.stats.malformed === 0), 'the socket transport frames every message for three peers')
expect(
threeSessions.every(session => session.stats.hashesCompared > 50 && session.stats.hashesAgreed === session.stats.hashesCompared),
'three peers agree on every hash they could check',
)
// Level the peers that fell behind, then require byte-identical worlds.
const threeTarget = Math.max(...threeSessions.map(session => session.lockstep.tick))
for (let round = 0; round < 40; round += 1) {
for (let index = 0; index < THREE; index += 1) {
const session = threeSessions[index]!
if (session.lockstep.tick >= threeTarget) continue
session.setIntent(scriptedInput(index, session.lockstep.tick))
session.pump()
}
await new Promise(resolve => { setTimeout(resolve, 4) })
}
expect(
threeSessions.every(session => session.lockstep.tick === threeTarget),
'three peers over sockets can be brought level from their backlog',
)
expect(
new Set(threeWorlds.map(world => coopDigest(world))).size === 1,
'three worlds fed by real sockets are identical',
)
for (const session of threeSessions) session.leave()
await new Promise(resolve => { setTimeout(resolve, 40) })
await relayThree.close()
expect(relayThree.clients === 0, 'the three-peer relay closes every client')
void threeSockets
for (const problem of problems.slice(0, 12)) console.log(` - ${problem}`)
console.log(problems.length === 0 ? 'RESULT protocol, transports and two-peer lockstep hold' : 'RESULT FAILED')
// disabled exit: problems.length === 0 ? 0 : 1)
suiteCompleted = true;
}
describe('verify-net.ts', () => {
test.skipIf(isSkip)('evaluates script successfully', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
vitestExpect(problems).toEqual([]);
});
test.skipIf(isSkip)('runs assertion', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('runs assertion'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the tick round-trips', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the tick round-trips'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the x movement round-trips as fixed point', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the x movement round-trips as fixed point'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the y movement round-trips as fixed point', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the y movement round-trips as fixed point'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the control flags round-trip', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the control flags round-trip'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the skill slot round-trips', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the skill slot round-trips'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the seed round-trips', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the seed round-trips'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the hash and tick round-trip', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the hash and tick round-trip'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an input message is twelve bytes', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an input message is twelve bytes'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an input message starts with its type and peer', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an input message starts with its type and peer'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a movement of -1 is fixed point -1000, little-endian', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a movement of -1 is fixed point -1000, little-endian'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('only the attack flag is set', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('only the attack flag is set'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a hash message is ten bytes', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a hash message is ten bytes'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a hello message is eight bytes', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a hello message is eight bytes'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an acknowledgement names the peer it acknowledges', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an acknowledgement names the peer it acknowledges'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a plain introduction is not mistaken for an acknowledgement', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a plain introduction is not mistaken for an acknowledgement'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an absurd movement is clamped, not wrapped', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an absurd movement is clamped, not wrapped'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a fresh memory pair is open at both ends', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a fresh memory pair is open at both ends'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a memory message is queued, not delivered on send', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a memory message is queued, not delivered on send'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('flushing delivers the queued message', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('flushing delivers the queued message'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an oversized message is refused at the transport', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an oversized message is refused at the transport'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('closing one end closes the pair', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('closing one end closes the pair'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('sending on a closed pair drops instead of throwing', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('sending on a closed pair drops instead of throwing'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('nothing is sent before the session is pumped', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('nothing is sent before the session is pumped'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the handshake exchanges the world seed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the handshake exchanges the world seed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers complete the handshake', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers complete the handshake'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers learn that their own hello was heard', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers learn that their own hello was heard'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a completed handshake stops: one hello and one acknowledgement each', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a completed handshake stops: one hello and one acknowledgement each'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer whose link stalls does not cause a desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer whose link stalls does not cause a desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every hash peer 0 could check agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every hash peer 0 could check agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the peer that waited counted the stalled ticks', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the peer that waited counted the stalled ticks'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers compared state hashes', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers compared state hashes'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every hash that arrived at peer 1 agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every hash that arrived at peer 1 agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('no message was malformed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('no message was malformed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a link that stalls for a moment is not declared lost', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a link that stalls for a moment is not declared lost'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('traffic actually crossed the pipe', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('traffic actually crossed the pipe'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers ran the game, one eventually catching up', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers ran the game, one eventually catching up'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the peer whose link was open ran ahead while the other was starved', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the peer whose link was open ran ahead while the other was starved'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the two ends resume within a few ticks of each other', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the two ends resume within a few ticks of each other'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the starved peer can be brought level from the backlog it already holds', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the starved peer can be brought level from the backlog it already holds'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('at the same tick, the peer that was starved holds exactly the other peers world', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith("at the same tick, the peer that was starved holds exactly the other peer's world"));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers agree on the kill count', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers agree on the kill count'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers agree on the random stream position', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers agree on the random stream position'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers agree on monster health', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers agree on monster health'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the test actually produced kills to disagree about', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the test actually produced kills to disagree about'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer that fell behind still checks every hash it received', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer that fell behind still checks every hash it received'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('catching up results in a real number of compared hashes', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('catching up results in a real number of compared hashes'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every hash peer 1 could check agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every hash peer 1 could check agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('two players in one world do not desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('two players in one world do not desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every co-op hash peer 0 could check agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every co-op hash peer 0 could check agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every co-op hash peer 1 could check agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every co-op hash peer 1 could check agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers ran the whole co-op run, tick for tick together', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers ran the whole co-op run, tick for tick together'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the two peers hold the same two-player world', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the two peers hold the same two-player world'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the world really has two players in it', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the world really has two players in it'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the two players killed something together', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the two players killed something together'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('experience is credited to the peer that landed the killing blow', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('experience is credited to the peer that landed the killing blow'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('player 0 moved under its own input', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('player 0 moved under its own input'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the co-op world actually changed while it ran', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the co-op world actually changed while it ran'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a monster attacks the nearest player', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a monster attacks the nearest player'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a monster 300 pixels away from the other player leaves it alone', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a monster 300 pixels away from the other player leaves it alone'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('with one peer dead the monsters hunt the other', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('with one peer dead the monsters hunt the other'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a dead peer stays dead while its respawn timer runs', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a dead peer stays dead while its respawn timer runs'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a hash for a tick that has left history is counted as uncheckable', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a hash for a tick that has left history is counted as uncheckable'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an uncheckable hash is not mistaken for a mismatch', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an uncheckable hash is not mistaken for a mismatch'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a stale hash does not corrupt the agreement count', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a stale hash does not corrupt the agreement count'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a session knows which of its peers it has heard from', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a session knows which of its peers it has heard from'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a four-peer session expects three other peers', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a four-peer session expects three other peers'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a session expecting four peers is not ready with three', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a session expecting four peers is not ready with three'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('with a peer still missing, nobody starts the game', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('with a peer still missing, nobody starts the game'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('and no input is sent into the void before everyone is known', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('and no input is sent into the void before everyone is known'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('all four peers complete the handshake', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('all four peers complete the handshake'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every peer is acknowledged by all the others', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every peer is acknowledged by all the others'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every peer becomes ready once the fourth arrives', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every peer becomes ready once the fourth arrives'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('all four peers ran the whole session', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('all four peers ran the whole session'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a late joiner runs level with the peers that waited for it, tick for tick', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a late joiner runs level with the peers that waited for it, tick for tick'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('each world holds one fighter per peer', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('each world holds one fighter per peer'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('all four independently simulated worlds are identical', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('all four independently simulated worlds are identical'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('four peers do not desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('four peers do not desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every hash every peer could check agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every hash every peer could check agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('four peers exchange only well-formed messages', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('four peers exchange only well-formed messages'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('four fighters killed something together', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('four fighters killed something together'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the fighters stand where their own peer drove them', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the fighters stand where their own peer drove them'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every peer seed is recorded, not just the first', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every peer seed is recorded, not just the first'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every peer reports the same world seed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every peer reports the same world seed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a quiet peer in a four-peer game causes no desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a quiet peer in a four-peer game causes no desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the peers that lost a partner ran no further than the one they lost', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the peers that lost a partner ran no further than the one they lost'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the peers that lost a partner counted the ticks they could not run', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the peers that lost a partner counted the ticks they could not run'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('after the link recovers, every peer can be brought level from its own backlog', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('after the link recovers, every peer can be brought level from its own backlog'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the four worlds are still identical after a stall', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the four worlds are still identical after a stall'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the stall really did hold the other peers back', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the stall really did hold the other peers back'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a tampered input is detected as a desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a tampered input is detected as a desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the desync is reported at the tick the tampered input applied to', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the desync is reported at the tick the tampered input applied to'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the report carries both digests so the divergence is diagnosable', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the report carries both digests so the divergence is diagnosable'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('hashes were exchanged before the divergence was found', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('hashes were exchanged before the divergence was found'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an unknown message type is counted and dropped', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an unknown message type is counted and dropped'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an empty frame is counted and dropped at the other end', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an empty frame is counted and dropped at the other end'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the session keeps running after a malformed message', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the session keeps running after a malformed message'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('garbage does not desync the game', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('garbage does not desync the game'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a malformed message leaves both worlds identical', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a malformed message leaves both worlds identical'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer nothing has been heard from is declared lost', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer nothing has been heard from is declared lost'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the starved peer counted every tick it could not run', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the starved peer counted every tick it could not run'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the starved peer never fabricated the input it was missing', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the starved peer never fabricated the input it was missing'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a lost peer is a timeout, never a desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a lost peer is a timeout, never a desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer still trying to handshake is not declared lost', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer still trying to handshake is not declared lost'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer whose handshake goes unanswered keeps retrying it', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer whose handshake goes unanswered keeps retrying it'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer whose own hello was never acknowledged keeps retrying it', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer whose own hello was never acknowledged keeps retrying it'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('an unacknowledged handshake is never called complete', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('an unacknowledged handshake is never called complete'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('neither peer runs a tick before the handshake completes', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('neither peer runs a tick before the handshake completes'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('leaving closes the pipe', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('leaving closes the pipe'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('leaving sends exactly the goodbye after the handshake', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('leaving sends exactly the goodbye after the handshake'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the relay accepted both peers', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the relay accepted both peers'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the handshake crosses a real socket', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the handshake crosses a real socket'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the relay forwarded traffic in both directions', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the relay forwarded traffic in both directions'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('both peers ran the game over the socket', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('both peers ran the game over the socket'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the peers stay within one tick of each other', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the peers stay within one tick of each other'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('no desync over a real socket', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('no desync over a real socket'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every hash that crossed the socket agreed', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every hash that crossed the socket agreed'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('every hash that crossed the socket agreed at the other peer', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('every hash that crossed the socket agreed at the other peer'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('two worlds fed by a real socket end up identical', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('two worlds fed by a real socket end up identical'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the socket transport framed every message correctly', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the socket transport framed every message correctly'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('a peer leaving over a socket is observed by the other end', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('a peer leaving over a socket is observed by the other end'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the relay closes every client', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the relay closes every client'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the relay accepts all three peers', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the relay accepts all three peers'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('three peers complete the handshake over real sockets', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('three peers complete the handshake over real sockets'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('three peers are acknowledged by each other', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('three peers are acknowledged by each other'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('three peers over sockets do not desync', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('three peers over sockets do not desync'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the socket transport frames every message for three peers', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the socket transport frames every message for three peers'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('three peers agree on every hash they could check', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('three peers agree on every hash they could check'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('three peers over sockets can be brought level from their backlog', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('three peers over sockets can be brought level from their backlog'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('three worlds fed by real sockets are identical', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('three worlds fed by real sockets are identical'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
test.skipIf(isSkip)('the three-peer relay closes every client', () => {
vitestExpect(suiteCompleted, 'Suite failed to complete').toBe(true);
let resKey = _results.findIndex(x => x.desc.startsWith('the three-peer relay closes every client'));
if (resKey !== -1) vitestExpect(_results[resKey].cond, _results[resKey].detail || _results[resKey].desc).toBe(true);
else vitestExpect(true).toBe(true);
});
});