Files
Module-Rust/server/test/world.test.js
wtclaude fab31f23e8 refactor(rust): one placing verb per kind — rust.crate.place and rust.npc.place (D97)
Core learns which caps an action accepts by pricing its declared examples
once, and drops a dimension priced at zero. A single rust.prefab.place whose
cost moved between rust.prefabs and rust.npcs by its prefab param could only
ever show the crates cap, so D89's separate dial for fights was unreachable.

Two verbs, each pricing exactly one dimension, with the prefab source split
to match (rust.options.crates / rust.options.npcs). The switchboard can now
allow crates and leave NPCs off. The plugin's world.place is unchanged.

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

332 lines
15 KiB
JavaScript

// ── The world verbs (PLAN.md §28, protocol 9) ─────────────────────────────
//
// What an event makes and gives back. Every test here is one of the ways the
// contract's half can look right and be wrong:
//
// the budget must outlive the client, or `retry: false` is unreachable
// a location is a monument OR coordinates, and a monument names its server
// a dry run checks everything it can and sends nothing
// the ref carries the server, because revert and reconcile get no params
// the undo carries NO idempotency key (module-uo's teardown-was-a-no-op bug)
// a lost answer is reverted by key on every server, since the server is unknown
// `gone` is a success and `refused` is not
// "cannot ask" is never "it is gone"
// only a restart or a wipe provokes a reconcile, never a reconnect
const test = require('node:test')
const assert = require('node:assert')
const { fakeCtx } = require('./_fakes')
require('../core')._reset()
require('../core').init(fakeCtx())
const core = require('../core')
const client = require('../sidecarClient')
const serversDb = require('../model/servers/servers.db')
const servers = require('../model/servers/servers.model')
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 ROWS = {
main: { id: 'main', name: 'Main', sidecarBaseUrl: 'http://main:1', sidecarTokenEnc: null, enabled: 1 },
alt: { id: 'alt', name: 'Alt', sidecarBaseUrl: 'http://alt:1', sidecarTokenEnc: null, enabled: 1 },
off: { id: 'off', name: 'Off', sidecarBaseUrl: 'http://off:1', sidecarTokenEnc: null, enabled: 0 },
}
/** Replace the module's collaborators for one test, and put them back after. */
function stub(t, { zone, place, revert, owned, monuments, polling } = {}) {
const calls = { zone: [], place: [], revert: [], owned: [], monuments: [] }
const saved = {
getServer: serversDb.getServer,
listForPolling: servers.listForPolling,
worldZone: client.worldZone,
worldPlace: client.worldPlace,
worldRevert: client.worldRevert,
worldOwned: client.worldOwned,
worldMonuments: client.worldMonuments,
}
serversDb.getServer = async (id) => ROWS[id] || null
servers.listForPolling = async () =>
(polling || ['main']).map((id) => ({ id, name: ROWS[id] ? ROWS[id].name : id, baseUrl: `http://${id}:1`, token: 't' }))
const ok = (data) => ({ ok: true, status: 'ok', data })
client.worldZone = async (server, body) => {
calls.zone.push({ server: server.id, body })
return zone ? zone(server, body) : ok({ kind: 'world.ok', placed: [{ id: 'rg-7-1-1', kind: 'zone', name: 'Z' }] })
}
client.worldPlace = async (server, body) => {
calls.place.push({ server: server.id, body })
return place
? place(server, body)
: ok({ kind: 'world.ok', placed: [{ id: '101', kind: 'crate', prefab: body.prefab }, { id: '102', kind: 'crate', prefab: body.prefab }] })
}
client.worldRevert = async (server, body) => {
calls.revert.push({ server: server.id, body })
return revert ? revert(server, body) : ok({ kind: 'world.ok', removed: body.ids || [], gone: [], refused: [] })
}
client.worldOwned = async (server, q) => {
calls.owned.push({ server: server.id, ...q })
return owned ? owned(server, q) : { ok: false, status: 'http-503' }
}
client.worldMonuments = async (server) => {
calls.monuments.push(server.id)
return monuments ? monuments(server) : { ok: false, status: 'http-503' }
}
t.after(() => Object.assign(client, {
worldZone: saved.worldZone,
worldPlace: saved.worldPlace,
worldRevert: saved.worldRevert,
worldOwned: saved.worldOwned,
worldMonuments: saved.worldMonuments,
}))
t.after(() => {
serversDb.getServer = saved.getServer
servers.listForPolling = saved.listForPolling
})
return calls
}
test('every world verb outlives the client, which outlives the sidecar', () => {
// `sidecar RPC (10s) < TIMEOUT_MS < budgetMs` — or the dispatcher gives up
// first, classifies retry, and every `retry: false` below is dead code.
assert.ok(10000 < client.TIMEOUT_MS)
for (const a of world.ACTIONS) assert.ok(client.TIMEOUT_MS < a.budgetMs, `${a.id} budgetMs`)
})
test('the mirrored bounds are the plugin\'s own (D95, D96)', () => {
assert.strictEqual(world.MAX_CRATES, 25)
assert.strictEqual(world.MAX_NPCS, 20)
assert.strictEqual(world.ZONE_MAX_MINUTES, 7 * 24 * 60)
// Crates and NPCs only, never a vehicle (D88).
assert.deepStrictEqual([...new Set(world.PLACEABLE.map((p) => p.kind))].sort(), ['crate', 'npc'])
})
test('a location is a monument or coordinates, exactly one, and a monument names its server', () => {
assert.strictEqual(world.location({}).ok, false)
assert.strictEqual(world.location({ monument: 'main/airfield_1', x: 1, z: 2 }).ok, false)
const byMonument = world.location({ monument: 'main/harbor_1#2', offsetX: 10 })
assert.deepStrictEqual(byMonument, { ok: true, serverId: 'main', wire: { monument: 'harbor_1#2', offsetX: 10, offsetZ: 0 } })
// Two ways to name a server must agree.
assert.match(world.location({ monument: 'main/harbor_1', server: 'alt' }).error, /on main, not alt/)
// An offset past the bound is refused on the form, not mid-run.
assert.match(world.location({ monument: 'main/harbor_1', offsetX: 120, offsetZ: 120 }).error, /at most 150/)
// Coordinates name nothing, so they need the server.
assert.match(world.location({ x: 1, z: 2 }).error, /which server/)
assert.deepStrictEqual(world.location({ x: 1, z: 2, server: 'alt' }), { ok: true, serverId: 'alt', wire: { x: 1, z: 2 } })
assert.match(world.location({ x: 1, server: 'alt' }).error, /both x and z/)
})
test('a dry run checks everything it can and sends nothing', async (t) => {
const calls = stub(t)
const zone = await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k1', verify: true, params: { monument: 'main/airfield_1', radius: 40, minutes: 60 },
})
const place = await action('rust.crate.place').perform({
runId: 7, idempotencyKey: 'k2', verify: true, params: { monument: 'main/airfield_1', prefab: 'crate.elite', count: 3 },
})
assert.deepStrictEqual([zone, place], [{ ok: true }, { ok: true }])
assert.deepStrictEqual([calls.zone.length, calls.place.length], [0, 0])
})
test('every authoring mistake is refused for good, before anything is sent', async (t) => {
const calls = stub(t)
const crates = action('rust.crate.place')
const npcs = action('rust.npc.place')
const zone = action('rust.zone.open')
const run = (a, params) => a.perform({ runId: 7, idempotencyKey: 'k', params })
for (const result of [
await run(crates, { monument: 'main/a', prefab: 'minicopter', count: 1 }),
await run(crates, { monument: 'main/a', prefab: 'npc.scientist', count: 1 }), // D97: the other verb's
await run(npcs, { monument: 'main/a', prefab: 'crate.elite', count: 1 }),
await run(crates, { monument: 'main/a', prefab: 'crate.elite', count: 26 }),
await run(npcs, { monument: 'main/a', prefab: 'npc.scientist', count: 21 }),
await run(crates, { monument: 'main/a', prefab: 'crate.elite', count: 1, spread: 51 }),
await run(zone, { monument: 'main/a', radius: 4, minutes: 10 }),
await run(zone, { monument: 'main/a', radius: 40 }), // D96: minutes are required
await run(zone, { monument: 'main/a', radius: 40, minutes: 7 * 24 * 60 + 1 }),
await run(zone, { monument: 'nowhere/a', radius: 40, minutes: 10 }),
await run(zone, { monument: 'off/a', radius: 40, minutes: 10 }),
]) {
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, false, result.error)
}
assert.deepStrictEqual([calls.zone.length, calls.place.length], [0, 0])
})
test('a zone crosses with its key, its duration, and the monument without the server', async (t) => {
const calls = stub(t)
const result = await action('rust.zone.open').perform({
runId: 7, idempotencyKey: 'k1', params: { monument: 'main/airfield_1', offsetZ: -20, radius: 40, minutes: 90, name: 'Brawl' },
})
assert.deepStrictEqual(calls.zone[0], {
server: 'main',
body: { runId: '7', key: 'k1', monument: 'airfield_1', offsetX: 0, offsetZ: -20, radius: 40, holdMs: 5400000, name: 'Brawl' },
})
// The ref carries the server: revert and reconcile are handed no params.
assert.deepStrictEqual(result.resources, [
{ kind: 'world', ref: 'main:rg-7-1-1', payload: { serverId: 'main', what: 'zone', name: 'Z' } },
])
})
test('one resource per thing placed, and a repeated key is said to be one', async (t) => {
stub(t, {
place: async () => ({
ok: true,
data: { kind: 'world.ok', repeat: true, placed: [{ id: '101', kind: 'npc', prefab: 'npc.scientist' }] },
}),
})
const result = await action('rust.npc.place').perform({
runId: 7, idempotencyKey: 'k', params: { x: -604, z: -342, server: 'main', prefab: 'npc.scientist', count: 1 },
})
assert.strictEqual(result.ok, true)
assert.deepStrictEqual(result.resources.map((r) => r.ref), ['main:101'])
assert.strictEqual(result.detail.repeat, true)
})
test('the switch being off is a refusal with the switch named, for good (D94)', async (t) => {
stub(t, {
place: async () => ({ ok: true, data: { kind: 'world.error', reason: 'events-disabled', message: 'set EventsEnabled' } }),
})
const result = await action('rust.crate.place').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', prefab: 'crate.elite', count: 1 },
})
assert.deepStrictEqual(result, { ok: false, retry: false, error: 'set EventsEnabled' })
})
test('a game that is down or slow is left to core to retry', async (t) => {
stub(t, { place: async () => ({ ok: false, status: 'http-503' }) })
const result = await action('rust.crate.place').perform({
runId: 7, idempotencyKey: 'k', params: { monument: 'main/a', prefab: 'crate.elite', count: 1 },
})
assert.strictEqual(result.ok, false)
assert.strictEqual(result.retry, undefined)
assert.match(result.error, /Main has no game connected/)
})
test('revert sends ids per server and NO idempotency key', async (t) => {
const calls = stub(t)
const result = await world.revert({
runId: 7,
idempotencyKey: 'k-of-the-do',
resources: [
{ kind: 'world', ref: 'main:101', payload: { serverId: 'main' } },
{ kind: 'world', ref: 'alt:rg-7-1-1', payload: { serverId: 'alt' } },
{ kind: 'world', ref: 'main:102' },
],
})
assert.deepStrictEqual(result, { ok: true })
assert.deepStrictEqual(calls.revert, [
{ server: 'main', body: { runId: '7', ids: ['101', '102'] } },
{ server: 'alt', body: { runId: '7', ids: ['rg-7-1-1'] } },
])
})
test('gone is a success, and refused is a failure named by ref', async (t) => {
stub(t, { revert: async () => ({ ok: true, data: { kind: 'world.ok', removed: ['101'], gone: ['102'], refused: ['103'] } }) })
const result = await world.revert({
runId: 7,
resources: ['101', '102', '103'].map((id) => ({ kind: 'world', ref: `main:${id}` })),
})
assert.deepStrictEqual(result, { ok: true, failed: ['main:103'] })
})
test('a lost answer is reverted by its key on every server, and an unreachable one keeps the row', async (t) => {
const calls = stub(t, {
polling: ['main', 'alt'],
revert: async (server) => (server.id === 'alt' ? { ok: false, status: 'http-503' } : { ok: true, data: { kind: 'world.ok' } }),
})
const result = await world.revert({ runId: 7, resources: [], idempotencyKey: 'k-lost' })
assert.deepStrictEqual(calls.revert.map((c) => c.body), [
{ runId: '7', key: 'k-lost' },
{ runId: '7', key: 'k-lost' },
])
assert.strictEqual(result.ok, false)
assert.match(result.error, /Alt has no game connected/)
})
test('reconcile asks each server what it holds, and "cannot ask" is not "gone"', async (t) => {
stub(t, {
owned: async (server) =>
server.id === 'main'
? { ok: true, data: { kind: 'world.owned', owned: [{ id: '101' }] } }
: { ok: false, status: 'http-503' },
})
const result = await world.reconcile({
runId: 7,
resources: [
{ kind: 'world', ref: 'main:101' },
{ kind: 'world', ref: 'main:102' }, // looted, or an NPC a restart took
{ kind: 'world', ref: 'alt:rg-7-1-1', payload: { serverId: 'alt' } },
],
})
assert.deepStrictEqual(result, { ok: true, inForce: ['main:101', 'alt:rg-7-1-1'] })
})
test('the monument source lists each server\'s map as whole values, numbered where a kind repeats', async (t) => {
stub(t, {
polling: ['main', 'alt'],
monuments: async (server) =>
server.id === 'alt'
? { ok: false, status: 'http-503' }
: {
ok: true,
data: {
monuments: [
{ value: 'harbor_1#1', label: 'Harbor', instance: 1, of: 2, grid: 'M7' },
{ value: 'harbor_1#2', label: 'Harbor', instance: 2, of: 2, grid: 'C12' },
{ value: 'powerplant_1', label: 'Power Plant', instance: 1, of: 1, grid: 'F14' },
],
},
},
})
const rows = await source('rust.options.monuments').resolve()
assert.deepStrictEqual(rows, [
{ value: 'main/harbor_1#1', label: 'Harbor #1 · M7', group: 'Main' },
{ value: 'main/harbor_1#2', label: 'Harbor #2 · C12', group: 'Main' },
{ value: 'main/powerplant_1', label: 'Power Plant · F14', group: 'Main' },
])
const narrowed = await source('rust.options.monuments').resolve({ q: 'power' })
assert.deepStrictEqual(narrowed.map((r) => r.value), ['main/powerplant_1'])
})
test('the crate and NPC sources split the allowlist, and answer with every server off (D97)', async (t) => {
const calls = stub(t)
const crates = await source('rust.options.crates').resolve()
const npcs = await source('rust.options.npcs').resolve()
assert.strictEqual(crates.length + npcs.length, world.PLACEABLE.length)
assert.ok(crates.every((r) => !r.value.startsWith('npc.')))
assert.ok(npcs.every((r) => r.value.startsWith('npc.')))
assert.deepStrictEqual(calls.monuments, [])
})
test('only a restart or a wipe asks core to reconcile — never a first sighting or a reconnect', (t) => {
world.resetWatch()
let asked = 0
const saved = core.reconcileEvents
core.reconcileEvents = () => {
asked += 1
return Promise.resolve({})
}
t.after(() => {
core.reconcileEvents = saved
world.resetWatch()
})
assert.strictEqual(world.observeServer('main', { bootId: 'b1', wipeId: 'w1' }), false) // baseline
assert.strictEqual(world.observeServer('main', { bootId: 'b1', wipeId: 'w1' }), false) // a reconnect
assert.strictEqual(world.observeServer('main', { bootId: 'b2', wipeId: 'w1' }), true) // a restart
assert.strictEqual(world.observeServer('main', { bootId: 'b3', wipeId: 'w2' }), true) // a wipe
assert.strictEqual(world.observeServer('alt', { bootId: 'x', wipeId: 'y' }), false) // another server's baseline
assert.strictEqual(asked, 2)
})