// ── Console commands as event steps and rewards (PLAN_REDESIGNS §16, D382–D385) ── // // Each test is one way the feature could look right and be wrong: // // both steps are irreversible, on one `rust.commands` budget, and run is the // one with a revert (its teardown); the price counts commands, teardown // included, and commands × people for a reward // the commands are core's list, at most ten, 512 characters, one line each // unticked, both steps refuse in the dry run (D384's bound is the toggle) // {steamid} and {name} are refused in a step that has nobody to fill them // the run sends the key and the teardown with it (D385), files the ledger's // own copy of the teardown, and shows the console's replies // a misspelled command is permanent `unknown-command`, saying what ran before // a reward sends Steam ids (no website account needed), never a site-side // filled command, and names who was skipped as offline (D383) // a revert runs the ledger's copy once, under its own key; one with only the // key asks every enabled server to run what it kept (D385) const test = require('node:test') const assert = require('node:assert') const { fakeCtx } = require('./_fakes') require('../core')._reset() require('../core').init(fakeCtx({ db: { query: () => Promise.resolve([]), pool: {} } })) const client = require('../sidecarClient') const serversDb = require('../model/servers/servers.db') const servers = require('../model/servers/servers.model') const linksDb = require('../model/links/links.db') const rewards = require('../eventRewards') const cmds = require('../eventCommands') const ok = (data) => ({ ok: true, status: 'ok', data }) const A = '76561198000000001' const B = '76561198000000002' const C = '76561198000000003' const ROW = { id: 'main', name: 'Main', sidecarBaseUrl: 'http://main:1', sidecarTokenEnc: null, enabled: 1 } const run = cmds.ACTIONS.find((a) => a.id === 'rust.command.run') const reward = cmds.ACTIONS.find((a) => a.id === 'rust.command.reward') const person = (steamId, score, name) => ({ steamId, name: name || `p${steamId.slice(-1)}`, seconds: 60, kills: 0, score, joinedAt: 1 }) /** Every call the steps make, recorded; `answer` is what the bridge says to each. */ function stub(t, { answer = () => ok({ kind: 'command.ran', ran: 1, replies: [] }), people = [], linked = [], enabled = [ROW] } = {}) { const calls = { sent: [] } const saved = { getServer: serversDb.getServer, commandsRun: client.commandsRun, tallySnapshot: client.tallySnapshot, linkedOnServer: linksDb.linkedOnServer, listForPolling: servers.listForPolling, } serversDb.getServer = async (id) => enabled.find((s) => s.id === id) || null client.commandsRun = async (server, body) => { calls.sent.push({ server: server.id, body }) return answer(body, server) } client.tallySnapshot = async (server, runId) => ok({ kind: 'tally.snapshot', runId, people, maxRecipients: 100 }) linksDb.linkedOnServer = async () => linked servers.listForPolling = async () => enabled t.after(() => { Object.assign(serversDb, { getServer: saved.getServer }) Object.assign(client, { commandsRun: saved.commandsRun, tallySnapshot: saved.tallySnapshot }) Object.assign(linksDb, { linkedOnServer: saved.linkedOnServer }) Object.assign(servers, { listForPolling: saved.listForPolling }) }) return calls } const RUN = { server: 'main', commands: ['rbe 1', 'say a raid base is up'], teardown: ['rbe stop'], confirm: true } const REWARD = { server: 'main', recipients: 'everyone', commands: ['sr add {steamid} 100'], confirm: true } const CTX = { runId: 7, stepId: 3, idempotencyKey: 'k'.repeat(40) } // ── The declarations ─────────────────────────────────────────────────────── test('both steps are irreversible on one budget; only the run has a teardown to revert', () => { assert.deepStrictEqual([run.risk, run.reversible, typeof run.revert, typeof run.reconcile], ['irreversible', 'ledger', 'function', 'function']) assert.deepStrictEqual([reward.risk, reward.reversible, reward.revert], ['irreversible', 'none', undefined]) assert.deepStrictEqual(cmds.BUDGETS.map((b) => b.id), ['rust.commands']) // One unit per command, start and teardown together (§16.2)… assert.deepStrictEqual(run.cost(RUN), { 'rust.commands': 3 }) assert.deepStrictEqual(run.cost({ commands: ['rbe 1'] }), { 'rust.commands': 1 }) // …and commands × people for a reward, the bound for a mode that is not a count (§16.3). assert.deepStrictEqual(reward.cost({ recipients: 'top', count: 3, commands: ['a', 'b'] }), { 'rust.commands': 6 }) assert.deepStrictEqual(reward.cost({ recipients: 'everyone', commands: ['a'] }), { 'rust.commands': rewards.MAX_RECIPIENTS }) // The switchboard prices the declared example, a list of one. assert.deepStrictEqual(run.cost({ commands: run.params.find((p) => p.name === 'commands').example }), { 'rust.commands': 1 }) }) test('the commands are core\'s list of at most ten, and confirm is a required toggle', () => { for (const action of [run, reward]) { const list = action.params.find((p) => p.name === 'commands') assert.deepStrictEqual([list.type, list.multiple, list.max, list.required], ['string', true, cmds.MAX_COMMANDS, true]) assert.strictEqual(Array.isArray(list.example), false) assert.strictEqual(list.source, undefined, 'a free text box: no allowlist (D384)') const confirm = action.params.find((p) => p.name === 'confirm') assert.deepStrictEqual([confirm.type, confirm.required, confirm.example], ['boolean', true, false]) } const teardown = run.params.find((p) => p.name === 'teardown') assert.deepStrictEqual([teardown.multiple, teardown.required], [true, false]) assert.strictEqual(reward.params.find((p) => p.name === 'teardown'), undefined) }) test('a command is trimmed, one line, at most 512 characters, and eleven are refused', () => { assert.deepStrictEqual(cmds.checkCommands([' rbe 1 ', '', 'say hi']).value, ['rbe 1', 'say hi']) assert.deepStrictEqual(cmds.checkCommands('rbe 1').value, ['rbe 1']) assert.match(cmds.checkCommands([]).error, /at least one command/) assert.deepStrictEqual(cmds.checkCommands([], { required: false }).value, []) assert.match(cmds.checkCommands(['say a\nquit']).error, /one line/) assert.match(cmds.checkCommands(['x'.repeat(513)]).error, /at most 512 characters/) assert.ok(cmds.checkCommands(['x'.repeat(512)]).ok) assert.match(cmds.checkCommands(Array.from({ length: 11 }, (_, i) => `c${i}`)).error, /at most 10 commands, not 11/) }) // ── Refusals before anything is sent ─────────────────────────────────────── test('unticked, both steps refuse in the dry run, and send nothing', async (t) => { const calls = stub(t) const a = await run.perform({ ...CTX, params: { ...RUN, confirm: false }, verify: true }) assert.deepStrictEqual([a.ok, a.retry], [false, false]) assert.match(a.error, /I am sure I want to run these commands on the server/) // A string is not a tick. const b = await reward.perform({ ...CTX, params: { ...REWARD, confirm: 'true' }, verify: true }) assert.deepStrictEqual([b.ok, b.retry], [false, false]) assert.match(b.error, /for every recipient/) assert.strictEqual(calls.sent.length, 0) }) test('{steamid} and {name} are refused in a run, teardown included: nobody fills them there', async (t) => { stub(t) const a = await run.perform({ ...CTX, params: { ...RUN, commands: ['sr add {steamid} 1'] }, verify: true }) assert.deepStrictEqual([a.ok, a.retry], [false, false]) assert.match(a.error, /Reward by console command/) const b = await run.perform({ ...CTX, params: { ...RUN, teardown: ['say bye {Name}'] }, verify: true }) assert.strictEqual(b.ok, false) }) test('a dry run that passes checks the server and sends nothing', async (t) => { const calls = stub(t) assert.deepStrictEqual(await run.perform({ ...CTX, params: RUN, verify: true }), { ok: true }) assert.deepStrictEqual(await reward.perform({ ...CTX, params: REWARD, verify: true }), { ok: true }) const off = await run.perform({ ...CTX, params: { ...RUN, server: 'nowhere' }, verify: true }) assert.deepStrictEqual([off.ok, off.retry], [false, false]) assert.strictEqual(calls.sent.length, 0) }) // ── Run console commands ─────────────────────────────────────────────────── test('a run sends its key and its teardown, and files the ledger\'s own copy of the teardown (D385)', async (t) => { const calls = stub(t, { answer: () => ok({ kind: 'command.ran', ran: 2, replies: [{ command: 'rbe 1', reply: 'Raid base spawned' }, { command: 'say a raid base is up', reply: '' }] }), }) const result = await run.perform({ ...CTX, params: RUN }) assert.deepStrictEqual(calls.sent, [{ server: 'main', body: { key: CTX.idempotencyKey, commands: RUN.commands, teardown: ['rbe stop'] } }]) assert.strictEqual(result.ok, true) assert.deepStrictEqual(result.resources, [ { kind: cmds.TEARDOWN_KIND, ref: 'main:7:3', payload: { serverId: 'main', key: CTX.idempotencyKey, teardown: ['rbe stop'] } }, ]) assert.strictEqual(result.detail.ran, 2) assert.deepStrictEqual(result.detail.replies[0], { command: 'rbe 1', reply: 'Raid base spawned' }) assert.match(result.detail.teardown, /1 command/) }) test('a run with no teardown leaves nothing to undo and sends none', async (t) => { const calls = stub(t) const result = await run.perform({ ...CTX, params: { ...RUN, teardown: [] } }) assert.strictEqual(result.ok, true) assert.strictEqual(result.resources, undefined) assert.strictEqual('teardown' in calls.sent[0].body, false) }) test('a repeat is answered from the first attempt, and says so', async (t) => { stub(t, { answer: () => ok({ kind: 'command.ran', ran: 2, repeat: true, replies: [] }) }) const result = await run.perform({ ...CTX, params: RUN }) assert.strictEqual(result.ok, true) assert.strictEqual(result.detail.repeat, true) assert.match(result.detail.note, /nothing was run twice/) }) test('a misspelled command is permanent unknown-command, saying how many ran before it', async (t) => { stub(t, { answer: () => ok({ kind: 'command.error', reason: 'unknown-command', message: "the server does not know the command 'rbee'", ran: 1 }) }) const result = await run.perform({ ...CTX, params: RUN }) assert.deepStrictEqual([result.ok, result.retry], [false, false]) assert.match(result.error, /^Main: the server does not know the command 'rbee' \(1 ran before it\)$/) }) test('events switched off on the server is permanent; a world still loading and a lost answer are not', async (t) => { let answer = ok({ kind: 'world.error', reason: 'events-disabled', message: 'events are switched off on this server' }) stub(t, { answer: () => answer }) const off = await run.perform({ ...CTX, params: RUN }) assert.deepStrictEqual([off.ok, off.retry], [false, false]) answer = ok({ kind: 'world.error', reason: 'not-ready', message: 'the server is still loading its world' }) const loading = await run.perform({ ...CTX, params: RUN }) assert.deepStrictEqual([loading.ok, loading.retry], [false, undefined]) answer = { ok: false, status: 'http-504' } const lost = await run.perform({ ...CTX, params: RUN }) assert.deepStrictEqual([lost.ok, lost.retry], [false, undefined]) assert.match(lost.error, /did not answer/) }) // ── The teardown (§16.2, D385) ───────────────────────────────────────────── test('a revert runs the ledger\'s copy once, under its own key, on the server it names', async (t) => { const calls = stub(t, { answer: () => ok({ kind: 'command.ran', ran: 1, replies: [] }) }) const resource = { kind: cmds.TEARDOWN_KIND, ref: 'main:7:3', payload: { serverId: 'main', key: CTX.idempotencyKey, teardown: ['rbe stop'] } } assert.deepStrictEqual(await run.revert({ runId: 7, resources: [resource], idempotencyKey: CTX.idempotencyKey }), { ok: true }) assert.deepStrictEqual(calls.sent, [{ server: 'main', body: { key: cmds.teardownKey(CTX.idempotencyKey), commands: ['rbe stop'] } }]) }) test('a teardown the server refuses leaves the row failed and visible', async (t) => { stub(t, { answer: () => ok({ kind: 'command.error', reason: 'unknown-command', message: "the server does not know the command 'rbe'", ran: 0 }) }) const resource = { kind: cmds.TEARDOWN_KIND, ref: 'main:7:3', payload: { serverId: 'main', key: CTX.idempotencyKey, teardown: ['rbe stop'] } } const result = await run.revert({ runId: 7, resources: [resource], idempotencyKey: CTX.idempotencyKey }) assert.strictEqual(result.ok, false) assert.match(result.error, /does not know the command 'rbe'/) }) test('a revert with only the key asks every enabled server to run what it kept (D385)', async (t) => { const PVE = { ...ROW, id: 'pve', name: 'PvE' } const calls = stub(t, { enabled: [ROW, PVE], answer: () => ok({ kind: 'command.ran', ran: 0, replies: [] }) }) assert.deepStrictEqual(await run.revert({ runId: 7, resources: [], idempotencyKey: CTX.idempotencyKey }), { ok: true }) assert.deepStrictEqual(calls.sent.map((c) => c.server), ['main', 'pve']) for (const c of calls.sent) assert.deepStrictEqual(c.body, { key: cmds.teardownKey(CTX.idempotencyKey), teardownOf: CTX.idempotencyKey }) }) test('a server that cannot be asked about a lost answer keeps the row visible', async (t) => { stub(t, { answer: () => ({ ok: false, status: 'http-503' }) }) const result = await run.revert({ runId: 7, resources: [], idempotencyKey: CTX.idempotencyKey }) assert.strictEqual(result.ok, false) assert.match(result.error, /no game connected/) }) test('a teardown not yet run is always in force: the site cannot look', async () => { const resources = [{ ref: 'main:7:3' }, { ref: 'pve:7:4' }] assert.deepStrictEqual(await run.reconcile({ runId: 7, resources }), { ok: true, inForce: ['main:7:3', 'pve:7:4'] }) }) // ── Reward by console command ────────────────────────────────────────────── test('a reward sends Steam ids and names to the bridge, which fills the placeholders, not the site', async (t) => { const calls = stub(t, { people: [person(A, 5, 'Some "One"'), person(B, 0), person(C, 2)], answer: () => ok({ kind: 'command.ran', ran: 1, rewarded: 1, skipped: ['p3'], skippedCount: 1, replies: [{ command: `sr add ${A} 100`, reply: 'added', steamId: A }] }), }) const result = await reward.perform({ ...CTX, params: REWARD }) // Everyone who scored: A and C, never B. The command goes untouched. assert.deepStrictEqual(calls.sent, [{ server: 'main', body: { key: CTX.idempotencyKey, commands: ['sr add {steamid} 100'], recipients: [{ steamId: A, name: 'Some "One"' }, { steamId: C, name: 'p3' }] }, }]) assert.strictEqual(result.ok, true) assert.deepStrictEqual([result.detail.people, result.detail.rewarded], [2, 1]) assert.deepStrictEqual([result.detail.skipped, result.detail.skippedCount], [['p3'], 1]) assert.match(result.detail.skippedNote, /D383/) assert.strictEqual(result.resources, undefined) }) test('linked reads no tally and needs no website user beyond the link', async (t) => { const calls = stub(t, { linked: [{ steamId: A, userId: 1, name: 'Ann' }, { steamId: B, userId: 2, name: null }] }) const result = await reward.perform({ ...CTX, params: { ...REWARD, recipients: 'linked' } }) assert.strictEqual(result.ok, true) assert.deepStrictEqual(calls.sent[0].body.recipients, [{ steamId: A, name: 'Ann' }, { steamId: B, name: B }]) }) test('nobody qualifying runs nothing, and the bridge is not asked', async (t) => { const calls = stub(t, { people: [person(A, 0)] }) const result = await reward.perform({ ...CTX, params: REWARD }) assert.strictEqual(result.ok, true) assert.strictEqual(result.detail.ran, 0) assert.strictEqual(calls.sent.length, 0) }) test('more than a hundred is refused before anything runs', async (t) => { const many = Array.from({ length: 101 }, (_, i) => ({ steamId: String(76561198000001000n + BigInt(i)), userId: i + 1, name: `n${i}` })) const calls = stub(t, { linked: many }) const result = await reward.perform({ ...CTX, params: { ...REWARD, recipients: 'linked' } }) assert.deepStrictEqual([result.ok, result.retry], [false, false]) assert.match(result.error, /more than 100 linked players/) assert.strictEqual(calls.sent.length, 0) const top = await reward.perform({ ...CTX, params: { ...REWARD, recipients: 'top', count: 101 }, verify: true }) assert.strictEqual(top.ok, false) }) test('the run log shows a bounded, plain view of the answer', () => { const detail = cmds.replyDetail({ ran: '3', replies: [{ command: 'a', reply: 5 }], skipped: ['x'], skippedCount: 4, repeat: true }) assert.deepStrictEqual(detail.replies, [{ command: 'a', reply: '5' }]) assert.deepStrictEqual([detail.ran, detail.skippedCount, detail.repeat], [3, 4, true]) assert.deepStrictEqual(cmds.replyDetail({}), { ran: 0 }) })