Files
Module-Rust/server/test/backpacks.test.js
wtclaude 3aa142bf06
All checks were successful
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 32s
PR Checks / frozen-manifest (pull_request) Successful in 44s
fix(rust): the last fixes' small ones (W3 W6 W7/W11 W8 W10 W13-W18)
Stage 2 of PLAN_LAST_FIXES (docs#366).

- W7/W11: an "is it loaded" refusal is confirmed live before it is given.
  New liveStatus.js asks the sidecar once (boot.refreshOne, as a poll tick
  would), at most once per server per 5 s and waiting at most 3 s, then
  reads the stored status again. Used by Economics (step and staff page),
  Backpacks, Dangerous Treasures, Guarded Crate, RaidableBases, ZoneDomes,
  TruePVE and RunicNPC.
- W3: a world call answered `already-out` (the bridge half comes in stage
  3) succeeds as called 0, saying a Travelling Vendor is already out.
- W6: "A locked crate dropped by a Chinook is being hacked."
- W8: a RaidableBases profile's audit row names RaidableBases, not "@data".
- W10: the backpack sizes picker lists each size once, naming its servers.
- W13: a RaidableBases name both servers describe differently shows each
  server's own description.
- W14: each RaidableBases note after the first starts its own sentence.
- W15: a staff deposit refused at Economics' limit answers 409.
- W16: a payment's failedNote explains only the reasons that happened.
- W17: the sync state counts player grants that did not land
  (waitingToLand), and the Permissions page says "in sync · 1 waits for
  their player to join" instead of plain "in sync".
- W18: Guarded Crate's kept note drops the "30 minutes" it could not know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-21 07:42:24 -05:00

335 lines
16 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.

// ── Backpacks (PLAN_REDESIGNS §11.3.2, protocol 14, D337, D338, D340, D341) ─
//
// Each test is one way the feature could look right and be wrong:
//
// the sizes a step may lease skip the smallest, which everybody holds
// a server without Backpacks is refused with the reason, in the dry run too (D199)
// a size the server does not offer is refused by name
// the lease is a kit reward's row: the size's permission, no kit, no credit (D337)
// an empty message is the take-back warning, never silence (D338)
// the revert deletes the step's rows and marks the server for a push
// a read answers what the game said, live; a stranger's id never reaches it
// a stack keeps its name, blueprint, skin as text, and one level of contents
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
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 { statements }
}
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 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 permDb = require('../model/permissions/permissions.db')
const backpacksDb = require('../model/backpacks/backpacks.db')
const backpacks = require('../model/backpacks/backpacks.model')
const rewards = require('../eventRewards')
const ok = (data) => ({ ok: true, status: 'ok', data })
const A = '76561198000000001'
const B = '76561198000000002'
const C = '76561198000000003'
const SIZES = [6, 12, 18, 24, 30, 36, 42, 48, 96, 144]
// ── What a server reports ──────────────────────────────────────────────────
test('a server’s sizes are whole, ascending and unique; the smallest is never leased', () => {
assert.deepStrictEqual(backpacks.shape(null), { loaded: false, sizes: [] })
assert.deepStrictEqual(backpacks.shape(JSON.stringify({ loaded: false })), { loaded: false, sizes: [] })
const report = backpacks.shape(JSON.stringify({ loaded: true, version: '3.17.8', sizes: [48, 6, 12, 12, 'x', -3, 2.5] }))
assert.deepStrictEqual(report, { loaded: true, version: '3.17.8', sizes: [6, 12, 48] })
assert.deepStrictEqual(backpacks.leasable(report), [12, 48])
assert.deepStrictEqual(backpacks.leasable({ loaded: false, sizes: [] }), [])
})
// ── The step ───────────────────────────────────────────────────────────────
const ROW = { id: 'main', name: 'Main', sidecarBaseUrl: 'http://main:1', sidecarTokenEnc: null, enabled: 1 }
const lend = rewards.ACTIONS.find((a) => a.id === 'rust.backpack.size')
function stub(t, { loaded = true, sizes = SIZES, people = [], links = {}, existing = [] } = {}) {
const calls = { inserted: [], dirty: [], deleted: [] }
const saved = {
getServer: serversDb.getServer,
reportFor: backpacksDb.reportFor,
tallySnapshot: client.tallySnapshot,
userIdsForSteamIds: linksDb.userIdsForSteamIds,
listRunGrantsForStep: permDb.listRunGrantsForStep,
insertRunGrants: permDb.insertRunGrants,
markDirty: permDb.markDirty,
deleteRunGrantsForStep: permDb.deleteRunGrantsForStep,
}
serversDb.getServer = async (id) => (id === 'main' ? ROW : null)
backpacksDb.reportFor = async () => ({ serverId: 'main', backpacks: loaded ? JSON.stringify({ loaded: true, version: '3.17.8', sizes }) : null })
client.tallySnapshot = async (server, runId) => ok({ kind: 'tally.snapshot', runId, people, maxRecipients: 100 })
linksDb.userIdsForSteamIds = async (ids) => ids.filter((id) => links[id]).map((id) => ({ steamId: id, userId: links[id] }))
permDb.listRunGrantsForStep = async () => existing
permDb.insertRunGrants = async (rows) => {
calls.inserted.push(...rows)
return rows.length
}
permDb.markDirty = async (serverId) => calls.dirty.push(serverId)
permDb.deleteRunGrantsForStep = async (runId, stepId) => {
calls.deleted.push(`${runId}:${stepId}`)
return ['main']
}
t.after(() => Object.assign(serversDb, { getServer: saved.getServer }))
t.after(() => Object.assign(backpacksDb, { reportFor: saved.reportFor }))
t.after(() => Object.assign(client, { tallySnapshot: saved.tallySnapshot }))
t.after(() => Object.assign(linksDb, { userIdsForSteamIds: saved.userIdsForSteamIds }))
t.after(() =>
Object.assign(permDb, {
listRunGrantsForStep: saved.listRunGrantsForStep,
insertRunGrants: saved.insertRunGrants,
markDirty: saved.markDirty,
deleteRunGrantsForStep: saved.deleteRunGrantsForStep,
}),
)
return calls
}
const PARAMS = { server: 'main', size: 48, recipients: 'everyone' }
const person = (steamId, score) => ({ steamId, name: `p${steamId.slice(-1)}`, seconds: 60, kills: 0, score, joinedAt: 1 })
test('a bigger backpack is a ledgered change, priced per recipient on its own budget', () => {
assert.strictEqual(lend.risk, 'change')
assert.strictEqual(lend.reversible, 'ledger')
assert.deepStrictEqual(lend.cost({ recipients: 'top', count: 3 }), { 'rust.backpacks': 3 })
assert.deepStrictEqual(lend.cost({ recipients: 'linked' }), { 'rust.backpacks': rewards.MAX_RECIPIENTS })
})
test('a server without Backpacks is refused in the dry run, with the reason (D199)', async (t) => {
stub(t, { loaded: false })
const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: PARAMS, verify: true })
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, false)
assert.match(result.error, /Backpacks is not installed on this server \(Main\)/)
})
test('a size the server does not offer, or its smallest, is refused by name', async (t) => {
stub(t, { sizes: [6, 12, 48] })
for (const size of [6, 96, 0, 'big']) {
const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k', params: { ...PARAMS, size }, verify: true })
assert.strictEqual(result.ok, false, String(size))
assert.strictEqual(result.retry, false)
}
const named = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k', params: { ...PARAMS, size: 96 }, verify: true })
assert.match(named.error, /offers 12, 48 slots/)
const ok48 = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k', params: PARAMS, verify: true })
assert.deepStrictEqual(ok48, { ok: true })
})
test('the lease is one row per website user: the size’s permission, no kit, no credit (D337)', async (t) => {
const calls = stub(t, {
// A and B are one person (user 1); C has linked nothing.
people: [person(A, 9), person(B, 5), person(C, 3)],
links: { [A]: 1, [B]: 1 },
})
const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: PARAMS, verify: false })
assert.strictEqual(result.ok, true)
assert.strictEqual(calls.inserted.length, 1)
const [row] = calls.inserted
assert.strictEqual(row.permission, 'backpacks.size.48')
assert.strictEqual(row.kit, '')
assert.strictEqual(row.credit, false)
assert.strictEqual(row.steamId, A)
assert.strictEqual(row.userId, 1)
assert.deepStrictEqual(calls.dirty, ['main'])
assert.deepStrictEqual(result.resources, [{ kind: 'backpack-size', ref: 'main:7:4', payload: { serverId: 'main', permission: 'backpacks.size.48' } }])
assert.strictEqual(result.detail.lent, 1)
assert.deepStrictEqual(result.detail.missed, ['p3'])
})
test('an empty message is the take-back warning, with the size in it (D338)', async (t) => {
const calls = stub(t, { people: [person(A, 1)], links: { [A]: 1 } })
await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: { ...PARAMS, message: ' ' }, verify: false })
assert.match(calls.inserted[0].notice, /^Your backpack holds 48 until the event ends\./)
assert.strictEqual(calls.inserted[0].noticeDelivery, 'chat')
assert.strictEqual(rewards.backpackNotice({}, 48).text, rewards.BACKPACK_NOTICE)
assert.strictEqual(rewards.backpackNotice({ message: 'Big bag: {size}', delivery: 'popup' }, 96).delivery, 'popup')
assert.strictEqual(rewards.backpackNotice({ message: 'x', delivery: 'carrier pigeon' }, 96).ok, false)
assert.strictEqual(rewards.backpackNotice({ message: `${'x'.repeat(254)}{size}` }, 144).ok, false)
})
test('a repeated key finds its rows and lends nothing new', async (t) => {
const calls = stub(t, { existing: [{ userId: 1 }, { userId: 2 }], people: [person(A, 1)], links: { [A]: 1 } })
const result = await lend.perform({ runId: 7, stepId: 4, idempotencyKey: 'k-1', params: PARAMS, verify: false })
assert.strictEqual(result.detail.repeat, true)
assert.strictEqual(result.detail.lent, 2)
assert.strictEqual(calls.inserted.length, 0)
})
test('the revert deletes the step’s rows, and the next sync takes the size back', async (t) => {
const calls = stub(t)
const result = await lend.revert({ runId: 7, resources: [{ kind: 'backpack-size', ref: 'main:7:4' }], idempotencyKey: 'k-1' })
assert.deepStrictEqual(result, { ok: true })
assert.deepStrictEqual(calls.deleted, ['7:4'])
assert.deepStrictEqual(calls.dirty, ['main'])
})
test('the size list offers each size above a server’s smallest once, naming every server that offers it (W10)', async (t) => {
const saved = { listForPolling: servers.listForPolling, reports: backpacksDb.reports }
t.after(() => Object.assign(servers, { listForPolling: saved.listForPolling }) && Object.assign(backpacksDb, { reports: saved.reports }))
servers.listForPolling = async () => [{ id: 'main', name: 'Main' }, { id: 'pve', name: 'PvE' }, { id: 'off', name: 'Off' }]
backpacksDb.reports = async () => [
{ serverId: 'main', backpacks: JSON.stringify({ loaded: true, sizes: [6, 48, 96] }) },
{ serverId: 'pve', backpacks: JSON.stringify({ loaded: true, sizes: [12, 24, 48] }) },
{ serverId: 'off', backpacks: null },
]
const source = rewards.OPTION_SOURCES.find((s) => s.id === 'rust.options.backpacksizes')
assert.deepStrictEqual(await source.resolve(), [
{ value: 24, label: '24 slots — PvE' },
{ value: 48, label: '48 slots — Main, PvE' },
{ value: 96, label: '96 slots — Main' },
])
})
// ── Reading one ────────────────────────────────────────────────────────────
function stubRead(t, answer) {
const sent = []
const saved = { getForCalling: servers.getForCalling, backpacksRead: client.backpacksRead }
t.after(() => Object.assign(servers, { getForCalling: saved.getForCalling }) && Object.assign(client, { backpacksRead: saved.backpacksRead }))
servers.getForCalling = async (id) => (id === 'main' ? { id: 'main', name: 'Main', baseUrl: 'http://main:1', token: 't' } : null)
client.backpacksRead = async (server, steamId) => {
sent.push(steamId)
return typeof answer === 'function' ? answer(steamId) : answer
}
return sent
}
test('a backpack is read live and reduced to what the grid draws', async (t) => {
const sent = stubRead(
t,
ok({
kind: 'backpacks.contents',
steamId: A,
exists: true,
capacity: 12,
pageSize: 48,
items: [
{ slot: 0, shortname: 'wood', display: 'Wood', amount: 1000 },
{ slot: 2, shortname: 'rifle.ak', display: 'Assault Rifle', amount: 1, contents: [{ slot: 0, shortname: 'weapon.mod.lasersight', display: 'Weapon Lasersight', amount: 1, contents: [{ slot: 0 }] }] },
{ slot: 3, shortname: 'hoodie', display: 'Hoodie', amount: 1, skin: 10129 },
{ slot: 5, shortname: 'blueprintbase', display: 'Assault Rifle', blueprint: 'rifle.ak', amount: 1 },
{ slot: 14, shortname: 'note', display: 'Note', amount: 1, name: 'spike-a', overflow: true },
{ slot: 15, shortname: null, itemId: 999, amount: 1, overflow: true },
],
}),
)
const result = await backpacks.read({ serverId: 'main', steamId: A })
assert.deepStrictEqual(sent, [A])
assert.strictEqual(result.ok, true)
const bp = result.backpack
assert.strictEqual(bp.server, 'Main')
assert.strictEqual(bp.capacity, 12)
assert.strictEqual(bp.pageSize, 48)
assert.strictEqual(bp.items.length, 6)
assert.strictEqual(bp.items[1].contents.length, 1)
assert.strictEqual(bp.items[1].contents[0].contents, undefined, 'one level of contents, no deeper')
assert.strictEqual(bp.items[2].skin, '10129', 'a skin id is text')
assert.strictEqual(bp.items[3].blueprint, 'rifle.ak')
assert.strictEqual(bp.items[4].name, 'spike-a')
assert.strictEqual(bp.items[4].overflow, true)
assert.strictEqual(bp.items[5].itemId, 999)
})
test('a player who never had a backpack is exists: false, with no capacity', async (t) => {
stubRead(t, ok({ kind: 'backpacks.contents', steamId: A, exists: false }))
const result = await backpacks.read({ serverId: 'main', steamId: A })
assert.deepStrictEqual(result.backpack, { serverId: 'main', server: 'Main', steamId: A, exists: false })
})
test('a read says why it has nothing: no server, no Backpacks, or no answer', async (t) => {
stubRead(t, ok({ kind: 'backpacks.error', reason: 'backpacks-missing', message: 'Backpacks is not installed on this server' }))
const missing = await backpacks.read({ serverId: 'main', steamId: A })
assert.strictEqual(missing.status, 409)
assert.strictEqual(missing.message, 'Backpacks is not installed on this server (Main)')
assert.strictEqual((await backpacks.read({ serverId: 'nope', steamId: A })).status, 404)
client.backpacksRead = async () => ({ ok: false, status: 'http-503' })
const down = await backpacks.read({ serverId: 'main', steamId: A })
assert.strictEqual(down.status, 503)
assert.match(down.message, /Main has no game connected/)
})
// ── The routes ─────────────────────────────────────────────────────────────
function res() {
return {
statusCode: 200,
body: null,
status(code) {
this.statusCode = code
return this
},
json(body) {
this.body = body
return this
},
}
}
test('a backpack is read only for an account its user has linked', async (t) => {
const links = require('../model/links/links.model')
const saved = { listForUser: links.listForUser }
t.after(() => Object.assign(links, saved))
links.listForUser = async (userId) => (userId === 1 ? [{ steamId: A, name: 'pa' }] : [])
const sent = stubRead(t, ok({ kind: 'backpacks.contents', steamId: A, exists: false }))
const player = require('../router/player/rust.controller')
const staff = require('../router/admin/usersRust.controller')
const own = res()
await player.ownBackpack({ user: { id: 1 }, query: { serverId: 'main', steamId: A } }, own)
assert.strictEqual(own.statusCode, 200)
assert.strictEqual(own.body.exists, false)
const stranger = res()
await player.ownBackpack({ user: { id: 1 }, query: { serverId: 'main', steamId: B } }, stranger)
assert.strictEqual(stranger.statusCode, 404)
const wrongUser = res()
await staff.readBackpack({ params: { id: '2' }, query: { serverId: 'main', steamId: A } }, wrongUser)
assert.strictEqual(wrongUser.statusCode, 404)
const viaStaff = res()
await staff.readBackpack({ params: { id: '1' }, query: { serverId: 'main', steamId: A } }, viaStaff)
assert.strictEqual(viaStaff.statusCode, 200)
assert.deepStrictEqual(sent, [A, A], 'a stranger’s id never reaches the game')
})