Files
Module-Rust/server/test/zones.test.js
wtclaude e1789c1df4
All checks were successful
PR Checks / server-tests (pull_request) Successful in 32s
PR Checks / frozen-manifest (pull_request) Successful in 1m13s
PR Checks / client-build (pull_request) Successful in 7m49s
feat(zones): a blank dome stack is 3 spheres, not 1 (runicnpc 9e, D326)
The org lead chose 3 by eye at noon in RunicNPC's 9d player session. The
zone step sends it when its stack is left blank, and the field's hint says
so. The wire is unchanged: the module always sends a stack, and the bridge's
own default for a dome without one stays 1.

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

293 lines
13 KiB
JavaScript
Raw Permalink 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
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
require('../core')._reset()
require('../core').init(fakeCtx())
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 } = {}) {
return {
id,
name: id.toUpperCase(),
enabled: true,
zoneManager: flags ? { version: '3.1.14', flags } : null,
zoneDomes: domes,
}
}
/** 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)
})
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'])
})