Two findings of the step-2 player walk (2026-09-27, both rigs). F6, option (b) of the org lead (D186): a code no recent issuer holds - every made-up one - was still asked of every other enabled server, and while any of them was down the redeem waited out its whole timeout (12 s on both rigs). The second pass now skips the servers the board poll last saw without a connected game; they count as offline without the wait. Issuers are still asked whatever their state, so a good code on a down server stays "unsure". Live on the walk core: 338 ms with five servers down, 360 ms with a rig stopped as well. F7 (D187): on Carbon a due audit sync went out the moment the sidecar reconnected, 80 s before "Server startup complete". The worldReady hold reads the stored hello, which is the OLD boot's until the poll reads the new one. reasonToSync now also holds while the stored state says the game is not connected (online 0), which the poll writes the moment the server goes away. titleSync already held on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
172 lines
8.1 KiB
JavaScript
172 lines
8.1 KiB
JavaScript
// ── PLAN_FIXES §6 step 2, the module's half ───────────────────────────────
|
|
//
|
|
// Each test here is one finding of the first player walk (2026-09-26), held
|
|
// down so it cannot come back:
|
|
//
|
|
// • F13/F14 — a zone that expired in the game is recorded `expired` by core,
|
|
// through `ctx.events.expired` (D170, D183)
|
|
// • F8 — a plugin that loads with permissions re-syncs within a tick (D184)
|
|
// • F7 — nothing is pushed to a game whose world is still loading, or
|
|
// that the poll saw go away (the step-2 walk)
|
|
// • D182 — the ZoneManager helper's state reaches the servers page
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
|
|
const { fakeCtx } = require('./_fakes')
|
|
|
|
/** A core whose `db.query` records every statement and answers from `answer`. */
|
|
function withCore(answer = () => []) {
|
|
const statements = []
|
|
const ctx = fakeCtx({
|
|
db: {
|
|
query: (sql, params = []) => {
|
|
statements.push({ sql, params })
|
|
return Promise.resolve(answer(sql, params))
|
|
},
|
|
pool: {},
|
|
},
|
|
})
|
|
require('../core')._reset()
|
|
require('../core').init(ctx)
|
|
return { ctx, statements }
|
|
}
|
|
|
|
const item = (kind, over = {}) => ({
|
|
id: 1,
|
|
t: 1,
|
|
kind,
|
|
frame: { kind, type: 'event', t: 1, serverId: 'main', wipeId: 'w-1', ...over },
|
|
})
|
|
|
|
// ── F13, F14 ───────────────────────────────────────────────────────────────
|
|
|
|
test('an expired zone is handed to core as the resource the zone step ledgered', async () => {
|
|
const { ctx } = withCore()
|
|
const { apply } = require('../ingest')
|
|
|
|
// Protocol 13's frame: the entity's kind is `what`, and the frame keeps its own.
|
|
await apply('srv-a', item('world.expired', { id: 'rg-13-35875416-1', what: 'zone', runId: '13' }))
|
|
|
|
const calls = ctx.events.expired.calls
|
|
assert.equal(calls.length, 1)
|
|
// The same kind and ref `place()` filed it under — `<serverId>:<id>` — or core
|
|
// would find no row to close.
|
|
assert.deepEqual(calls[0][0], { kind: 'world', ref: 'srv-a:rg-13-35875416-1' })
|
|
})
|
|
|
|
test('a world.expired that names nothing is not passed on', async () => {
|
|
const { ctx } = withCore()
|
|
const { apply } = require('../ingest')
|
|
|
|
await apply('srv-a', item('world.expired', { what: 'zone' }))
|
|
assert.equal(ctx.events.expired.calls.length, 0)
|
|
})
|
|
|
|
// ── F8 ─────────────────────────────────────────────────────────────────────
|
|
|
|
test('a plugin that loads with permissions marks the permission sync dirty; one without does not', async () => {
|
|
const { statements } = withCore()
|
|
const { apply } = require('../ingest')
|
|
const dirtying = () => statements.filter((s) => /UPDATE\s+rust_perm_sync\s+SET\s+dirty\s*=\s*1/i.test(s.sql))
|
|
|
|
await apply('srv-a', item('plugin.loaded', { name: 'PopupNotifications', version: '0.2.1', permissions: [] }))
|
|
assert.equal(dirtying().length, 0, 'a plugin that registers nothing cannot change what a grant resolves to')
|
|
|
|
await apply('srv-a', item('plugin.loaded', { name: 'Kits', version: '4.4.9', permissions: ['kits.admin', 'kits.vip'] }))
|
|
assert.equal(dirtying().length, 1)
|
|
assert.deepEqual(dirtying()[0].params, ['srv-a'])
|
|
|
|
await apply('srv-a', item('plugin.unloaded', { name: 'Kits', permissions: ['kits.admin', 'kits.vip'] }))
|
|
assert.equal(dirtying().length, 2, 'an unload is a reason too: its grants are now unresolved')
|
|
})
|
|
|
|
// ── F7 ─────────────────────────────────────────────────────────────────────
|
|
|
|
test('no permission sync goes to a world that is still loading — unless a human asked', () => {
|
|
withCore()
|
|
const permSync = require('../permSync')
|
|
const sync = { state: 'ok', dirty: false, syncedHash: 'h1', bootId: 'boot-1', wipeId: 'w-1', lastAttemptAt: new Date() }
|
|
const at = (state, force = false) => permSync.reasonToSync({ desiredHash: 'h1', sync, state, force })
|
|
|
|
// The first walk's restart sync: a new boot id, arriving before the save loaded.
|
|
assert.equal(at({ bootId: 'boot-2', wipeId: 'w-1', worldReady: false }), null)
|
|
assert.equal(at({ bootId: 'boot-2', wipeId: 'w-1', worldReady: true }), 'restart')
|
|
// A plugin that never says is ready, as it always was (§28.6).
|
|
assert.equal(at({ bootId: 'boot-2', wipeId: 'w-1', worldReady: null }), 'restart')
|
|
assert.equal(permSync.reasonToSync({ desiredHash: 'h1', sync: null, state: { worldReady: false }, force: false }), null)
|
|
assert.equal(at({ worldReady: false }, true), 'requested')
|
|
})
|
|
|
|
test('no permission sync goes to a game the poll saw go away, however ready its last hello said it was', () => {
|
|
// The step-2 walk, on Carbon: the sidecar came back and a due audit went out in
|
|
// the same moment, 80 s before "Server startup complete". The stored hello was the
|
|
// OLD boot's (same boot id, hence "audit" and not "restart", worldReady true);
|
|
// only `online: 0` — written by the poll when the server went away — said the
|
|
// game was not there.
|
|
withCore()
|
|
const permSync = require('../permSync')
|
|
const stale = new Date(Date.now() - permSync.AUDIT_MS - 1000)
|
|
const sync = { state: 'ok', dirty: false, syncedHash: 'h1', bootId: 'boot-1', wipeId: 'w-1', lastAttemptAt: stale }
|
|
const at = (state, force = false) => permSync.reasonToSync({ desiredHash: 'h1', sync, state, force })
|
|
|
|
assert.equal(at({ online: 0, bootId: 'boot-1', wipeId: 'w-1', worldReady: true }), null)
|
|
assert.equal(at({ online: false, bootId: 'boot-1', wipeId: 'w-1', worldReady: true }), null)
|
|
// Back, with the new boot's hello still loading: held by worldReady, as before.
|
|
assert.equal(at({ online: 1, bootId: 'boot-2', wipeId: 'w-1', worldReady: false }), null)
|
|
// Loaded: the restart sync goes.
|
|
assert.equal(at({ online: 1, bootId: 'boot-2', wipeId: 'w-1', worldReady: true }), 'restart')
|
|
// The audit itself still runs for a server that is up.
|
|
assert.equal(at({ online: 1, bootId: 'boot-1', wipeId: 'w-1', worldReady: true }), 'audit')
|
|
// A human's "sync now" is not held.
|
|
assert.equal(at({ online: 0 }, true), 'requested')
|
|
})
|
|
|
|
test('no title push goes to a world that is still loading', async () => {
|
|
withCore()
|
|
const titleSync = require('../titleSync')
|
|
const client = require('../sidecarClient')
|
|
const original = client.titles
|
|
let pushed = 0
|
|
client.titles = async () => {
|
|
pushed += 1
|
|
return { ok: true, data: { kind: 'titles.ok' } }
|
|
}
|
|
try {
|
|
const answer = await titleSync.syncOne(
|
|
{ id: 'srv-a', name: 'A' },
|
|
{ online: true, protocol: 13, bootId: 'boot-2', wipeId: 'w-1', worldReady: false },
|
|
)
|
|
assert.equal(answer, null)
|
|
assert.equal(pushed, 0)
|
|
} finally {
|
|
client.titles = original
|
|
}
|
|
})
|
|
|
|
test('worldReady and the zone helper are read out of the stored hello', async () => {
|
|
withCore((sql) =>
|
|
/FROM rust_server_state/.test(sql)
|
|
? [
|
|
{ serverId: 'a', worldReady: 'false', zoneHelper: '{"state":"missing"}' },
|
|
{ serverId: 'b', worldReady: 'true', zoneHelper: '{"state":"patched","version":"0.1.0"}' },
|
|
{ serverId: 'c', worldReady: null, zoneHelper: null },
|
|
{ serverId: 'd', worldReady: 'true', zoneHelper: '{"state":"unsupported","reason":"no Zone.InitializeZone"}' },
|
|
{ serverId: 'e', worldReady: 'true', zoneHelper: 'not json' },
|
|
]
|
|
: [],
|
|
)
|
|
const serversDb = require('../model/servers/servers.db')
|
|
const rows = await serversDb.listState()
|
|
const by = Object.fromEntries(rows.map((r) => [r.serverId, r]))
|
|
|
|
assert.equal(by.a.worldReady, false)
|
|
assert.equal(by.b.worldReady, true)
|
|
assert.equal(by.c.worldReady, null)
|
|
assert.deepEqual(by.a.zoneHelper, { state: 'missing' })
|
|
assert.deepEqual(by.b.zoneHelper, { state: 'patched', version: '0.1.0' })
|
|
assert.equal(by.c.zoneHelper, null)
|
|
assert.deepEqual(by.d.zoneHelper, { state: 'unsupported', reason: 'no Zone.InitializeZone' })
|
|
assert.equal(by.e.zoneHelper, null, 'an unreadable value is no report, not a crash')
|
|
})
|