Files
Module-Rust/server/test/guardedCrate.test.js
wtclaude da1d74bdff
All checks were successful
PR Checks / client-build (pull_request) Successful in 30s
PR Checks / server-tests (pull_request) Successful in 34s
PR Checks / frozen-manifest (pull_request) Successful in -1m34s
feat(rust): Guarded Crate — feed, map, Warden, Place a guarded crate (PLAN_REDESIGNS §14.3.5)
Stage 6 of the §14.3 integration plan, on protocol 14. Spec: docs
rust-link/PROTOCOL.md §20.9.

- gc.started and gc.ended public, gc.failed staff's; guardedCrate.js reads
  each row per viewer (the clearer's name for the presence audience, Steam id,
  position and key for staff, `won` for everyone).
- Triggers rust.gc.started and rust.gc.ended (a crate a player cleared only).
- The step rust.gc.place: an event from rust.options.gc_events and a world
  call's location; refused at verify for Guarded Crate absent, NpcSpawn missing
  (D364), console unavailable, or an unknown event. Kept (D373); the run log's
  success line says so (D380). Budget rust.gc.crates.
- Warden (D375): guarded_won, title category guardedwon, Crates cleared column.
- Map marker, server settings line, swagger, engagement manifest, bundle, README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 12:26:50 -05:00

369 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()
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)
})