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

379 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.

// ── Zones: presets, options and the dome (PLAN_REDESIGNS §3, D210, D211) ───
//
// What each test guards:
//
// a step's options line reads back as the flags and settings it names, spelled
// as the server's own ZoneManager spells them (D211)
// a flag the server does not have is refused on the form, not at 4 a.m.
// a flag this module does not know is drawn under Other, never hidden
// a preset is checked against every server it covers, and one that has not
// said is left to the plugin
// two presets of one name may not share a server; across servers they may (D189)
// the step's dropdown VALUE is the line itself, so picking copies it (D210)
// the zone crosses with its flags, settings, messages and dome
// a dome on a server without ZoneDomes or its helper is refused with the reason
// TruePVE's rules cross as exclude or one ruleset, never both, and never to a
// server that said it has no TruePVE (§14.3.2, D368); a ruleset TruePVE does
// not have fails for good, with the plugin's words
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
require('../core')._reset()
require('../core').init(fakeCtx())
// 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 zonesDb = require('../model/zones/zones.db')
const zones = require('../model/zones/zones.model')
const options = require('../model/zones/zoneOptions')
const voice = require('../model/permissions/voice')
const world = require('../eventWorld')
const action = (id) => world.ACTIONS.find((a) => a.id === id)
const source = (id) => world.OPTION_SOURCES.find((s) => s.id === id)
const FLAGS = ['AutoLights', 'NoBuild', 'NoPlayerLoot', 'PvpGod', 'NoChat', 'SomethingNew']
function server(id, { flags = FLAGS, domes = null, truePve = null } = {}) {
return {
id,
name: id.toUpperCase(),
enabled: true,
zoneManager: flags ? { version: '3.1.14', flags } : null,
zoneDomes: domes,
truePve,
}
}
/** Stubs the preset store and the servers' hellos for one test. */
function stubZones(t, { servers = [server('main')], presets = [] } = {}) {
const saved = { ...zonesDb }
const store = presets.map((p) => ({ ...p }))
const writes = []
zonesDb.listZoneServers = async () => servers
zonesDb.listPresets = async () => store.map((p) => ({ ...p }))
zonesDb.getPreset = async (id) => store.find((p) => p.id === id) || null
zonesDb.savePreset = async (row) => {
writes.push(row)
const id = row.id || store.length + 100
const next = { id, ...row, servers: row.allServers ? [] : row.servers, updatedAt: null }
const at = store.findIndex((p) => p.id === id)
if (at >= 0) store[at] = next
else store.push(next)
return id
}
zonesDb.deletePreset = async (id) => {
const at = store.findIndex((p) => p.id === id)
if (at >= 0) store.splice(at, 1)
}
t.after(() => Object.assign(zonesDb, saved))
return { store, writes }
}
function stubWorld(t) {
const calls = []
const saved = { getServer: serversDb.getServer, worldZone: client.worldZone, currentFormat: voice.currentFormat }
serversDb.getServer = async (id) => ({ id, name: id, sidecarBaseUrl: `http://${id}:1`, sidecarTokenEnc: null, enabled: 1 })
client.worldZone = async (srv, body) => {
calls.push({ server: srv.id, body })
return { ok: true, status: 'ok', data: { kind: 'world.ok', placed: [{ id: 'rg-7-1-1', kind: 'zone', name: 'Z' }] } }
}
voice.currentFormat = async () => '<color=#ff0>[Event]</color> {message}'
t.after(() => {
serversDb.getServer = saved.getServer
client.worldZone = saved.worldZone
voice.currentFormat = saved.currentFormat
})
return calls
}
// ── the options line ───────────────────────────────────────────────────────
test('an options line reads as flags and settings, spelled as the server spells them', () => {
const read = options.parse('nobuild, NOPLAYERLOOT radiation=10 safezone=true permission=Arena_1', FLAGS)
assert.deepStrictEqual(read, {
ok: true,
flags: ['NoBuild', 'NoPlayerLoot'],
settings: { radiation: 10, safezone: true, permission: 'arena_1' },
})
assert.deepStrictEqual(options.parse('', FLAGS), { ok: true, flags: [], settings: {} })
assert.deepStrictEqual(options.parse(undefined), { ok: true, flags: [], settings: {} })
})
test('a flag the server lacks, or a setting out of range, is refused', () => {
for (const line of [
'NoFlying',
'radiation=501',
'comfort=2',
'temperature=-101',
'safezone=maybe',
'permission=has.dots',
'rotation=90',
'radiation=1 radiation=2',
'No-Build',
]) {
const read = options.parse(line, FLAGS)
assert.strictEqual(read.ok, false, line)
assert.ok(read.error, line)
}
})
test('without a flag list the flags are kept as written, and the plugin checks them', () => {
assert.deepStrictEqual(options.parse('NoFlying', null), { ok: true, flags: ['NoFlying'], settings: {} })
})
test('format and parse are inverses', () => {
const preset = { flags: ['NoBuild', 'PvpGod'], settings: { safezone: false, radiation: 2.5 } }
const line = options.format(preset)
assert.strictEqual(line, 'NoBuild, PvpGod, radiation=2.5, safezone=false')
const back = options.parse(line, FLAGS)
assert.deepStrictEqual({ flags: back.flags, settings: back.settings }, preset)
})
test('a flag this module does not know is drawn under Other, never hidden', () => {
const groups = options.grouped(FLAGS)
assert.deepStrictEqual(groups.map((g) => g.group), ['Combat', 'Building', 'Loot', 'Communication', 'Other'])
assert.deepStrictEqual(groups.find((g) => g.group === 'Other').flags.map((f) => f.name), ['SomethingNew'])
assert.strictEqual(groups.flatMap((g) => g.flags).length, FLAGS.length)
})
// ── presets ────────────────────────────────────────────────────────────────
test('a preset is checked against every server it covers, and saved as the server spells it', async (t) => {
const { writes } = stubZones(t, {
servers: [server('main'), server('alt', { flags: ['NoBuild'] }), server('dark', { flags: null })],
})
await assert.rejects(zones.create({ name: 'Arena', flags: ['NoPlayerLoot'], servers: ['main', 'alt'] }), (err) => {
assert.strictEqual(err.status, 400)
assert.match(err.message, /ALT/)
return true
})
// `dark` has not said which flags it has: skipped, and the plugin checks.
const saved = await zones.create({ name: 'Arena', flags: ['noplayerloot'], settings: { radiation: '5' }, servers: ['main', 'dark'] }, 9)
assert.deepStrictEqual(writes[0], {
name: 'Arena', flags: ['NoPlayerLoot'], settings: { radiation: 5 }, allServers: false, servers: ['main', 'dark'],
})
assert.strictEqual(saved.name, 'Arena')
})
test('a preset names a server, or every server, and only ones that exist', async (t) => {
stubZones(t)
await assert.rejects(zones.create({ name: 'A', flags: [] }), /at least one server/)
await assert.rejects(zones.create({ name: 'A', servers: ['nowhere'] }), (err) => err.status === 404)
await assert.rejects(zones.create({ name: '', servers: ['main'] }), /name/)
})
test('two presets of one name may not share a server; on different servers they may (D189)', async (t) => {
stubZones(t, {
servers: [server('main'), server('alt')],
presets: [{ id: 1, name: 'Arena', flags: [], settings: {}, allServers: false, servers: ['main'] }],
})
await assert.rejects(zones.create({ name: 'arena', servers: ['main', 'alt'] }), (err) => err.status === 409)
await assert.rejects(zones.create({ name: 'Arena', allServers: true }), (err) => err.status === 409)
const alt = await zones.create({ name: 'Arena', servers: ['alt'] })
assert.deepStrictEqual(alt.servers, ['alt'])
// Editing the first keeps its own name without clashing with itself.
const same = await zones.update(1, { name: 'Arena', flags: ['PvpGod'], servers: ['main'] })
assert.deepStrictEqual(same.flags, ['PvpGod'])
})
test('the zone step’s dropdown VALUE is the options line, so picking a preset copies it (D210)', async (t) => {
stubZones(t, {
servers: [server('main')],
presets: [
{ id: 1, name: 'Arena', flags: ['NoBuild'], settings: { radiation: 10 }, allServers: false, servers: ['main'] },
{ id: 2, name: 'Calm', flags: ['PvpGod'], settings: {}, allServers: true, servers: [] },
],
})
assert.deepStrictEqual(await source('rust.options.zone_presets').resolve({}), [
{ value: 'NoBuild, radiation=10', label: 'Arena', group: 'MAIN' },
{ value: 'PvpGod', label: 'Calm', group: 'Every server' },
])
})
// ── the step ───────────────────────────────────────────────────────────────
const PATCHED = { loaded: true, version: '2.0.2', helper: { state: 'patched', version: '0.1.0' }, ready: true }
test('a zone crosses with its flags, settings, messages in the chat voice, and its dome', async (t) => {
stubZones(t, { servers: [server('main', { domes: PATCHED })] })
const calls = stubWorld(t)
const result = await action('rust.zone.open').perform({
runId: 7,
idempotencyKey: 'k1',
params: {
monument: 'main/airfield_1', radius: 40, minutes: 30,
options: 'nobuild, pvpgod, radiation=10',
enterMessage: ' In you go. ', leaveMessage: 'Out.',
dome: 'Red', domeStack: 2,
},
})
assert.strictEqual(result.ok, true, result.error)
assert.deepStrictEqual(calls[0].body, {
runId: '7', key: 'k1', monument: 'airfield_1', offsetX: 0, offsetZ: 0, radius: 40, holdMs: 1800000,
flags: ['NoBuild', 'PvpGod'],
settings: { radiation: 10 },
enterMessage: 'In you go.',
leaveMessage: 'Out.',
delivery: 'chat',
format: '<color=#ff0>[Event]</color> {message}',
dome: { type: 1, stack: 2 },
})
})
test('a dome with its stack left blank stacks 3 spheres (D326)', async (t) => {
stubZones(t, { servers: [server('main', { domes: PATCHED })] })
const calls = stubWorld(t)
const result = await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5, dome: 'standard', domeStack: '' },
})
assert.strictEqual(result.ok, true, result.error)
assert.deepStrictEqual(calls[0].body.dome, { type: 0, stack: 3 })
})
test('a zone with no options crosses exactly as it always did', async (t) => {
stubZones(t)
const calls = stubWorld(t)
await action('rust.zone.open').perform({ runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5 } })
assert.deepStrictEqual(Object.keys(calls[0].body).sort(), ['holdMs', 'key', 'monument', 'offsetX', 'offsetZ', 'radius', 'runId'])
})
test('a popup carries no chat voice', async (t) => {
stubZones(t)
const calls = stubWorld(t)
await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5, enterMessage: 'Hi', delivery: 'popup' },
})
assert.strictEqual(calls[0].body.delivery, 'popup')
assert.strictEqual(calls[0].body.format, undefined)
})
test('a bad option, message or dome is refused for good, before anything is sent', async (t) => {
const run = (params) =>
action('rust.zone.open').perform({ runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5, ...params } })
for (const [domes, params, words] of [
[PATCHED, { options: 'NoFlying' }, /not a flag/],
[PATCHED, { enterMessage: 'x'.repeat(257) }, /256/],
[PATCHED, { delivery: 'shout' }, /chat or to a popup/],
[PATCHED, { dome: 'orange' }, /standard, red/],
[PATCHED, { dome: 'red', domeStack: 11 }, /1 to 10/],
[{ loaded: false }, { dome: 'red' }, /ZoneDomes is not loaded/],
[{ loaded: true, helper: { state: 'missing' } }, { dome: 'red' }, /RunicGatewayDomes\.cs is not installed/],
[{ loaded: true, helper: { state: 'unsupported' } }, { dome: 'red' }, /could not patch/],
]) {
await t.test(String(words), async (tt) => {
stubZones(tt, { servers: [server('main', { domes })] })
const calls = stubWorld(tt)
const result = await run(params)
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, false)
assert.match(result.error, words)
assert.strictEqual(calls.length, 0)
})
}
})
test('a dry run checks the options and the dome and sends nothing', async (t) => {
stubZones(t, { servers: [server('main', { domes: PATCHED })] })
const calls = stubWorld(t)
const result = await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k', verify: true, params: { monument: 'main/a', radius: 40, minutes: 5, options: 'NoBuild', dome: 'standard' },
})
assert.deepStrictEqual(result, { ok: true })
assert.strictEqual(calls.length, 0)
})
// ── TruePVE in the zone (§14.3.2, D368) ────────────────────────────────────
const TRUEPVE = { loaded: true, version: '2.4.6' }
const zoneRun = (params, extra = {}) =>
action('rust.zone.open').perform({ runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', radius: 40, minutes: 5, ...params }, ...extra })
test('no PvP protection crosses as exclude, and a ruleset by its name, trimmed', async (t) => {
for (const [params, wire] of [
[{ noPvpProtection: true }, { exclude: true }],
[{ noPvpProtection: 'true' }, { exclude: true }],
[{ pvpRuleset: ' pvp-arena ' }, { ruleset: 'pvp-arena' }],
]) {
await t.test(JSON.stringify(params), async (tt) => {
stubZones(tt, { servers: [server('main', { truePve: TRUEPVE })] })
const calls = stubWorld(tt)
const result = await zoneRun(params)
assert.strictEqual(result.ok, true, result.error)
assert.deepStrictEqual(calls[0].body.truePve, wire)
})
}
})
test('left unset, switched off or blank, a zone sends no truePve at all', async (t) => {
stubZones(t, { servers: [server('main', { truePve: { loaded: false } })] })
const calls = stubWorld(t)
for (const params of [{}, { noPvpProtection: false, pvpRuleset: '' }, { noPvpProtection: null, pvpRuleset: ' ' }]) {
const result = await zoneRun(params)
assert.strictEqual(result.ok, true, result.error)
}
assert.ok(calls.every((c) => !('truePve' in c.body)))
})
test('both, a long name, or a server without TruePVE is refused for good, at verify too', async (t) => {
for (const [truePve, params, words] of [
[TRUEPVE, { noPvpProtection: true, pvpRuleset: 'pvp-arena' }, /not both/],
[TRUEPVE, { pvpRuleset: 'x'.repeat(65) }, /64 characters/],
[{ loaded: false }, { noPvpProtection: true }, /TruePVE is not installed on this server/],
[{ loaded: false }, { pvpRuleset: 'pvp-arena' }, /TruePVE is not installed on this server/],
]) {
for (const verify of [true, false]) {
await t.test(`${words} verify=${verify}`, async (tt) => {
stubZones(tt, { servers: [server('main', { truePve })] })
const calls = stubWorld(tt)
const result = await zoneRun(params, { verify })
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, false)
assert.match(result.error, words)
assert.strictEqual(calls.length, 0)
})
}
}
})
test('a server that has not said whether it has TruePVE is left to the plugin', async (t) => {
stubZones(t, { servers: [server('main', { truePve: null })] })
const calls = stubWorld(t)
const result = await zoneRun({ noPvpProtection: true })
assert.strictEqual(result.ok, true, result.error)
assert.deepStrictEqual(calls[0].body.truePve, { exclude: true })
})
test('a ruleset TruePVE does not have, or no TruePVE, fails for good with the plugin’s words', async (t) => {
for (const [reason, message] of [
['truepve-ruleset', "TruePVE on this server has no ruleset named 'pvp-arna' (names are case-sensitive), so the zone was not opened"],
['truepve-missing', 'TruePVE is not loaded on this server, so a zone cannot change its PvP rules'],
]) {
await t.test(reason, async (tt) => {
stubZones(tt, { servers: [server('main', { truePve: TRUEPVE })] })
stubWorld(tt)
client.worldZone = async () => ({ ok: true, status: 'ok', data: { kind: 'world.error', reason, message } })
const result = await zoneRun({ pvpRuleset: 'pvp-arna' })
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, false)
assert.strictEqual(result.error, message)
})
}
})
test('the dome dropdown offers ZoneDomes’ five colours', async () => {
assert.deepStrictEqual((await source('rust.options.domes').resolve()).map((r) => r.value), ['standard', 'red', 'blue', 'green', 'purple'])
})