// ── The ingest ──────────────────────────────────────────────────────────── // // Every test here is about one of three things, and all three are mistakes that // look correct in review: // // • **who gets credited.** A suicide must not credit the victim with a kill. // That single line would produce a leaderboard topped by whoever died most, // and it would look plausible for a whole wipe. // • **the cursor's ordering.** It advances AFTER the batch, never before, so a // crash re-reads rather than skips. Skipping is silent and permanent. // • **absent is not zero.** A session whose start was never seen contributes // no playtime rather than zero playtime. // // The database is a recorder. Asserting the SQL exactly would be a test of the // SQL's punctuation, so each case asserts the *statement shape* and the values — // which table was written, and with what. const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') /** Installs a core whose `db.query` records every statement. */ function withRecorder() { const statements = [] const ctx = fakeCtx({ db: { query: (sql, params = []) => { statements.push({ sql, params }) return Promise.resolve([]) }, pool: {}, }, }) require('../core')._reset() require('../core').init(ctx) return { statements, /** Every statement that touched a table, with its parameters. */ touching(table) { return statements.filter((s) => s.sql.includes(table)) }, } } const frame = (over = {}) => ({ type: 'event', t: 1789560564452, serverId: 'main', wipeId: 'w-20260915T195817Z', ...over, }) const item = (kind, over = {}) => ({ id: 1, t: 1, kind, frame: frame({ kind, ...over }) }) test('every frame is stored, whether or not this build understands it', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', item('player.death', { steamId: '76561198000000001' })) await apply('main', item('something.from.protocol.9')) const stored = rec.touching('rust_events') assert.equal(stored.length, 2, 'an unrecognised kind must still be stored') // The one copy of an event a later version will know how to read is the one // this version chose not to throw away. assert.ok(stored[1].params.includes('something.from.protocol.9')) }) test('a wipe exists because a frame mentioned it', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', item('player.chat', { steamId: '1', message: 'hello' })) const wipes = rec.touching('rust_wipes') assert.equal(wipes.length, 1) assert.deepEqual(wipes[0].params.slice(0, 2), ['main', 'w-20260915T195817Z']) }) test('a kill credits the attacker and a death the victim', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply( 'main', item('player.death', { steamId: 'victim', attackerType: 'player', attackerId: 'killer', attackerName: 'Killer', }), ) const stats = rec.touching('rust_player_wipe_stats') assert.equal(stats.length, 2, 'one row for the victim, one for the attacker') // The parameter order is (server, wipe, steam, kills, deaths, suicides, ...). const victim = stats.find((s) => s.params[2] === 'victim') const killer = stats.find((s) => s.params[2] === 'killer') assert.ok(victim && killer) assert.equal(victim.params[3], 0, 'the victim scored no kill') assert.equal(victim.params[4], 1, 'the victim died once') assert.equal(killer.params[3], 1, 'the attacker scored one kill') assert.equal(killer.params[4], 0, 'the attacker did not die') }) test('a suicide is a death and a suicide, and credits nobody with a kill', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', item('player.death', { steamId: 'victim', attackerType: 'self' })) const stats = rec.touching('rust_player_wipe_stats') assert.equal(stats.length, 1, 'nobody is credited with the kill') assert.equal(stats[0].params[4], 1, 'it is still a death') assert.equal(stats[0].params[5], 1, 'and a suicide') assert.equal(stats[0].params[3], 0) }) test('an environment or NPC death credits no attacker', async () => { for (const attackerType of ['environment', 'npc']) { const rec = withRecorder() const { apply } = require('../ingest') await apply('main', item('player.death', { steamId: 'victim', attackerType })) const stats = rec.touching('rust_player_wipe_stats') assert.equal(stats.length, 1, `${attackerType} must credit nobody`) assert.equal(stats[0].params[4], 1) } }) test('an absent session length adds no playtime and no session', async () => { const rec = withRecorder() const { apply } = require('../ingest') // A player who was already on the server when the plugin loaded: the plugin // omits `sessionSec` rather than sending 0, and the difference has to survive // all the way to the column. Adding a zero would record a session of no // length, which is a different claim from recording no session. await apply('main', item('player.disconnected', { steamId: 'p1', reason: 'quit' })) const stats = rec.touching('rust_player_wipe_stats') assert.equal(stats[0].params[8], 0, 'no session counted') assert.equal(stats[0].params[9], 0, 'no playtime added') const rec2 = withRecorder() await require('../ingest').apply( 'main', item('player.disconnected', { steamId: 'p1', sessionSec: 600 }), ) const counted = rec2.touching('rust_player_wipe_stats') assert.equal(counted[0].params[8], 1) assert.equal(counted[0].params[9], 600) }) test('a tally is added per resource, as a delta', async () => { const rec = withRecorder() const { apply } = require('../ingest') await apply( 'main', item('player.tally', { steamId: 'p1', gathered: { wood: 1200, stones: 300 }, npcKills: 3, structures: 2, }), ) const gathered = rec.touching('rust_gather_totals') assert.equal(gathered.length, 2) assert.deepEqual( gathered.map((g) => [g.params[3], g.params[4]]), [ ['wood', 1200], ['stones', 300], ], ) const stats = rec.touching('rust_player_wipe_stats') assert.equal(stats[0].params[6], 3, 'npc kills') assert.equal(stats[0].params[7], 2, 'structures') // `amount = amount + VALUES(amount)` is what makes a delta correct. A running // total on the wire would double every number here, slowly, looking right. assert.match(gathered[0].sql, /amount = amount \+ VALUES\(amount\)/) }) test('a new server starts at the feed tail, not at the beginning of history', async () => { withRecorder() const sidecar = require('../sidecarClient') const db = require('../model/events/events.db') const ingest = require('../ingest') const originalTail = sidecar.feedTail const originalCursor = db.getCursor const originalSet = db.setCursor const written = [] db.getCursor = async () => null db.setCursor = async (...args) => written.push(args) sidecar.feedTail = async () => ({ ok: true, status: 'ok', data: { lastId: 4021, items: [] } }) try { const applied = await ingest.ingestServer({ id: 'main' }) assert.equal(applied, 0, 'nothing is replayed') assert.deepEqual(written, [['main', 4021, 0]], 'the cursor starts at the end') } finally { sidecar.feedTail = originalTail db.getCursor = originalCursor db.setCursor = originalSet } }) test('an unreachable sidecar writes no cursor at all', async () => { withRecorder() const sidecar = require('../sidecarClient') const db = require('../model/events/events.db') const ingest = require('../ingest') const originalTail = sidecar.feedTail const originalCursor = db.getCursor const originalSet = db.setCursor const written = [] db.getCursor = async () => null db.setCursor = async (...args) => written.push(args) sidecar.feedTail = async () => ({ ok: false, status: 'transport-error', data: null }) try { await ingest.ingestServer({ id: 'main' }) // A cursor of 0 written here would replay the sidecar's whole retained // history the moment it came back — which is the failure that looks like a // working catch-up until somebody reads the leaderboard. assert.deepEqual(written, []) } finally { sidecar.feedTail = originalTail db.getCursor = originalCursor db.setCursor = originalSet } }) test('the cursor advances after the batch, and one bad event does not wedge it', async () => { withRecorder() const sidecar = require('../sidecarClient') const db = require('../model/events/events.db') const ingest = require('../ingest') const originals = { feed: sidecar.feed, getCursor: db.getCursor, setCursor: db.setCursor, insertEvent: db.insertEvent, } const order = [] db.getCursor = async () => ({ lastEventId: 10 }) db.setCursor = async (_id, last) => order.push(`cursor:${last}`) db.insertEvent = async (row) => { order.push(`event:${row.kind}`) if (row.kind === 'player.chat') throw new Error('malformed') } sidecar.feed = async (_server, since) => since === 10 ? { ok: true, status: 'ok', data: { items: [item('player.chat'), item('player.connected', { steamId: 'p1' })], lastId: 12, more: false, }, } : { ok: true, status: 'ok', data: { items: [], lastId: since, more: false } } try { const applied = await ingest.ingestServer({ id: 'main' }) // The bad row is logged and skipped; the good one still counts. assert.equal(applied, 1) // And the ordering the whole design rests on: every event is written before // the cursor moves past it. assert.deepEqual(order, ['event:player.chat', 'event:player.connected', 'cursor:12']) } finally { Object.assign(db, { getCursor: originals.getCursor, setCursor: originals.setCursor, insertEvent: originals.insertEvent, }) sidecar.feed = originals.feed } }) test('a board replaces presence rather than appending to it', async () => { const rec = withRecorder() const ingest = require('../ingest') await ingest.applyBoards('main', { 'players.online': { kind: 'players.online', type: 'snapshot', count: 1, players: [{ steamId: 'p1', name: 'One', sleeping: false }], }, }) const presence = rec.touching('rust_presence') // The DELETE is what makes it a board. Without it a player who left stays // online for ever, which is the exact drift the board exists to correct. assert.match(presence[0].sql, /^DELETE FROM rust_presence/) assert.match(presence[1].sql, /INSERT INTO rust_presence/) })