fix(rust): protocol 13 step 2 — expiry, plugin loads, the loading hold, NPC names, the link fleet (F2 F5 F6 F7 F8 F13 F14)
The module's half of PLAN_FIXES §6 step 2 (decisions D181-D185, docs#288). - F13/F14 (D170, D183): `world.expired`, recognisable from protocol 13 by its `what`, is handed to core as the resource the zone step ledgered (`world`, `<serverId>:<id>`) through ctx.events.expired, which records it `expired`. coreApi moves to ^1.11.0 (website#209). - F8 (D184): `plugin.loaded` / `plugin.unloaded` mark the permission sync dirty when the plugin added or removed permissions, so an unresolved grant lands on the next tick instead of the fifteen-minute audit. - Catalogue: plugin.loaded/unloaded, world.expired and lease.expired are staff kinds. The last two were never classified (default deny kept them off public pages); the test now covers every event kind through protocol 13. - F7: permission and title pushes hold while the stored hello says `worldReady: false` (a human's "sync now" does not); a failed or refused permission sync now logs at warn. - F2 (D185): the killfeed names an NPC attacker — a family (Scientist, Bandit guard, Bradley APC…) or the prefab without its variant digits (wolf2 → Wolf). - F5/F6: a link code is asked of the servers that minted one in the last six minutes first, then of the rest, each group in parallel; "unsure" only when one of the minting servers is unreachable. - D182: the admin server list carries the ZoneManager helper's state from the hello, and the servers page says what a missing or failed helper costs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
146
server/test/planFixesStep2.test.js
Normal file
146
server/test/planFixesStep2.test.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// ── 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
|
||||
// • 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 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')
|
||||
})
|
||||
Reference in New Issue
Block a user