// ── 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) })