// ── What a refresh writes when nobody answers ───────────────────────────── // // The refresh loop has three outcomes (see `boot.js`), and the two unhappy ones // are the interesting half of this module's promise: the site renders the last // thing each server said **while every server is off**. A page can only do that // if the row still holds what the server said. // // The defect this suite exists for shipped in phase 3 and was found by walking // phase 4's own pages: an unreachable refresh called `putState` with two fields, // and `putState` replaces the row — so the first time a game host rebooted, the // hostname, the map, the size, the seed and the wipe id were all set to NULL. // The list then read "Offline" with nothing beside it, which is not "here is // what we know about a server that is down", it is "we have never heard of it". // // It is invisible to any test that stubs a sidecar which answers, which is why // there was not one. const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') function withCore(ctx = fakeCtx()) { require('../core')._reset() require('../core').init(ctx) return ctx } /** The columns a description lives in — the ones an unreachable write must not touch. */ const DESCRIPTION = ['hostname', 'level', 'seed', 'world_size', 'boot_id', 'save_created_at', 'wipe_id'] test('an unreachable refresh does not write the description columns at all', async () => { const queries = [] withCore(fakeCtx({ db: { query: (sql, params) => { queries.push({ sql, params }) return Promise.resolve([]) }, pool: {}, }, })) const db = require('../model/servers/servers.db') await db.markUnreachable('main', false) assert.equal(queries.length, 1) const { sql, params } = queries[0] // Asserted against the SQL rather than against a round trip, because the whole // failure is about which columns a statement mentions. A column named here is // a column that can be nulled. for (const column of DESCRIPTION) { assert.ok(!sql.includes(column), `markUnreachable writes ${column}, which is the server's description`) } assert.ok(sql.includes('reachable')) assert.ok(sql.includes('online')) assert.ok(sql.includes('updated_at')) assert.deepStrictEqual(params, ['main', 0]) }) test('a sidecar that is up with no game behind it is reachable and offline', async () => { // The middle outcome, and the one that is easy to collapse into the other two: // a fresh install whose plugin is not loaded yet. Reporting it as unreachable // sends an operator to look at the network instead of at the game server. const queries = [] withCore(fakeCtx({ db: { query: (sql, params) => { queries.push({ sql, params }) return Promise.resolve([]) }, pool: {}, }, })) await require('../model/servers/servers.db').markUnreachable('main', true) assert.deepStrictEqual(queries[0].params, ['main', 1]) }) test('neither unhappy path calls putState', async () => { // The regression in one assertion: `putState` is the whole-row write, and // calling it with two fields is what blanked the description. withCore(fakeCtx({ db: { query: () => Promise.resolve([]), pool: {} } })) const db = require('../model/servers/servers.db') const sidecar = require('../sidecarClient') const boot = require('../boot') const originalPut = db.putState const originalMark = db.markUnreachable const originalBoards = sidecar.boards const originalHealth = sidecar.health const marked = [] let putCalls = 0 sidecar.health = async () => ({ ok: false, status: 0, data: null }) db.putState = async () => { putCalls += 1 } db.markUnreachable = async (id, reachable) => { marked.push([id, reachable]) } try { // Nothing answered. sidecar.boards = async () => ({ ok: false, status: 0, data: null }) await boot.refreshOne({ id: 'main', baseUrl: 'http://127.0.0.1:1', token: 't', protocol: 2 }) // The sidecar answered, and has never heard from a game. sidecar.boards = async () => ({ ok: true, status: 200, data: { boards: {} } }) await boot.refreshOne({ id: 'main', baseUrl: 'http://127.0.0.1:1', token: 't', protocol: 2 }) assert.equal(putCalls, 0, 'an unhappy refresh replaced the whole state row') assert.deepStrictEqual(marked, [['main', false], ['main', true]]) } finally { db.putState = originalPut db.markUnreachable = originalMark sidecar.boards = originalBoards sidecar.health = originalHealth } }) test('a board the game left behind is not a game that is up (D68)', async () => { // The sidecar keeps its last `server.hello` after the plugin disconnects, so // until phase 10 a hung game — or an unloaded bridge — with the sidecar still // up read as ONLINE here, with the players it had when it stopped. Only // `/health` knows whether the plugin is connected now. withCore(fakeCtx({ db: { query: () => Promise.resolve([]), pool: {} } })) const db = require('../model/servers/servers.db') const sidecar = require('../sidecarClient') const ingest = require('../ingest') const boot = require('../boot') const engagement = require('../engagement/emit') const saved = { put: db.putState, boards: sidecar.boards, health: sidecar.health, apply: ingest.applyBoards } const puts = [] const applied = [] engagement.reset() db.putState = async (state) => { puts.push(state) } ingest.applyBoards = async (id, boards) => { applied.push(boards) } sidecar.boards = async () => ({ ok: true, status: 200, data: { boards: { 'server.hello': { players: 12, maxPlayers: 100, hostname: 'Main' }, 'players.online': { players: [{ steamId: '1', name: 'Still here?' }] }, } }, }) const server = { id: 'main', name: 'Main', baseUrl: 'http://127.0.0.1:1', token: 't', protocol: 7 } try { sidecar.health = async () => ({ ok: true, status: 200, data: { plugin_connected: false } }) await boot.refreshOne(server) assert.strictEqual(puts[0].online, false) assert.strictEqual(puts[0].players, 0, 'the last count is not a count') assert.strictEqual(puts[0].seen, false, 'a stale board must not move "last seen"') assert.strictEqual(puts[0].hostname, 'Main', 'the description is still written') assert.deepStrictEqual(applied[0]['players.online'].players, [], 'nobody is named as online') sidecar.health = async () => ({ ok: true, status: 200, data: { plugin_connected: true } }) await boot.refreshOne(server) assert.strictEqual(puts[1].online, true) assert.strictEqual(puts[1].players, 12) assert.notStrictEqual(puts[1].seen, false) assert.strictEqual(applied[1]['players.online'].players.length, 1) // An unanswered /health is unknown, and unknown is not up. sidecar.health = async () => ({ ok: false, status: 0, data: null }) await boot.refreshOne(server) assert.strictEqual(puts[2].online, false) } finally { db.putState = saved.put sidecar.boards = saved.boards sidecar.health = saved.health ingest.applyBoards = saved.apply engagement.reset() } }) test('putState moves last_seen_at only for a game that was seen', async () => { const queries = [] withCore(fakeCtx({ db: { query: (sql, params) => { queries.push({ sql, params }); return Promise.resolve([]) }, pool: {} }, })) const db = require('../model/servers/servers.db') await db.putState({ serverId: 'main', reachable: true, online: false, seen: false }) await db.putState({ serverId: 'main', reachable: true, online: true }) // The two trailing parameters feed the two IF(?, CURRENT_TIMESTAMP, …)s. assert.deepStrictEqual(queries[0].params.slice(-2), [0, 0]) assert.deepStrictEqual(queries[1].params.slice(-2), [1, 1]) assert.strictEqual((queries[0].sql.match(/\?/g) || []).length, queries[0].params.length, 'every placeholder has a value') })