// ── 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 marked = [] let putCalls = 0 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 } })