Files
Module-Rust/server/test/leases.test.js
wtclaude d973db7a43
All checks were successful
PR Checks / client-build (pull_request) Successful in 19s
PR Checks / server-tests (pull_request) Successful in 22s
PR Checks / frozen-manifest (pull_request) Successful in 40s
feat(rust): the leases and their option sources (phase 12, protocol 8)
Four leases on core.lease: rust.decay.scale, rust.population, rust.spawn.scalar and rust.group.permission. Every lease is targeted and the target names the server (D73). Also the three target option sources plus rust.options.servers, with no budgets (D79). Held for up to seven days (D77).

A key that is already held reads as its baseline. Drift is an answer, not a failure. inForce reads the plugin's holds and never compares values. Lease calls get a 4.5s timeout so that two of them fit in core.lease's 10s budget, and a timed-out apply is followed by a release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-23 21:14:06 -05:00

330 lines
14 KiB
JavaScript

// ── The leases (PLAN.md §27, protocol 8) ──────────────────────────────────
//
// Core owns the lease: it reads the baseline, reserves the target, applies the
// value and restores it. What this module owns is four callables per lease, and
// every test here is one of the ways those can be subtly wrong while looking
// right:
//
// the target must name the server, or two servers share one holder (D73)
// a duration crosses the wire, not a deadline, or two clocks disagree
// drift is an ANSWER, not a failure of the call
// "still held" is read from the holds, never inferred from a changed value
// a key already held reads as its BASELINE, or teardown restores the event
// an apply this end gave up on is followed by a release
// two lease calls fit inside core.lease's budget, asserted not trusted
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 servers = require('../model/servers/servers.model')
const { LEASES, OPTION_SOURCES, MAX_LEASE_MS, splitTarget } = require('../eventLeases')
const byId = (id) => LEASES.find((l) => l.id === id)
const source = (id) => OPTION_SOURCES.find((s) => s.id === id)
const ROWS = {
main: { id: 'main', name: 'Main', sidecarBaseUrl: 'http://main: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, { list, apply, release, catalogue, polling } = {}) {
const calls = { list: [], apply: [], release: [], catalogue: [] }
const saved = {
getServer: serversDb.getServer,
listForPolling: servers.listForPolling,
leaseList: client.leaseList,
leaseApply: client.leaseApply,
leaseRelease: client.leaseRelease,
permCatalogue: client.permCatalogue,
}
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' }))
client.leaseList = async (server, q) => {
calls.list.push({ server: server.id, ...q })
return list ? list(server, q) : { ok: false, status: 'http-503' }
}
client.leaseApply = async (server, body) => {
calls.apply.push({ server: server.id, body })
return apply ? apply(server, body) : { ok: true, data: { kind: 'lease.ok' } }
}
client.leaseRelease = async (server, body) => {
calls.release.push({ server: server.id, body })
return release ? release(server, body) : { ok: true, data: { kind: 'lease.ok' } }
}
client.permCatalogue = async (server) => {
calls.catalogue.push(server.id)
return catalogue ? catalogue(server) : { ok: false, status: 'http-503' }
}
t.after(() => {
serversDb.getServer = saved.getServer
servers.listForPolling = saved.listForPolling
client.leaseList = saved.leaseList
client.leaseApply = saved.leaseApply
client.leaseRelease = saved.leaseRelease
client.permCatalogue = saved.permCatalogue
})
return calls
}
test('two lease calls fit inside the budget core.lease runs under', () => {
// `core.lease` declares no budgetMs, so the dispatcher's 10s default applies,
// and it spends it on read() THEN apply(). At the client's ordinary 12s either
// one alone would outlast the step, and the dispatcher would call a retry
// while this module was still waiting — the ordering sidecarClient's header
// exists to forbid.
assert.ok(2 * client.LEASE_TIMEOUT_MS < client.CORE_LEASE_BUDGET_MS)
assert.ok(client.LEASE_TIMEOUT_MS < client.TIMEOUT_MS)
})
test('every lease may be held for seven days and no longer (D77)', () => {
assert.strictEqual(MAX_LEASE_MS, 7 * 24 * 60 * 60 * 1000)
for (const l of LEASES) assert.strictEqual(l.maxDurationMs, MAX_LEASE_MS, l.id)
})
test('a target splits at the FIRST slash, so a group name may contain one', () => {
assert.deepStrictEqual(splitTarget('main'), { serverId: 'main', rest: '' })
assert.deepStrictEqual(splitTarget('main/bear.population'), { serverId: 'main', rest: 'bear.population' })
assert.deepStrictEqual(splitTarget('main/a/b/kits.vip'), { serverId: 'main', rest: 'a/b/kits.vip' })
})
test('a target naming no server, or a switched-off one, is refused for good', async (t) => {
stub(t)
const decay = byId('rust.decay.scale')
for (const target of ['', 'nowhere', 'off']) {
const answer = await decay.read({ target })
assert.strictEqual(answer.ok, false, target)
assert.strictEqual(answer.retry, false, `${target}: the second attempt carries the same params`)
}
})
test('the decay rate takes only a server, and a population needs one named', async (t) => {
stub(t)
const decay = await byId('rust.decay.scale').read({ target: 'main/bear.population' })
assert.strictEqual(decay.ok, false)
assert.strictEqual(decay.retry, false)
const pop = await byId('rust.population').read({ target: 'main' })
assert.strictEqual(pop.ok, false)
assert.strictEqual(pop.retry, false)
})
test('read asks the plugin about one key on the named server, and answers the current value as text', async (t) => {
const calls = stub(t, {
list: () => ({ ok: true, data: { leases: [{ key: 'bear.population', family: 'population', current: '2', held: false }], holds: [] } }),
})
const answer = await byId('rust.population').read({ target: 'main/Bear.Population' })
assert.deepStrictEqual(answer, { ok: true, value: '2' })
assert.deepStrictEqual(calls.list, [{ server: 'main', key: 'bear.population', target: undefined }])
})
test('a key from another family is refused even when the plugin lends it', async (t) => {
stub(t, {
list: () => ({ ok: true, data: { leases: [{ key: 'spawn.max_rate', family: 'spawn', current: '1' }] } }),
})
const answer = await byId('rust.population').read({ target: 'main/spawn.max_rate' })
assert.strictEqual(answer.ok, false)
assert.strictEqual(answer.retry, false)
})
test('a key the plugin already holds reads as its BASELINE, not the value the lease put there', async (t) => {
// Core's reservation stops a second run reaching read(). A hold core does not
// know about is this run's first attempt with its answer lost, and the value
// to give back at the end is what was there before anybody borrowed it.
stub(t, {
list: () => ({
ok: true,
data: { leases: [{ key: 'decay.scale', family: 'decay', current: '0', held: true, baseline: '1', applied: '0' }] },
}),
})
const answer = await byId('rust.decay.scale').read({ target: 'main' })
assert.deepStrictEqual(answer, { ok: true, value: '1' })
})
test('apply sends a DURATION, the family, and the value as text', async (t) => {
const calls = stub(t)
const until = new Date(Date.now() + 60 * 60 * 1000)
const answer = await byId('rust.population').apply(4, until, { target: 'main/bear.population' })
assert.deepStrictEqual(answer, { ok: true })
const { body } = calls.apply[0]
assert.strictEqual(body.key, 'bear.population')
assert.strictEqual(body.family, 'population')
assert.strictEqual(body.value, '4')
assert.strictEqual(body.untilMs, until.getTime())
// holdMs is what the plugin arms its timer with; it must be the remaining
// duration, measured here, and never the absolute time.
assert.ok(body.holdMs > 59 * 60 * 1000 && body.holdMs <= 60 * 60 * 1000, String(body.holdMs))
})
test('a group permission crosses as a key and a lowered group/permission target', async (t) => {
const calls = stub(t)
await byId('rust.group.permission').apply(true, new Date(Date.now() + 60000), { target: 'main/Default/Kits.VIP' })
const { body } = calls.apply[0]
assert.strictEqual(body.key, 'group.permission')
assert.strictEqual(body.target, 'default/kits.vip')
assert.strictEqual(body.value, 'true')
assert.strictEqual(body.family, undefined)
})
test('events switched off on the server is a refusal with the switch named, for good (D76)', async (t) => {
stub(t, {
apply: () => ({
ok: true,
data: { kind: 'lease.error', reason: 'events-disabled', message: 'events are switched off on this server — set EventsEnabled' },
}),
})
const answer = await byId('rust.decay.scale').apply(0, new Date(Date.now() + 60000), { target: 'main' })
assert.strictEqual(answer.ok, false)
assert.strictEqual(answer.retry, false)
assert.match(answer.error, /EventsEnabled/)
})
test('an apply this end gave up on is followed by a release of the same value', async (t) => {
const calls = stub(t, { apply: () => ({ ok: false, status: 'timeout' }) })
const answer = await byId('rust.decay.scale').apply(0, new Date(Date.now() + 60000), { target: 'main' })
assert.strictEqual(answer.ok, false)
assert.match(answer.error, /Main/, 'every notice says which server')
await new Promise((resolve) => setImmediate(resolve))
assert.strictEqual(calls.release.length, 1)
assert.deepStrictEqual(calls.release[0].body, { key: 'decay.scale', target: undefined, expected: '0' })
})
test('an apply that failed for any other reason sends no release', async (t) => {
const calls = stub(t, { apply: () => ({ ok: false, status: 'http-503' }) })
await byId('rust.decay.scale').apply(0, new Date(Date.now() + 60000), { target: 'main' })
await new Promise((resolve) => setImmediate(resolve))
assert.strictEqual(calls.release.length, 0)
})
test('an apply whose deadline has already passed is refused before anything is sent', async (t) => {
const calls = stub(t)
const answer = await byId('rust.decay.scale').apply(0, new Date(Date.now() - 1000), { target: 'main' })
assert.strictEqual(answer.ok, false)
assert.strictEqual(calls.apply.length, 0)
})
test('drift is an answer with the current value beside it, not a failed call', async (t) => {
const calls = stub(t, { release: () => ({ ok: true, data: { kind: 'lease.drifted', current: '3' } }) })
const answer = await byId('rust.decay.scale').restore(1, { expected: 0, target: 'main' })
assert.deepStrictEqual(answer, { ok: false, drifted: true, current: '3' })
assert.deepStrictEqual(calls.release[0].body, { key: 'decay.scale', expected: '0', baseline: '1' })
})
test('a release the plugin accepted — including a group that is gone — is a success', async (t) => {
stub(t, { release: () => ({ ok: true, data: { kind: 'lease.ok', targetGone: true } }) })
const answer = await byId('rust.group.permission').restore(false, { expected: true, target: 'main/vip/kits.vip' })
assert.deepStrictEqual(answer, { ok: true })
})
test('a release that could not be made is a failure core will ask again about', async (t) => {
stub(t, { release: () => ({ ok: false, status: 'http-503' }) })
const answer = await byId('rust.decay.scale').restore(1, { expected: 0, target: 'main' })
assert.strictEqual(answer.ok, false)
assert.strictEqual(answer.drifted, undefined)
assert.strictEqual(answer.retry, undefined)
})
test('"still held" is read from the holds, never from a value that changed', async (t) => {
// The current value differs from anything a lease applied — somebody moved it.
// That is DRIFT, for restore() to report; inForce must still say the hold is
// there, or core orphans the row before restore ever gets to say so.
stub(t, {
list: () => ({
ok: true,
data: {
leases: [{ key: 'decay.scale', family: 'decay', current: '7', held: true }],
holds: [{ key: 'decay.scale', applied: '0', baseline: '1' }],
},
}),
})
assert.deepStrictEqual(await byId('rust.decay.scale').inForce({ target: 'main' }), { ok: true, held: true })
})
test('after a restart the plugin holds nothing, and inForce says so', async (t) => {
stub(t, { list: () => ({ ok: true, data: { leases: [{ key: 'decay.scale', family: 'decay', current: '1' }], holds: [] } }) })
assert.deepStrictEqual(await byId('rust.decay.scale').inForce({ target: 'main' }), { ok: true, held: false })
})
test('inForce that cannot reach the game is not an answer', async (t) => {
stub(t)
const answer = await byId('rust.decay.scale').inForce({ target: 'main' })
assert.strictEqual(answer.ok, false)
})
test('a group permission hold is matched on its own target', async (t) => {
stub(t, {
list: () => ({ ok: true, data: { holds: [{ key: 'group.permission', target: 'default/kits.vip' }] } }),
})
const lease = byId('rust.group.permission')
assert.deepStrictEqual(await lease.inForce({ target: 'main/default/kits.vip' }), { ok: true, held: true })
assert.deepStrictEqual(await lease.inForce({ target: 'main/default/kits.gold' }), { ok: true, held: false })
})
test('the servers source is every enabled server, with no game call', async (t) => {
const calls = stub(t, { polling: ['main', 'creative'] })
const rows = await source('rust.options.servers').resolve()
assert.deepStrictEqual(rows.map((r) => r.value), ['main', 'creative'])
assert.strictEqual(calls.list.length, 0)
})
test('a family source lists whole targets, and one silent server blanks nothing', async (t) => {
stub(t, {
polling: ['main', 'creative'],
list: (server) =>
server.id === 'creative'
? { ok: false, status: 'timeout' }
: {
ok: true,
data: {
leases: [
{ key: 'decay.scale', family: 'decay', current: '1' },
{ key: 'bear.population', family: 'population', current: '2' },
{ key: 'zombie.population', family: 'population', unreadable: 'this server has no convar' },
],
},
},
})
const rows = await source('rust.options.populations').resolve()
assert.deepStrictEqual(rows, [{ value: 'main/bear.population', label: 'bear.population', group: 'Main' }])
})
test('the group-permission source is searchable and narrows by the term', async (t) => {
stub(t, {
catalogue: () => ({
ok: true,
data: { permissions: ['kits.vip', 'Kits.Gold', 'zonemanager.admin'], groups: [{ name: 'default' }, { name: 'VIP' }] },
}),
})
const src = source('rust.options.grouppermissions')
assert.strictEqual(src.searchable, true)
const all = await src.resolve({ q: '' })
assert.strictEqual(all.length, 6)
const narrowed = await src.resolve({ q: 'kits' })
assert.deepStrictEqual(
narrowed.map((r) => r.value).sort(),
['main/default/kits.gold', 'main/default/kits.vip', 'main/vip/kits.gold', 'main/vip/kits.vip'],
)
})