Files
Module-Rust/server/test/guardedCrate.test.js
wtclaude 3aa142bf06
All checks were successful
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 32s
PR Checks / frozen-manifest (pull_request) Successful in 44s
fix(rust): the last fixes' small ones (W3 W6 W7/W11 W8 W10 W13-W18)
Stage 2 of PLAN_LAST_FIXES (docs#366).

- W7/W11: an "is it loaded" refusal is confirmed live before it is given.
  New liveStatus.js asks the sidecar once (boot.refreshOne, as a poll tick
  would), at most once per server per 5 s and waiting at most 3 s, then
  reads the stored status again. Used by Economics (step and staff page),
  Backpacks, Dangerous Treasures, Guarded Crate, RaidableBases, ZoneDomes,
  TruePVE and RunicNPC.
- W3: a world call answered `already-out` (the bridge half comes in stage
  3) succeeds as called 0, saying a Travelling Vendor is already out.
- W6: "A locked crate dropped by a Chinook is being hacked."
- W8: a RaidableBases profile's audit row names RaidableBases, not "@data".
- W10: the backpack sizes picker lists each size once, naming its servers.
- W13: a RaidableBases name both servers describe differently shows each
  server's own description.
- W14: each RaidableBases note after the first starts its own sentence.
- W15: a staff deposit refused at Economics' limit answers 409.
- W16: a payment's failedNote explains only the reasons that happened.
- W17: the sync state counts player grants that did not land
  (waitingToLand), and the Permissions page says "in sync · 1 waits for
  their player to join" instead of plain "in sync".
- W18: Guarded Crate's kept note drops the "30 minutes" it could not know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-21 07:42:24 -05:00

373 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── Guarded Crate, with NpcSpawn (PLAN_REDESIGNS §14.3.5, protocol 14) ──────
//
// Each test is one way the feature could look right and be wrong:
//
// the clearer's name reaches a viewer below the presence audience (D376), or
// a Steam id, a position or a step's key reaches anybody but staff
// an ending with nobody clearing it reads as a win
// the step runs on a server that said it has no Guarded Crate, no NpcSpawn
// (D364), a framework that cannot run its command (D363), or no event of that
// name, or a server that has not said is refused
// the step's location is not a world call's, or the run log is not told the
// crate is kept (D373, D380)
// a refusal Guarded Crate would repeat is retried, or a crowded spot is not
// a retry of a key places a second crate
// the tally's crates cleared is dropped, or Warden's column and sort are missing
// a trigger declares a variable its emitter never fills, names a Steam id, or
// fires for a crate nobody cleared
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
/** Installs a core whose `db.query` records every statement and answers `rows`. */
function withRecorder(rows = []) {
const statements = []
const ctx = fakeCtx({
db: {
query: (sql, params = []) => {
statements.push({ sql, params })
return Promise.resolve(typeof rows === 'function' ? rows(sql, params) : rows)
},
pool: {},
},
})
require('../core')._reset()
require('../core').init(ctx)
return { ctx, statements, touching: (table) => statements.filter((s) => s.sql.includes(table)) }
}
withRecorder()
// The stored status is the whole answer in these tests: nothing asks a
// sidecar before a refusal (W7, W11). liveStatus.test.js covers the ask.
require('../liveStatus').refresh = async () => false
const gc = require('../guardedCrate')
const { gridOf } = require('../worldEvents')
const { TRIGGERS } = require('../engagement/triggers')
const A = '76561198000000001'
const NOW = Date.now()
const GEOMETRY = { worldSize: 6000, gridCells: 41, gridCellSize: 146.34146341463415 }
const frame = (kind, over = {}) => ({ kind, type: 'event', t: NOW, serverId: 'main', wipeId: 'w1', x: -604, z: -342, ...over })
const cleared = (over = {}) => frame('gc.ended', { steamId: A, name: 'Alice', ...over })
const REPORT = { loaded: true, version: '2.0.17', npcSpawn: { loaded: true, version: '3.4.8' }, events: ['Easy', 'Medium', 'Hard', 'Elite'] }
// ── Who may read what (D353, D376) ─────────────────────────────────────────
test('the public reads a guarded crate’s grid, never its position or the step’s key', () => {
const out = gc.shapeFor(frame('gc.started', { key: '7:1' }), { geometry: GEOMETRY })
assert.strictEqual(out.grid, gridOf(GEOMETRY, -604, -342))
assert.strictEqual(out.x, undefined)
assert.strictEqual(out.z, undefined)
assert.strictEqual(out.key, undefined)
assert.strictEqual(out.kind, 'gc.started')
})
test('a clearing names its clearer only to the presence audience, and the Steam id only to staff', () => {
const signedOut = gc.shapeFor(cleared(), { geometry: GEOMETRY })
assert.strictEqual(signedOut.won, true, 'that somebody cleared it names nobody')
assert.strictEqual(signedOut.name, undefined)
assert.strictEqual(signedOut.steamId, undefined)
const member = gc.shapeFor(cleared(), { presence: true, geometry: GEOMETRY })
assert.strictEqual(member.name, 'Alice')
assert.strictEqual(member.steamId, undefined)
const staff = gc.shapeFor(cleared({ key: '7:1' }), { staff: true, geometry: GEOMETRY })
assert.strictEqual(staff.name, 'Alice')
assert.strictEqual(staff.steamId, A)
assert.strictEqual(staff.x, -604)
assert.strictEqual(staff.key, '7:1')
})
test('an ending with nobody clearing it is not a win, for anybody', () => {
for (const view of [{}, { presence: true }, { staff: true }]) {
const out = gc.shapeFor(frame('gc.ended'), { ...view, geometry: GEOMETRY })
assert.strictEqual(out.won, false)
assert.strictEqual(out.name, undefined)
assert.strictEqual(out.steamId, undefined)
}
// A name without a Steam id is not a clearer either.
assert.strictEqual(gc.shapeFor(frame('gc.ended', { name: 'Alice' }), { staff: true }).name, undefined)
const other = { kind: 'player.death', x: 1 }
assert.strictEqual(gc.shapeFor(other, {}), other)
})
test('the feed reads a stored clearing for its viewer, the row’s own Steam id staff’s', async () => {
withRecorder((sql) => {
if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'gc.ended', t: NOW, wipeId: 'w1', steamId: A, raw: JSON.stringify(cleared()) }]
if (sql.includes('FROM rust_map_images')) return [{ serverId: 'main', ...GEOMETRY }]
return []
})
const events = require('../model/events/events.model')
const [signedOut] = await events.recent({ serverId: 'main', presence: false })
assert.strictEqual(signedOut.steamId, null)
assert.strictEqual(signedOut.frame.name, undefined)
assert.strictEqual(signedOut.frame.won, true)
assert.ok(signedOut.frame.grid)
const [staff] = await events.recent({ serverId: 'main', admin: true })
assert.strictEqual(staff.steamId, A)
assert.strictEqual(staff.frame.name, 'Alice')
})
test('a server’s report is read leniently: what it never said is null', () => {
assert.deepStrictEqual(gc.reportOf(null), { guardedCrate: null, consolePlugins: null })
assert.deepStrictEqual(gc.reportOf({ guardedCrate: JSON.stringify(REPORT), console: '{"plugins":false}' }), {
guardedCrate: REPORT,
consolePlugins: false,
})
// A plugin that could not read the data file sends no events; NpcSpawn unsaid is null.
assert.deepStrictEqual(gc.reportOf({ guardedCrate: '{"loaded":true,"version":"2.0.17"}', console: null }).guardedCrate, {
loaded: true,
version: '2.0.17',
npcSpawn: null,
events: null,
})
assert.deepStrictEqual(gc.reportOf({ guardedCrate: 'not json', console: '{}' }), { guardedCrate: null, consolePlugins: null })
})
// ── The step ───────────────────────────────────────────────────────────────
const SERVERS = {
main: { id: 'main', name: 'Main', enabled: 1, sidecarBaseUrl: 'http://main:1' },
pve: { id: 'pve', name: 'PvE', enabled: 1, sidecarBaseUrl: 'http://pve:1' },
}
/** The step against servers `main` and `pve`, whose last reports are `reports`, and a sidecar answering `reply`. */
function stubPlace(t, { reports = { main: { guardedCrate: REPORT, console: { plugins: true } } }, reply } = {}) {
const row = (id) => {
const r = reports[id]
return r ? { serverId: id, name: SERVERS[id].name, guardedCrate: JSON.stringify(r.guardedCrate ?? null), console: JSON.stringify(r.console ?? null) } : null
}
withRecorder((sql, params) => {
if (sql.includes('JOIN rust_server_state')) return Object.keys(reports).map(row)
if (sql.includes('FROM rust_server_state')) return row(params[0]) ? [row(params[0])] : []
if (sql.includes('FROM rust_map_images')) return [{ serverId: 'main', ...GEOMETRY }]
return []
})
const client = require('../sidecarClient')
const serversDb = require('../model/servers/servers.db')
const saved = { gcPlace: client.gcPlace, getServer: serversDb.getServer }
const calls = []
serversDb.getServer = async (id) => SERVERS[id] || null
client.gcPlace = async (server, body) => {
calls.push({ server: server.id, body })
return typeof reply === 'function' ? reply(body) : reply
}
t.after(() => {
client.gcPlace = saved.gcPlace
serversDb.getServer = saved.getServer
})
return calls
}
const module_ = () => require('../eventGuardedCrate')
const placeStep = () => module_().ACTIONS[0]
const ok = (data) => ({ ok: true, status: 'ok', data })
const placed = (over = {}) => ok({ kind: 'gc.placed', key: '7:1', event: 'Elite', repeat: false, x: -604, z: -342, ...over })
test('the step is kept, priced one crate each, and has no revert (D373)', () => {
const step = placeStep()
assert.strictEqual(step.id, 'rust.gc.place')
assert.strictEqual(step.reversible, 'none')
assert.strictEqual(step.risk, 'change')
assert.strictEqual(step.revert, undefined)
assert.deepStrictEqual(step.cost({ event: 'Elite', monument: 'main/powerplant_1' }), { 'rust.gc.crates': 1 })
assert.match(step.description, /kept when the run ends/)
})
test('the event list is every server’s own, once each, and Guarded Crate’s defaults where none has said', async (t) => {
const [events] = module_().OPTION_SOURCES
stubPlace(t, {
reports: {
main: { guardedCrate: REPORT, console: { plugins: true } },
pve: { guardedCrate: { ...REPORT, events: ['easy', 'Boss Drop'] }, console: { plugins: true } },
},
})
assert.deepStrictEqual(await events.resolve(), [
{ value: 'Easy', label: 'Easy — Main, PvE' },
{ value: 'Medium', label: 'Medium — Main' },
{ value: 'Hard', label: 'Hard — Main' },
{ value: 'Elite', label: 'Elite — Main' },
{ value: 'Boss Drop', label: 'Boss Drop — PvE' },
])
stubPlace(t, { reports: { main: { guardedCrate: { loaded: false }, console: null } } })
assert.deepStrictEqual((await events.resolve()).map((e) => e.value), ['Easy', 'Medium', 'Hard', 'Elite'])
})
test('the dry run refuses what the server said it cannot do, and nothing else (D364)', async (t) => {
const run = (params) => placeStep().perform({ runId: 7, idempotencyKey: '7:1', params, verify: true })
const at = { event: 'Elite', monument: 'main/powerplant_1' }
stubPlace(t, { reports: { main: { guardedCrate: { loaded: false }, console: { plugins: true } } } })
const absent = await run(at)
assert.strictEqual(absent.retry, false)
assert.match(absent.error, /Guarded Crate is not installed on Main/)
stubPlace(t, { reports: { main: { guardedCrate: { ...REPORT, npcSpawn: { loaded: false } }, console: { plugins: true } } } })
const noGuards = await run(at)
assert.strictEqual(noGuards.retry, false)
assert.strictEqual(noGuards.error, 'Guarded Crate is loaded on Main but NpcSpawn is missing: its events cannot start')
stubPlace(t, { reports: { main: { guardedCrate: REPORT, console: { plugins: false } } } })
const carbon = await run(at)
assert.match(carbon.error, /cannot run Guarded Crate's console command/)
assert.match(carbon.error, /Rust-Plugins#34/)
const calls = stubPlace(t)
const unknown = await run({ ...at, event: 'Legendary' })
assert.strictEqual(unknown.retry, false)
assert.match(unknown.error, /"Legendary" is not one of Guarded Crate's events on Main \(Easy, Medium, Hard, Elite\)/)
assert.ok((await run({ ...at, event: 'elite' })).ok, 'a name is found ignoring case, as Guarded Crate finds it')
assert.match((await run({ monument: 'main/powerplant_1' })).error, /which of Guarded Crate's events/)
assert.match((await run({ event: 'Elite', x: 1, z: 2 })).error, /which server/)
assert.match((await run({ event: 'Elite', monument: 'main/powerplant_1', x: 1, z: 2 })).error, /exactly one of them/)
assert.strictEqual(calls.length, 0, 'the dry run places nothing')
// A plugin before protocol 14, or a server never reached, has not said: the plugin answers.
stubPlace(t, { reports: {} })
assert.ok((await run(at)).ok)
// Nor does a report that could not read the event names refuse a name.
stubPlace(t, { reports: { main: { guardedCrate: { ...REPORT, events: undefined }, console: { plugins: true } } } })
assert.ok((await run({ ...at, event: 'Boss Drop' })).ok)
})
test('a placement sends the key, the event and a world call’s location, and tells the run log the crate is kept', async (t) => {
const calls = stubPlace(t, { reply: placed() })
const result = await placeStep().perform({
runId: 7,
idempotencyKey: '7:1',
params: { event: 'Elite', monument: 'main/powerplant_1', offsetX: 10, offsetZ: -5, y: 40 },
})
assert.ok(result.ok)
assert.deepStrictEqual(calls, [{ server: 'main', body: { key: '7:1', event: 'Elite', monument: 'powerplant_1', offsetX: 10, offsetZ: -5 } }])
assert.strictEqual(result.detail.event, 'Elite')
assert.strictEqual(result.detail.grid, gridOf(GEOMETRY, -604, -342))
assert.strictEqual(result.detail.note, `The crate is ${module_().KEPT}`)
const byPoint = stubPlace(t, { reply: placed() })
await placeStep().perform({ runId: 7, idempotencyKey: '7:2', params: { event: 'Easy', server: 'main', x: -604, z: -342 } })
assert.deepStrictEqual(byPoint[0].body, { key: '7:2', event: 'Easy', x: -604, z: -342 })
})
test('a crowded spot and a game that is down are retried; a refusal Guarded Crate would repeat is not', async (t) => {
stubPlace(t, {
reply: (body) => ok(body.event === 'Hard'
? { kind: 'gc.error', reason: 'gc-too-close', message: 'another guarded crate is running within 80 m of that spot' }
: { kind: 'gc.error', reason: 'npcspawn-missing', message: 'Guarded Crate is loaded but NpcSpawn is missing: its events cannot start' }),
})
const step = placeStep()
const params = (event) => ({ event, monument: 'main/powerplant_1' })
const crowded = await step.perform({ runId: 7, idempotencyKey: '7:3', params: params('Hard') })
assert.strictEqual(crowded.ok, false)
assert.strictEqual(crowded.retry, undefined)
assert.match(crowded.error, /within 80 m/)
const noGuards = await step.perform({ runId: 7, idempotencyKey: '7:4', params: params('Easy') })
assert.strictEqual(noGuards.retry, false)
assert.match(noGuards.error, /NpcSpawn is missing/)
for (const reason of ['gc-missing', 'npcspawn-missing', 'console-unavailable', 'bad-event', 'bad-position', 'refused', 'no-monument', 'off-map', 'malformed', 'events-disabled']) {
assert.ok(module_().PERMANENT.has(reason), reason)
}
assert.ok(!module_().PERMANENT.has('gc-too-close'))
stubPlace(t, { reply: { ok: false, status: 'http-503' } })
const down = await step.perform({ runId: 7, idempotencyKey: '7:5', params: params('Elite') })
assert.strictEqual(down.retry, undefined)
assert.match(down.error, /has no game connected/)
})
test('a repeat of a key places nothing new, and says so beside the crate being kept', async (t) => {
stubPlace(t, { reply: placed({ repeat: true }) })
const again = await placeStep().perform({ runId: 7, idempotencyKey: '7:1', params: { event: 'Elite', monument: 'main/powerplant_1' } })
assert.ok(again.ok)
assert.strictEqual(again.detail.repeat, true)
assert.match(again.detail.note, /^answered from the first attempt; nothing new was placed\. The crate is kept/)
})
// ── Warden (D375) ──────────────────────────────────────────────────────────
test('the tally’s crates cleared is added up', async () => {
const rec = withRecorder()
const { apply } = require('../ingest')
await apply('main', { id: 1, kind: 'player.tally', frame: { kind: 'player.tally', type: 'event', t: NOW, serverId: 'main', wipeId: 'w1', steamId: A, name: 'Alice', guardedWon: 2 } })
const titleAdd = rec.touching('rust_player_wipe_stats').find((s) => s.sql.includes('guarded_won'))
assert.ok(titleAdd, 'the tally reached the title columns')
const cols = titleAdd.sql.match(/\(server_id, wipe_id, steam_id, ([^)]+)\)/)[1].split(', ')
assert.strictEqual(titleAdd.params[3 + cols.indexOf('guarded_won')], 2)
})
test('the leaderboard sorts by crates cleared and carries the count on every row', async () => {
const rec = withRecorder((sql) => (sql.includes('FROM rust_player_wipe_stats') ? [{ steamId: A, name: 'Alice', kills: 1, guardedWon: '3' }] : []))
const events = require('../model/events/events.model')
const [row] = await events.leaderboard({ serverId: 'main', wipeId: 'w1', sort: 'guardedWon' })
assert.strictEqual(row.guardedWon, 3)
const board = rec.touching('rust_player_wipe_stats').at(-1)
assert.match(board.sql, /SUM\(s\.guarded_won\) AS guardedWon/)
assert.match(board.sql, /ORDER BY[^]*guarded_won/)
})
// ── Triggers (D355, §14.3.9) ───────────────────────────────────────────────
test('a landing and a clearing are public triggers, neither declaring a Steam id', () => {
const found = TRIGGERS.filter((t) => t.id.startsWith('rust.gc.'))
assert.deepStrictEqual(found.map((t) => t.id).sort(), ['rust.gc.ended', 'rust.gc.started'])
for (const t of found) {
assert.strictEqual(t.ceiling, 'everyone', t.id)
assert.strictEqual(t.subjectKey, 'subject', t.id)
assert.ok(!t.variables.some((v) => /steam/i.test(v.name)), `${t.id} declares a Steam id`)
}
})
test('a landing and a clearing fire with what they declare; nobody clearing it and a timeout fire nothing', async () => {
const { ctx } = withRecorder((sql) => (sql.includes('FROM rust_map_images') ? [{ serverId: 'main', ...GEOMETRY }] : []))
const emit = require('../engagement/emit')
emit.reset()
const server = { id: 'main', name: 'Main' }
await emit.onEvent(server, { id: 1, kind: 'gc.started', frame: frame('gc.started', { key: '7:1' }) })
await emit.onEvent(server, { id: 2, kind: 'gc.ended', frame: cleared() })
await emit.onEvent(server, { id: 3, kind: 'gc.ended', frame: frame('gc.ended') })
await emit.onEvent(server, { id: 4, kind: 'gc.failed', frame: frame('gc.failed') })
const calls = ctx.events.emit.calls
assert.strictEqual(calls.length, 2)
const [startedId, started] = calls[0]
assert.strictEqual(startedId, 'rust.gc.started')
assert.strictEqual(started.data.byEvent, true)
assert.strictEqual(started.data.subject, 'main:gc.started')
assert.match(started.data.intro, /A guarded crate landed at [A-Z]+\d+ on Main\./)
const [endedId, ended] = calls[1]
assert.strictEqual(endedId, 'rust.gc.ended')
assert.strictEqual(ended.data.player, 'Alice')
assert.match(ended.data.intro, /Alice cleared the guarded crate at [A-Z]+\d+ on Main\./)
for (const [id, envelope] of calls) {
const declared = new Set(TRIGGERS.find((x) => x.id === id).variables.map((v) => v.name))
for (const name of Object.keys(envelope.data)) assert.ok(declared.has(name), `${id} fills ${name}, which it does not declare`)
}
})
test('an old crate tells nobody (D63)', async () => {
const { ctx } = withRecorder()
const emit = require('../engagement/emit')
emit.reset()
await emit.onEvent({ id: 'main', name: 'Main' }, { id: 1, kind: 'gc.ended', frame: cleared({ t: NOW - 3 * 3600 * 1000 }) })
assert.strictEqual(ctx.events.emit.calls.length, 0)
})