Files
Module-Rust/server/test/commandSteps.test.js
wtclaude 3741c954f5
All checks were successful
PR Checks / client-build (pull_request) Successful in 34s
PR Checks / frozen-manifest (pull_request) Successful in 1m3s
PR Checks / server-tests (pull_request) Successful in 7m12s
fix(admin): no decision numbers in admin copy, and "1 player holds one"
Twenty-four strings an administrator reads carried an internal decision
number - "(D189)", "(D198)", "(D257, D270)" and the like: the permission
screen's group Servers tab and a chip tooltip, the event editor's step
field and budget descriptions, two option sources, a command reward's
skipped note, and five NPC profile validation errors. A D-number means
something to the plan, never to an operator. Each is removed, and the
sentence around it left as it was. The developer-facing swagger
descriptions and code comments keep theirs.

The servers page's chat-title line read "Last pushed: 1 player hold
one." It now says "holds" for one.

Two tests pinned the old wording and now assert the text instead.

Found while writing the runicgateway.com Rust docs (its PLAN.md §16,
phase 15).

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

301 lines
17 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.

// ── 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, /not on the server when the reward ran/)
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 })
})