// ── Dangerous Treasures (PLAN_REDESIGNS §14.3.4, protocol 14) ────────────── // // Each test is one way the feature could look right and be wrong: // // the winner's name reaches a viewer below the presence audience (D376), or a // Steam id, an invader, a position or a step's key reaches anybody but staff // the step runs on a server that said it has no DT, or whose framework cannot // run DT's console command (D363), or a server that has not said is refused // DT's warm-up is not answered with core's ten-minute retry (D365, D371), or // a refusal DT would repeat is retried // a retry of a key opens a second chest // the tally's treasures won is dropped on the floor, or Treasure Hunter's // column and sort are missing // a trigger declares a variable its emitter never fills, or names a Steam id 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 dt = require('../dangerousTreasures') const { gridOf } = require('../worldEvents') const { TRIGGERS } = require('../engagement/triggers') const A = '76561198000000001' const B = '76561198000000002' 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: 812, z: -340.5, ...over }) const won = (over = {}) => frame('dt.won', { steamId: A, name: 'Alice', invaders: [A, B], invaderCount: 2, ...over }) // ── Who may read what (D353, D376) ───────────────────────────────────────── test('the public reads a treasure event’s grid, never its position or the step’s key', () => { const out = dt.shapeFor(frame('dt.started', { key: '7:1' }), { geometry: GEOMETRY }) assert.strictEqual(out.grid, gridOf(GEOMETRY, 812, -340.5)) assert.strictEqual(out.x, undefined) assert.strictEqual(out.z, undefined) assert.strictEqual(out.key, undefined) assert.strictEqual(out.kind, 'dt.started') }) test('a win names its winner only to the presence audience, and its Steam ids only to staff', () => { const signedOut = dt.shapeFor(won(), { geometry: GEOMETRY }) assert.strictEqual(signedOut.name, undefined) assert.strictEqual(signedOut.steamId, undefined) assert.strictEqual(signedOut.invaders, undefined) assert.strictEqual(signedOut.invaderCount, 2, 'a count names nobody') const member = dt.shapeFor(won(), { presence: true, geometry: GEOMETRY }) assert.strictEqual(member.name, 'Alice') assert.strictEqual(member.steamId, undefined) assert.strictEqual(member.invaders, undefined) const staff = dt.shapeFor(won({ invaders: [A, B, 'not-an-id'] }), { staff: true, geometry: GEOMETRY }) assert.strictEqual(staff.name, 'Alice') assert.strictEqual(staff.steamId, A) assert.deepStrictEqual(staff.invaders, [A, B]) assert.strictEqual(staff.x, 812) }) test('an older or partial win still counts its invaders, and something off the map has no grid', () => { assert.strictEqual(dt.shapeFor(won({ invaderCount: undefined }), {}).invaderCount, 2) assert.strictEqual(dt.shapeFor(frame('dt.started', { x: 9000 }), { geometry: GEOMETRY }).grid, undefined) assert.strictEqual(dt.shapeFor(frame('dt.started'), {}).grid, undefined, 'no geometry, no grid') const other = { kind: 'player.death', x: 1 } assert.strictEqual(dt.shapeFor(other, {}), other) }) test('the feed reads a stored win for its viewer, the row’s own Steam id staff’s', async () => { withRecorder((sql) => { if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'dt.won', t: NOW, wipeId: 'w1', steamId: A, raw: JSON.stringify(won()) }] 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.ok(signedOut.frame.grid) const [staff] = await events.recent({ serverId: 'main', admin: true }) assert.strictEqual(staff.steamId, A) assert.deepStrictEqual(staff.frame.invaders, [A, B]) }) test('a server’s report is read leniently: what it never said is null', () => { assert.deepStrictEqual(dt.reportOf(null), { dangerousTreasures: null, consolePlugins: null }) assert.deepStrictEqual(dt.reportOf({ dangerousTreasures: '{"loaded":true,"version":"3.0.0"}', console: '{"plugins":false}' }), { dangerousTreasures: { loaded: true, version: '3.0.0' }, consolePlugins: false, }) assert.deepStrictEqual(dt.reportOf({ dangerousTreasures: 'not json', console: '{}' }), { dangerousTreasures: null, consolePlugins: null }) }) // ── The step ─────────────────────────────────────────────────────────────── /** The step against one server `main`, whose last report is `report`, and a sidecar answering `reply`. */ function stubStart(t, { report = { dangerousTreasures: { loaded: true, version: '3.0.0' }, console: { plugins: true } }, reply } = {}) { withRecorder((sql) => { if (sql.includes('FROM rust_server_state')) { return report === null ? [] : [{ dangerousTreasures: JSON.stringify(report.dangerousTreasures ?? null), console: JSON.stringify(report.console ?? null) }] } 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 = { dtStart: client.dtStart, getServer: serversDb.getServer } const calls = [] serversDb.getServer = async (id) => (id === 'main' ? { id: 'main', name: 'Main', enabled: 1, sidecarBaseUrl: 'http://main:1' } : null) client.dtStart = async (server, body) => { calls.push({ server: server.id, body }) return typeof reply === 'function' ? reply(body) : reply } t.after(() => { client.dtStart = saved.dtStart serversDb.getServer = saved.getServer }) return calls } const startStep = () => require('../eventDangerousTreasures').ACTIONS[0] const ok = (data) => ({ ok: true, status: 'ok', data }) const opened = (over = {}) => ok({ kind: 'dt.opened', key: '7:1', level: 1, repeat: false, x: 812, z: -340.5, ...over }) test('the step is kept, priced one event each, and offers DT’s three difficulties', async () => { const step = startStep() assert.strictEqual(step.id, 'rust.dt.start') assert.strictEqual(step.reversible, 'none') assert.strictEqual(step.risk, 'change') assert.strictEqual(step.revert, undefined) assert.deepStrictEqual(step.cost({ server: 'main', level: 2 }), { 'rust.dt.events': 1 }) const [levels] = require('../eventDangerousTreasures').OPTION_SOURCES assert.deepStrictEqual(await levels.resolve(), [ { value: 0, label: 'Easy' }, { value: 1, label: 'Medium' }, { value: 2, label: 'Hard' }, ]) }) test('the dry run refuses a server without DT, or whose framework cannot run its command, and nothing else', async (t) => { const run = (params) => startStep().perform({ runId: 7, idempotencyKey: '7:1', params, verify: true }) stubStart(t, { report: { dangerousTreasures: { loaded: false }, console: { plugins: true } } }) const absent = await run({ server: 'main', level: 1 }) assert.strictEqual(absent.retry, false) assert.match(absent.error, /Dangerous Treasures is not installed on Main/) stubStart(t, { report: { dangerousTreasures: { loaded: true }, console: { plugins: false } } }) const carbon = await run({ server: 'main', level: 1 }) assert.strictEqual(carbon.retry, false) assert.match(carbon.error, /cannot run Dangerous Treasures' console command/) assert.match(carbon.error, /Rust-Plugins#34/) // A plugin before protocol 14, or a server never reached, has not said: the plugin answers. stubStart(t, { report: null }) assert.ok((await run({ server: 'main', level: 1 })).ok) const calls = stubStart(t) assert.ok((await run({ server: 'main' })).ok, 'blank is Easy') assert.match((await run({ server: 'main', level: 3 })).error, /0 \(Easy\), 1 \(Medium\) or 2 \(Hard\)/) assert.match((await run({ server: 'main', level: 'hard' })).error, /not "hard"/) assert.match((await run({ level: 1 })).error, /which server/) assert.strictEqual(calls.length, 0, 'the dry run starts nothing') }) test('a start sends the key and the level, and says where it opened', async (t) => { const calls = stubStart(t, { reply: opened() }) const result = await startStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', level: '1' } }) assert.ok(result.ok) assert.deepStrictEqual(calls, [{ server: 'main', body: { key: '7:1', level: 1 } }]) assert.deepStrictEqual(result.detail, { level: 'Medium', grid: gridOf(GEOMETRY, 812, -340.5) }) }) test('DT not ready is said in the plan’s words and retried once in ten minutes (D365, D371)', async (t) => { stubStart(t, { reply: ok({ kind: 'dt.error', reason: 'dt-not-ready', message: 'Dangerous Treasures is not ready yet: …' }) }) const result = await startStep().perform({ runId: 7, idempotencyKey: '7:2', params: { server: 'main', level: 0 } }) assert.strictEqual(result.ok, false) assert.strictEqual(result.retry, true) assert.strictEqual(result.retryAfterMs, 600000) assert.strictEqual(result.error, 'Dangerous Treasures is not ready yet on Main') }) test('no spot retries on core’s ordinary delay; a refusal DT would repeat is not retried', async (t) => { stubStart(t, { reply: (body) => ok(body.level === 2 ? { kind: 'dt.error', reason: 'dt-no-spot', message: 'Dangerous Treasures found no valid spot for an event' } : { kind: 'dt.error', reason: 'refused', message: 'Dangerous Treasures did not start: Invalid difficulty' }), }) const step = startStep() const noSpot = await step.perform({ runId: 7, idempotencyKey: '7:3', params: { server: 'main', level: 2 } }) assert.strictEqual(noSpot.ok, false) assert.strictEqual(noSpot.retry, undefined) assert.strictEqual(noSpot.retryAfterMs, undefined) assert.match(noSpot.error, /no valid spot/) const refused = await step.perform({ runId: 7, idempotencyKey: '7:4', params: { server: 'main', level: 1 } }) assert.strictEqual(refused.retry, false) assert.match(refused.error, /Invalid difficulty/) for (const reason of ['dt-missing', 'console-unavailable', 'bad-level', 'malformed', 'events-disabled']) { assert.ok(require('../eventDangerousTreasures').PERMANENT.has(reason), reason) } }) test('a game that is down is retried, and a repeat of a key opens nothing new', async (t) => { stubStart(t, { reply: { ok: false, status: 'http-503' } }) const down = await startStep().perform({ runId: 7, idempotencyKey: '7:5', params: { server: 'main', level: 1 } }) assert.strictEqual(down.retry, undefined) assert.match(down.error, /has no game connected/) stubStart(t, { reply: opened({ repeat: true }) }) const again = await startStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', level: 1 } }) assert.ok(again.ok) assert.strictEqual(again.detail.repeat, true) }) // ── Treasure Hunter (D375) ───────────────────────────────────────────────── test('the tally’s treasures won 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', treasuresWon: 3 } }) const titleAdd = rec.touching('rust_player_wipe_stats').find((s) => s.sql.includes('treasures_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('treasures_won')], 3) }) test('the leaderboard sorts by treasures won 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, treasuresWon: '4' }] : [])) const events = require('../model/events/events.model') const [row] = await events.leaderboard({ serverId: 'main', wipeId: 'w1', sort: 'treasuresWon' }) assert.strictEqual(row.treasuresWon, 4) const board = rec.touching('rust_player_wipe_stats').at(-1) assert.match(board.sql, /SUM\(s\.treasures_won\) AS treasuresWon/) assert.match(board.sql, /ORDER BY[^]*treasures_won/) }) // ── Triggers (D355, §14.3.9) ─────────────────────────────────────────────── test('a start and a win are public triggers, neither declaring a Steam id', () => { const found = TRIGGERS.filter((t) => t.id.startsWith('rust.dt.')) assert.deepStrictEqual(found.map((t) => t.id).sort(), ['rust.dt.started', 'rust.dt.won']) 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 start and a win fire their triggers with what they declare; an ending fires 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: 'dt.started', frame: frame('dt.started', { key: '7:1' }) }) await emit.onEvent(server, { id: 2, kind: 'dt.won', frame: won({ invaderCount: 3 }) }) await emit.onEvent(server, { id: 3, kind: 'dt.ended', frame: frame('dt.ended') }) const calls = ctx.events.emit.calls assert.strictEqual(calls.length, 2) const [startedId, started] = calls[0] assert.strictEqual(startedId, 'rust.dt.started') assert.strictEqual(started.data.byEvent, true) assert.strictEqual(started.data.subject, 'main:dt.started') assert.match(started.data.intro, /A Dangerous Treasures chest opened at [A-Z]+\d+ on Main/) const [wonId, winner] = calls[1] assert.strictEqual(wonId, 'rust.dt.won') assert.strictEqual(winner.data.player, 'Alice') assert.strictEqual(winner.data.invaders, 3) assert.match(winner.data.intro, /Alice won the treasure at [A-Z]+\d+ on Main, with 3 players fighting for it\./) 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 treasure tells nobody (D63)', async () => { const { ctx } = withRecorder() const emit = require('../engagement/emit') emit.reset() await emit.onEvent({ id: 'main', name: 'Main' }, { id: 1, kind: 'dt.won', frame: won({ t: NOW - 3 * 3600 * 1000 }) }) assert.strictEqual(ctx.events.emit.calls.length, 0) })