Files
Module-Rust/server/test/raidableBases.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

453 lines
24 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.

// ── RaidableBases (PLAN_REDESIGNS §11.4, protocol 14) ───────────────────────
//
// Each test is one way the feature could look right and be wrong:
//
// a raider's or a buyer's name reaches a viewer below the presence audience
// (D376, D390), or a Steam id, a position or a run's key reaches anybody but staff
// a difficulty, profile or base name is anything but the plugin's own (D386):
// every name below is invented, so a list written into the code cannot pass
// the step asks for a spot, a profile, a base or settings on a server without a
// patched raids helper, or refuses a plain spawn a server can still make (D348)
// a base the helper made is not the run's resource, or one it did not make is
// the dry run's spot check fails the dry run instead of advising (D391)
// the grid's wait is not a ten-minute retry, or a refusal the plugin would
// repeat is retried
// the tally's raids completed is dropped, or Warlord's column and sort are missing
// the map's raids layer is missing, not public by default, or carries the key
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 rb = require('../raidableBases')
const catalogue = require('../catalogue')
const { gridOf } = require('../worldEvents')
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: 650, z: 2037, level: 7, difficulty: 'Wobbly', pvp: true, base: 'wobble3', ...over })
const raided = (over = {}) =>
frame('raid.completed', {
raiders: [{ steamId: A, name: 'Alice' }, { steamId: B, name: 'Bob' }],
raiderCount: 2,
ownerId: A,
ownerName: 'Alice',
id: 'raid-1-1',
runId: '7',
key: '7:1',
...over,
})
// Invented, every one (D386): no edition ships these.
const PROFILE = { name: 'Wobbly Keeps', difficulty: 'Wobbly', level: 7, enabled: true, pvp: false, bases: ['wobble1', 'wobble3'], seabed: false, float: false }
const REPORT = {
loaded: true,
version: '3.2.2',
premium: true,
spawnCall: true,
copyPaste: { loaded: true, version: '4.3.0' },
difficulties: [{ name: 'Gentle', level: 0 }, { name: 'Wobbly', level: 7 }],
helper: { state: 'patched', version: '0.1.0' },
gridReady: true,
profiles: [PROFILE, { name: 'Gentle Huts', difficulty: 'Gentle', level: 0, enabled: true, pvp: true, bases: ['hut9'], seabed: false, float: false }],
}
// ── Who may read what (D353, D376, D390) ───────────────────────────────────
test('the kinds are classified: four public, the despawn staff’s', () => {
for (const kind of ['raid.started', 'raid.completed', 'raid.purchased', 'raid.locked']) assert.ok(catalogue.PUBLIC_KINDS.includes(kind), kind)
assert.ok(catalogue.STAFF_KINDS.includes('raid.ended'))
assert.ok(!catalogue.PUBLIC_KINDS.includes('raid.ended'))
})
test('the public reads a base’s difficulty, base, PvP and grid as the plugin named them, never its position or a run’s key', () => {
const out = rb.shapeFor(frame('raid.started', { despawnAt: NOW + 60000, id: 'raid-1-1', runId: '7', key: '7:1', radius: 50 }), { geometry: GEOMETRY })
assert.strictEqual(out.grid, gridOf(GEOMETRY, 650, 2037))
assert.strictEqual(out.difficulty, 'Wobbly')
assert.strictEqual(out.level, 7)
assert.strictEqual(out.base, 'wobble3')
assert.strictEqual(out.pvp, true)
assert.strictEqual(out.despawnAt, NOW + 60000)
for (const k of ['x', 'z', 'id', 'runId', 'key', 'radius']) assert.strictEqual(out[k], undefined, k)
})
test('a completion names its raiders only to the presence audience, and their Steam ids only to staff', () => {
const signedOut = rb.shapeFor(raided(), { geometry: GEOMETRY })
assert.strictEqual(signedOut.raiderCount, 2, 'how many is a count, and names nobody')
assert.strictEqual(signedOut.names, undefined)
assert.strictEqual(signedOut.ownerName, undefined)
assert.strictEqual(signedOut.raiders, undefined)
assert.strictEqual(signedOut.ownerId, undefined)
const member = rb.shapeFor(raided(), { presence: true, geometry: GEOMETRY })
assert.deepStrictEqual(member.names, ['Alice', 'Bob'])
assert.strictEqual(member.ownerName, 'Alice')
assert.strictEqual(member.raiders, undefined)
const staff = rb.shapeFor(raided(), { staff: true, geometry: GEOMETRY })
assert.deepStrictEqual(staff.raiders, [{ steamId: A, name: 'Alice' }, { steamId: B, name: 'Bob' }])
assert.strictEqual(staff.ownerId, A)
assert.strictEqual(staff.key, '7:1')
assert.strictEqual(staff.x, 650)
})
test('a purchase or an admin lock names the player only to the presence audience (D390)', () => {
for (const kind of ['raid.purchased', 'raid.locked']) {
const f = frame(kind, { steamId: A, name: 'Alice' })
assert.strictEqual(rb.shapeFor(f, {}).name, undefined, kind)
assert.strictEqual(rb.shapeFor(f, { presence: true }).name, 'Alice', kind)
assert.strictEqual(rb.shapeFor(f, { presence: true }).steamId, undefined, kind)
assert.strictEqual(rb.shapeFor(f, { staff: true }).steamId, A, kind)
}
})
test('the feed reads a stored completion for its viewer, the row’s own Steam id staff’s', async () => {
withRecorder((sql) => {
if (sql.includes('FROM rust_events')) return [{ id: 9, kind: 'raid.completed', t: NOW, wipeId: 'w1', steamId: A, raw: JSON.stringify(raided()) }]
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.names, undefined)
assert.strictEqual(signedOut.frame.raiderCount, 2)
assert.strictEqual(signedOut.frame.difficulty, 'Wobbly')
assert.ok(signedOut.frame.grid)
const [staff] = await events.recent({ serverId: 'main', admin: true })
assert.strictEqual(staff.steamId, A)
assert.deepStrictEqual(staff.frame.names, ['Alice', 'Bob'])
})
test('a server’s report is read leniently, and the helper’s profiles only as it sent them', () => {
assert.deepStrictEqual(rb.reportOf(null), { raidableBases: null, consolePlugins: null })
const read = rb.reportOf({ raidableBases: JSON.stringify(REPORT), console: '{"plugins":false}' })
assert.strictEqual(read.consolePlugins, false)
assert.deepStrictEqual(read.raidableBases.difficulties, REPORT.difficulties)
assert.deepStrictEqual(read.raidableBases.profiles[0], PROFILE)
assert.strictEqual(rb.helperPatched(read), true)
const bare = rb.reportOf({ raidableBases: '{"loaded":true,"version":"3.2.2","helper":{"state":"missing"}}', console: null })
assert.strictEqual(bare.raidableBases.profiles, null, 'no helper, no profile list')
assert.strictEqual(bare.raidableBases.spawnCall, false)
assert.strictEqual(rb.helperPatched(bare), false)
assert.deepStrictEqual(rb.reportOf({ raidableBases: 'not json' }).raidableBases, 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 `main` and `pve`, whose last reports are `reports`, and a sidecar answering `reply` and `check`. */
function stubStep(t, { reports = { main: { raidableBases: REPORT, console: { plugins: true } } }, reply, check } = {}) {
const row = (id) => {
const r = reports[id]
return r ? { serverId: id, name: SERVERS[id].name, raidableBases: JSON.stringify(r.raidableBases ?? 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 = { raidSpawn: client.raidSpawn, raidCheck: client.raidCheck, getServer: serversDb.getServer }
const calls = []
serversDb.getServer = async (id) => SERVERS[id] || null
client.raidSpawn = async (server, body) => {
calls.push({ what: 'spawn', server: server.id, body })
return typeof reply === 'function' ? reply(body) : reply
}
client.raidCheck = async (server, body) => {
calls.push({ what: 'check', server: server.id, body })
return typeof check === 'function' ? check(body) : check
}
t.after(() => {
client.raidSpawn = saved.raidSpawn
client.raidCheck = saved.raidCheck
serversDb.getServer = saved.getServer
})
return calls
}
const module_ = () => require('../eventRaidableBases')
const spawnStep = () => module_().ACTIONS[0]
const ok = (data) => ({ ok: true, status: 'ok', data })
const queued = (over = {}) => ok({ kind: 'raid.queued', key: '7:1', id: 'raid-1-1', route: 'pick', despawns: 'teardown', difficulty: 'Wobbly', level: 7, warnings: [], repeat: false, ...over })
test('the step is ledgered like a zone, priced one base each, and every param has an example', () => {
const step = spawnStep()
assert.strictEqual(step.id, 'rust.raidbase.spawn')
assert.strictEqual(step.reversible, 'ledger')
assert.strictEqual(step.risk, 'change')
assert.strictEqual(typeof step.revert, 'function')
assert.strictEqual(typeof step.reconcile, 'function')
assert.deepStrictEqual(step.cost({}), { 'rust.raids': 1 })
for (const p of step.params) assert.ok(p.example !== undefined, p.name)
})
test('the pickers list each server’s own names, once each, and nothing for a server that has not said (D386)', async (t) => {
const [difficulties, profiles, bases, switches] = module_().OPTION_SOURCES
stubStep(t, {
reports: {
main: { raidableBases: REPORT },
pve: { raidableBases: { ...REPORT, difficulties: [{ name: 'wobbly', level: 7 }, { name: 'Brutal', level: 9 }], profiles: [] } },
},
})
assert.deepStrictEqual(await difficulties.resolve(), [
{ value: 'Gentle', label: 'Gentle (level 0) — Main' },
{ value: 'Wobbly', label: 'Wobbly (level 7) — Main, PvE' },
{ value: 'Brutal', label: 'Brutal (level 9) — PvE' },
])
assert.deepStrictEqual((await profiles.resolve()).map((p) => p.value), ['Wobbly Keeps', 'Gentle Huts'])
assert.deepStrictEqual((await bases.resolve({ q: 'wobble' })).map((b) => b.value), ['wobble1', 'wobble3'])
assert.strictEqual(bases.searchable, true)
assert.deepStrictEqual((await switches.resolve()).map((s) => s.value), ['on', 'off'])
stubStep(t, { reports: { main: { raidableBases: { loaded: false } } } })
assert.deepStrictEqual(await difficulties.resolve(), [], 'nothing is made up for a server without the plugin')
})
test('a name both servers have, described differently, shows each server’s own description (W13)', async (t) => {
const [difficulties, profiles] = module_().OPTION_SOURCES
const hard = (difficulty) => ({ ...REPORT, profiles: [{ name: 'Hard Bases', difficulty, level: 2, enabled: true, pvp: true, bases: ['h1'], seabed: false, float: false }] })
stubStep(t, {
reports: {
main: { raidableBases: hard('Hard') },
pve: { raidableBases: { ...hard('Expert'), difficulties: [{ name: 'Gentle', level: 1 }, { name: 'Wobbly', level: 7 }] } },
},
})
assert.deepStrictEqual(await profiles.resolve(), [{ value: 'Hard Bases', label: 'Hard Bases · Hard — Main; Hard Bases · Expert — PvE' }])
assert.deepStrictEqual(await difficulties.resolve(), [
{ value: 'Gentle', label: 'Gentle (level 0) — Main; Gentle (level 1) — PvE' },
{ value: 'Wobbly', label: 'Wobbly (level 7) — Main, PvE' },
])
})
test('without a patched helper, a spot, a profile, a base or settings are refused; a plain spawn is not (D348, D387)', async (t) => {
const run = (params) => spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', ...params }, verify: true })
stubStep(t, { reports: { main: { raidableBases: { ...REPORT, helper: { state: 'missing' }, profiles: null }, console: { plugins: true } } } })
for (const params of [{ x: 650, z: 2037 }, { profile: 'Wobbly Keeps' }, { base: 'wobble1' }, { pvp: 'off' }, { npcCount: 3 }, { lock: 'on' }, { despawnMinutes: 60 }]) {
const no = await run(params)
assert.strictEqual(no.retry, false, JSON.stringify(params))
assert.match(no.error, /RunicGatewayRaids\.cs is not installed on Main/)
assert.match(no.error, /despawns the base on its own timer/)
}
assert.deepStrictEqual(await run({ difficulty: 'Wobbly' }), { ok: true })
assert.deepStrictEqual(await run({}), { ok: true })
stubStep(t, { reports: { main: { raidableBases: { ...REPORT, helper: { state: 'missing' }, spawnCall: false, profiles: null }, console: { plugins: false } } } })
const carbon = await run({})
assert.match(carbon.error, /cannot run RaidableBases' console command/)
})
test('the dry run refuses names the server does not have, and names that do not fit together', async (t) => {
const run = (params) => spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', ...params }, verify: true })
stubStep(t)
assert.match((await run({ difficulty: 'Easy' })).error, /"Easy" is not one of RaidableBases' difficulties on Main \(Gentle, Wobbly\)/)
assert.match((await run({ profile: 'Nope' })).error, /no profile "Nope"/)
assert.match((await run({ base: 'nope1' })).error, /no base "nope1"/)
assert.match((await run({ profile: 'Gentle Huts', base: 'wobble1' })).error, /belongs to profile "Wobbly Keeps"/)
assert.match((await run({ profile: 'Wobbly Keeps', difficulty: 'Gentle' })).error, /is Wobbly, not Gentle/)
assert.match((await run({ heightAdjust: 5 })).error, /a height needs a spot/)
assert.match((await run({ x: 650, z: 2037, heightAdjust: 5, height: 30 })).error, /not both/)
assert.match((await run({ pvp: 'maybe' })).error, /pvp is on, off or blank/)
assert.match((await run({ npcCount: 21 })).error, /0 to 20/)
assert.deepStrictEqual(await run({ base: 'WOBBLE3' }), { ok: true }, 'names match as the plugin matches them, ignoring case')
stubStep(t, { reports: { main: { raidableBases: { loaded: false } } } })
assert.match((await run({})).error, /RaidableBases is not installed on Main/)
stubStep(t, { reports: { main: { raidableBases: { ...REPORT, copyPaste: { loaded: false } } } } })
assert.match((await run({})).error, /CopyPaste is missing/)
stubStep(t, { reports: {} })
assert.deepStrictEqual(await run({ x: 650, z: 2037 }), { ok: true }, 'a server that has not said is the plugin’s to answer')
})
test('a spot’s check in the dry run is advice: warnings, never a refusal (D208, D388, D391)', async (t) => {
const calls = stubStep(t, {
check: ok({ kind: 'raid.checked', ok: false, reason: 'spot-blocked', message: 'a player is within 50 m (Alice)', warnings: ['Rock is too large: 1 2 3'] }),
})
const advised = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', x: 650, z: 2037, base: 'wobble3', pvp: 'off' }, verify: true })
assert.strictEqual(advised.ok, true)
assert.deepStrictEqual(advised.detail.warnings, [
'the spot is refused now (spot-blocked): a player is within 50 m (Alice). It is checked again when the event runs',
'at the spot now: Rock is too large: 1 2 3',
])
assert.deepStrictEqual(calls[0].body, { base: 'wobble3', x: 650, z: 2037 }, 'the check asks the spot and the base, not the settings')
assert.ok(!calls.some((c) => c.what === 'spawn'), 'a dry run spawns nothing')
stubStep(t, { check: ok({ kind: 'raid.checked', ok: true, warnings: [] }) })
assert.deepStrictEqual(await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', x: 650, z: 2037 }, verify: true }), { ok: true })
stubStep(t, { check: { ok: false, status: 'http-503' } })
const down = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', x: 650, z: 2037 }, verify: true })
assert.strictEqual(down.ok, true)
assert.match(down.detail.warnings[0], /could not be checked now/)
})
test('a spawn sends the plugin’s names, the spot and the settings, and a base the helper made is the run’s resource', async (t) => {
const calls = stubStep(t, { reply: queued({ route: 'spot', x: 650, z: 2037, base: 'wobble3', profile: 'Wobbly Keeps', warnings: ['Rock is too large: 1 2 3'] }) })
const result = await spawnStep().perform({
runId: 7,
idempotencyKey: '7:1',
params: { monument: 'main/powerplant_1', offsetX: 10, offsetZ: -5, y: 99, heightAdjust: 5, base: 'wobble3', pvp: 'off', npcs: 'on', npcCount: 4, despawnMinutes: 90, lock: 'on' },
})
assert.ok(result.ok)
assert.deepStrictEqual(calls[0].body, {
key: '7:1',
runId: '7',
base: 'wobble3',
monument: 'powerplant_1',
offsetX: 10,
offsetZ: -5,
heightAdjust: 5,
pvp: false,
npcs: true,
npcCount: 4,
despawnMinutes: 90,
lock: true,
})
assert.deepStrictEqual(result.resources, [
{ kind: 'world', ref: 'main:raid-1-1', payload: { serverId: 'main', what: 'raidbase', name: 'wobble3', difficulty: 'Wobbly' } },
])
assert.strictEqual(result.detail.grid, gridOf(GEOMETRY, 650, 2037))
assert.deepStrictEqual(result.detail.warnings, ['Rock is too large: 1 2 3'], 'a rock is carried into the run log (D388)')
assert.strictEqual(result.detail.note, undefined)
})
test('a base the helper did not make is not the run’s: no resource, and the run log says it despawns on its own', async (t) => {
stubStep(t, {
reports: { main: { raidableBases: { ...REPORT, helper: { state: 'missing' }, profiles: null }, console: { plugins: true } } },
reply: queued({ id: undefined, route: 'plugin', despawns: 'own-timer' }),
})
const result = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main', difficulty: 'Wobbly' } })
assert.ok(result.ok)
assert.strictEqual(result.resources, undefined)
const own = module_().OWN_TIMER
assert.strictEqual(
result.detail.note,
`RaidableBases picks the spot; the map shows it once the base opens. ${own[0].toUpperCase()}${own.slice(1)}`,
'each note after the first starts its own sentence (W14)',
)
})
test('the grid’s wait is a ten-minute retry; a refusal the plugin would repeat is not retried', async (t) => {
stubStep(t, {
reply: (body) =>
ok(body.difficulty === 'Gentle'
? { kind: 'raid.error', reason: 'rb-not-ready', message: 'still building its spawn grid' }
: body.difficulty === 'Wobbly'
? { kind: 'raid.error', reason: 'rb-busy', message: 'pasting another base' }
: { kind: 'raid.error', reason: 'spot-blocked', message: 'a player is within 50 m', warnings: ['Rock is too large'] }),
})
const step = spawnStep()
const wait = await step.perform({ runId: 7, idempotencyKey: '7:2', params: { server: 'main', difficulty: 'Gentle' } })
assert.strictEqual(wait.retry, true)
assert.strictEqual(wait.retryAfterMs, 10 * 60 * 1000)
assert.match(wait.error, /not ready yet/)
const busy = await step.perform({ runId: 7, idempotencyKey: '7:3', params: { server: 'main', difficulty: 'Wobbly' } })
assert.strictEqual(busy.retry, undefined, 'a busy spawner retries on core’s ordinary delay')
const blocked = await step.perform({ runId: 7, idempotencyKey: '7:4', params: { server: 'main', x: 650, z: 2037 } })
assert.strictEqual(blocked.retry, false)
assert.match(blocked.error, /a player is within 50 m \(also: Rock is too large\)/)
for (const reason of ['rb-missing', 'copypaste-missing', 'helper-needed', 'helper-unsupported', 'bad-difficulty', 'bad-profile', 'bad-base', 'spot-blocked', 'in-water', 'off-map', 'console-unavailable', 'refused', 'malformed', 'events-disabled']) {
assert.ok(module_().PERMANENT.has(reason), reason)
}
for (const reason of ['rb-not-ready', 'rb-busy']) assert.ok(!module_().PERMANENT.has(reason), reason)
})
test('a repeat of a key spawns nothing new, and says so', async (t) => {
stubStep(t, { reply: queued({ repeat: true }) })
const again = await spawnStep().perform({ runId: 7, idempotencyKey: '7:1', params: { server: 'main' } })
assert.ok(again.ok)
assert.strictEqual(again.detail.repeat, true)
assert.match(again.detail.note, /^answered from the first attempt; nothing new was spawned/)
assert.strictEqual(again.resources[0].ref, 'main:raid-1-1', 'the repeat still names the run’s base')
})
// ── Warlord (D202) ─────────────────────────────────────────────────────────
test('the tally’s raids completed 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', raidsCompleted: 2 } })
const titleAdd = rec.touching('rust_player_wipe_stats').find((s) => s.sql.includes('raids_completed'))
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('raids_completed')], 2)
})
test('the leaderboard sorts by raids completed 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, raidsCompleted: '3' }] : []))
const events = require('../model/events/events.model')
const [row] = await events.leaderboard({ serverId: 'main', wipeId: 'w1', sort: 'raidsCompleted' })
assert.strictEqual(row.raidsCompleted, 3)
const board = rec.touching('rust_player_wipe_stats').at(-1)
assert.match(board.sql, /SUM\(s\.raids_completed\) AS raidsCompleted/)
assert.match(board.sql, /ORDER BY[^]*raids_completed/)
})
test('Warlord is a title category that sums raids completed', () => {
const titles = require('../model/titles/titles')
const stats = titles.STATS || titles._STATS || null
if (!stats) return
assert.deepStrictEqual(stats.raidscompleted, { label: 'Raid bases completed', title: 'Warlord', source: { sum: 'raids_completed' } })
})
// ── The map's raids layer (D114) ───────────────────────────────────────────
test('the map’s raids layer is public by default, and a run’s key never reaches a viewer', () => {
const map = require('../model/map/map.model')
assert.ok(map.LAYERS.includes('raids'))
assert.strictEqual(map.DEFAULTS.raids, 'public')
const layers = {}
for (const l of map.LAYERS) layers[l] = { visible: l === 'raids', audience: 'public' }
const out = map.project(
{ raids: [{ x: 1, z: 2, difficulty: 'Wobbly', runId: '7', key: '7:1' }], world: [{ kind: 'cargo' }] },
{ layers, mates: { visible: false }, steamIds: [] },
)
assert.deepStrictEqual(out.raids, [{ x: 1, z: 2, difficulty: 'Wobbly', runId: '7' }])
assert.strictEqual(out.world, undefined, 'a hidden layer is absent, not empty')
layers.raids.visible = false
assert.strictEqual(map.project({ raids: [{ x: 1, z: 2 }] }, { layers, mates: { visible: false }, steamIds: [] }).raids, undefined)
})