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
453 lines
24 KiB
JavaScript
453 lines
24 KiB
JavaScript
// ── 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)
|
||
})
|