feat(rust): the world verbs, their budgets and the reconcile watch (phase 13a, protocol 9)
- registerEventActions: rust.zone.open and rust.prefab.place, both reversible 'ledger' with revert() and reconcile(), budgetMs 15000 above the client's 12 s. A location is a monument (kind + instance, carrying its server) or raw coordinates, exactly one (D87, D93); bounds mirrored from the plugin so a bad step is refused on the form (D95); zone minutes required and held by the game (D96). - registerEventBudgets: rust.prefabs, rust.npcs and rust.zone.minutes, each beside the verb that spends it (D79, D89). - Option sources rust.options.monuments (live, searchable) and rust.options.prefabs (mirrored, answers with every server off), registered in the one batch core accepts alongside the lease sources. - Refs are <serverId>:<id>, since revert and reconcile get no params. The undo sends no idempotency key; a lost answer is reverted by key on every server. reconcile asks the plugin, and a server that cannot be asked keeps its rows. - The refresh's bootId/wipeId watch calls ctx.events.reconcile() on a restart or a wipe, never on a first sighting or a reconnect (§11.1). - The permission mirror keeps the plugin's new notLanded grants out of what it records as pushed, and the admin page says so (D85). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -142,10 +142,41 @@ test('nothing is registered that has nothing behind it yet', () => {
|
||||
// NOT register (D62) moved into the assertions below. Phase 12 deleted the
|
||||
// leases and option sources, and kept budgets here on purpose (D79): a lease
|
||||
// spends none, and a dimension nothing spends is a dial that does nothing.
|
||||
// Phase 13a deleted the budgets and actions lines, and registered each budget
|
||||
// beside the verb that spends it (below). The announce leg is 13b's.
|
||||
assert.deepStrictEqual(api.record.legs, [])
|
||||
assert.strictEqual(api.record.hooks.post, undefined)
|
||||
assert.strictEqual(api.record.eventBudgets, null)
|
||||
assert.strictEqual(api.record.eventActions, null)
|
||||
})
|
||||
|
||||
test('the world verbs are registered, and every budget has a verb that spends it (phase 13a)', () => {
|
||||
const { api } = register()
|
||||
|
||||
const actions = api.record.eventActions
|
||||
const budgets = api.record.eventBudgets
|
||||
assert.deepStrictEqual(actions.map((a) => a.id).sort(), ['rust.prefab.place', 'rust.zone.open'])
|
||||
assert.deepStrictEqual(budgets.map((b) => b.id).sort(), ['rust.npcs', 'rust.prefabs', 'rust.zone.minutes'])
|
||||
|
||||
// D79/D89: no dimension without a verb that spends it. A crate step and an
|
||||
// NPC step are priced on different dials, and a zone on its minutes.
|
||||
const spent = new Set()
|
||||
const place = actions.find((a) => a.id === 'rust.prefab.place')
|
||||
for (const cost of [
|
||||
place.cost({ prefab: 'crate.elite', count: 3 }),
|
||||
place.cost({ prefab: 'npc.scientist', count: 2 }),
|
||||
actions.find((a) => a.id === 'rust.zone.open').cost({ minutes: 90 }),
|
||||
]) {
|
||||
for (const id of Object.keys(cost)) spent.add(id)
|
||||
}
|
||||
assert.deepStrictEqual([...spent].sort(), budgets.map((b) => b.id).sort())
|
||||
|
||||
for (const a of actions) {
|
||||
assert.strictEqual(a.reversible, 'ledger')
|
||||
assert.strictEqual(typeof a.revert, 'function')
|
||||
assert.strictEqual(typeof a.reconcile, 'function')
|
||||
// Every param source is one this module registers.
|
||||
const sources = new Set(api.record.eventOptionSources.map((s) => s.id))
|
||||
for (const p of a.params) if (p.source) assert.ok(sources.has(p.source), `${a.id}.${p.name} names ${p.source}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('the leases and their option sources are registered, every source a lease reads (phase 12)', () => {
|
||||
@@ -158,8 +189,10 @@ test('the leases and their option sources are registered, every source a lease r
|
||||
['rust.decay.scale', 'rust.group.permission', 'rust.population', 'rust.spawn.scalar'],
|
||||
)
|
||||
|
||||
// D78: exactly the sources the leases' targets name — none without a reader.
|
||||
// D78: exactly the sources the leases' targets name, plus those the world
|
||||
// verbs' params name (phase 13a) — none without a reader.
|
||||
const read = new Set(leases.map((l) => l.target.source))
|
||||
for (const a of api.record.eventActions) for (const p of a.params) if (p.source) read.add(p.source)
|
||||
assert.deepStrictEqual([...read].sort(), sources.map((s) => s.id).sort())
|
||||
|
||||
for (const l of leases) {
|
||||
|
||||
@@ -205,6 +205,43 @@ test('a permission the server could not resolve is not recorded as pushed', asyn
|
||||
assert.ok(!recorded.includes('7656003'), 'a pending membership is not in the game yet')
|
||||
})
|
||||
|
||||
test('a grant the store did not hold after the plugin read it back is not recorded as pushed (D85)', async () => {
|
||||
const queries = withCore()
|
||||
const permSync = require('../permSync')
|
||||
|
||||
const desired = {
|
||||
hash: 'h1',
|
||||
rows: [
|
||||
{ kind: 'grant', subject: '7656001', object: 'kits.gold' },
|
||||
{ kind: 'grant', subject: '7656001', object: 'zonemanager.zone' },
|
||||
{ kind: 'group-permission', subject: 'vip', object: 'kits.vip' },
|
||||
],
|
||||
}
|
||||
|
||||
// Protocol 9's read-back. The case it exists for is phase 7's owner bug: a
|
||||
// grant the plugin made that never reached Oxide's store, which the site had
|
||||
// been recording as pushed.
|
||||
const report = {
|
||||
kind: 'perm.report',
|
||||
applied: { grants: 1 },
|
||||
unresolved: [],
|
||||
pending: [],
|
||||
notLanded: ['7656001:ZoneManager.Zone', 'vip:kits.vip'],
|
||||
foreign: [],
|
||||
}
|
||||
|
||||
const sidecar = require('../sidecarClient')
|
||||
sidecar.permCatalogue = async () => ({ ok: false, status: 'no-token', data: null })
|
||||
|
||||
await permSync.applyReport({ id: 'main' }, { desired, retire: [], report, bootId: null, wipeId: null })
|
||||
|
||||
const insert = queries.find((q) => q.sql.startsWith('INSERT IGNORE INTO rust_perm_pushed'))
|
||||
const recorded = insert.params.join(' ')
|
||||
assert.ok(recorded.includes('kits.gold'), 'a grant that landed is pushed')
|
||||
assert.ok(!recorded.includes('zonemanager.zone'), 'a grant that did not land is not, whatever its case')
|
||||
assert.ok(!recorded.includes('kits.vip'), 'nor a group permission that did not land')
|
||||
})
|
||||
|
||||
test('a restart, a wipe and a hand edit each provoke a sync; a quiet server does not', () => {
|
||||
withCore()
|
||||
const permSync = require('../permSync')
|
||||
|
||||
325
server/test/world.test.js
Normal file
325
server/test/world.test.js
Normal file
@@ -0,0 +1,325 @@
|
||||
// ── 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.prefab.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 place = action('rust.prefab.place')
|
||||
const zone = action('rust.zone.open')
|
||||
const run = (a, params) => a.perform({ runId: 7, idempotencyKey: 'k', params })
|
||||
|
||||
for (const result of [
|
||||
await run(place, { monument: 'main/a', prefab: 'minicopter', count: 1 }),
|
||||
await run(place, { monument: 'main/a', prefab: 'crate.elite', count: 26 }),
|
||||
await run(place, { monument: 'main/a', prefab: 'npc.scientist', count: 21 }),
|
||||
await run(place, { 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.prefab.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.prefab.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.prefab.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 prefab source answers with every server off', async (t) => {
|
||||
const calls = stub(t)
|
||||
const rows = await source('rust.options.prefabs').resolve()
|
||||
assert.strictEqual(rows.length, world.PLACEABLE.length)
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user