// module-uo's half of protocol 7 part b (EVENTS_PLAN.md Phase 12b). // // What an event BORROWS — five targeted leases over two planes — and the two // one-shots that are neither borrowed nor owned. // // The tests below are the places where the obvious implementation is subtly the // wrong one and nothing would fail if it were written the other way: // // • every callable of a targeted lease must PASS THE TARGET ON. A read that // dropped it would answer about the wrong spawner, and a restore that // dropped it would write a baseline onto one // • a target the shard can no longer read is a REFUSAL at apply time, never a // value: taking the lease anyway records a fictional baseline and later // writes it onto whatever next holds that id // • a target that vanished mid-run is a SUCCESSFUL restore, not a failure — // there is nothing to give back, and reporting it failed leaves a ledger row // unresolved for ever over an object that is gone // • `inForce()` reads the frame's `holds`, which is the only thing that can // answer for a targeted key: there is no list of spawners to walk // • a grant that reached NOBODY is a success, because an event nobody attended // still happened — while a run the shard was never told to count is a 404 // • a non-stackable granted in quantity is refused at BOTH ends const { test, beforeEach, afterEach } = require('node:test') const assert = require('node:assert/strict') const uoLinkClient = require('../utils/uoLinkClient') const shardAtlas = require('../model/shardAtlas/shardAtlas.model') require('./_setup') const actions = require('../config/uoEventActions') const byId = (id) => actions.ACTIONS.find((a) => a.id === id) const leaseById = (id) => actions.LEASES.find((l) => l.id === id) const STUBBED = ['getLeases', 'applyLease', 'releaseLease', 'grantItem', 'saveWorld'] let calls let frame const saved = {} beforeEach(() => { calls = { leases: [], apply: [], release: [], grant: [], save: [] } frame = { leases: [{ key: 'Spawner.MaxCount', kind: 'property', current: '3', held: false }], holds: [], } for (const name of STUBBED) saved[name] = uoLinkClient[name] saved.listSpawners = shardAtlas.listSpawners uoLinkClient.getLeases = async (q) => { calls.leases.push(q) return { ok: true, status: 200, data: frame } } uoLinkClient.applyLease = async (b) => { calls.apply.push(b); return { ok: true, status: 200, data: {} } } uoLinkClient.releaseLease = async (b) => { calls.release.push(b); return { ok: true, status: 200, data: {} } } uoLinkClient.grantItem = async (b) => { calls.grant.push(b) return { ok: true, status: 200, data: { granted: 2, missed: [] } } } uoLinkClient.saveWorld = async (b) => { calls.save.push(b); return { ok: true, status: 200, data: {} } } shardAtlas.listSpawners = async (opts) => { calls.spawners = opts return [ { uniqueId: 'uid-1', name: 'fel orc fort', facet: 'Felucca', region: 'Britain', maxCount: 9 }, { uniqueId: 'uid-2', name: null, facet: 'Trammel', region: null, landmark: null, maxCount: 1 }, ] } }) afterEach(() => { for (const name of STUBBED) uoLinkClient[name] = saved[name] shardAtlas.listSpawners = saved.listSpawners }) // ── The targeted leases ──────────────────────────────────────────────────── test('every callable carries the target through to the shard', async () => { // The one thing that cannot be got wrong quietly. Core composes the ledger ref // as `#` and hands the target back on every call; a callable // that ignored it would read, apply to and restore whichever spawner the shard // happened to answer about, and nothing here or there would report an error. const lease = leaseById('uo.spawner.maxcount') const target = '003f11b8-9bfa-4587-991e-ca263004efe6' const read = await lease.read({ target }) assert.deepEqual(read, { ok: true, value: '3' }) assert.deepEqual(calls.leases[0], { key: 'Spawner.MaxCount', target }) await lease.apply('30', new Date(Date.now() + 600_000), { target }) assert.equal(calls.apply[0].key, 'Spawner.MaxCount') assert.equal(calls.apply[0].target, target) // A DURATION, not the deadline — 11b's rule, unchanged by targeting. A shard // whose clock runs fast would restore an absolute deadline the instant it // took it. assert.ok(calls.apply[0].holdMs > 0 && calls.apply[0].holdMs <= 600_000) await lease.restore('3', { expected: '30', target }) assert.deepEqual(calls.release[0], { key: 'Spawner.MaxCount', target, expected: '30', baseline: '3', }) }) test('a target the shard cannot read refuses the lease rather than defaulting', async () => { // The failure this guards is silent and permanent: a lease taken over a // spawner that is not there records whatever came back as the baseline, and // teardown then WRITES that baseline onto whatever next holds the id. frame.leases = [{ key: 'Spawner.MaxCount', unreadable: "nothing on this shard has serial 0x99" }] const refused = await leaseById('uo.spawner.maxcount').read({ target: '0x99' }) assert.equal(refused.ok, false) assert.match(refused.error, /nothing on this shard has serial/) // A row with neither a value nor a reason is refused too. The shard should // always send one of them, and "it sent neither" must not read as zero. frame.leases = [{ key: 'Spawner.MaxCount' }] const empty = await leaseById('uo.spawner.maxcount').read({ target: 'uid-1' }) assert.equal(empty.ok, false) assert.match(empty.error, /could not read/) }) test('a target that vanished mid-run is a successful restore, not a failure', async () => { // 12a's `gone` in the lease plane's vocabulary. Somebody deleted the spawner // while the run held it: there is nothing to give back and nothing is owed. // Reported as a failure it would sit in the ledger unresolved for ever, over // an object that no longer exists — and every sweep would try again. uoLinkClient.releaseLease = async () => ({ ok: true, status: 200, data: { kind: 'lease.ok', released: true, targetGone: true, reason: 'that object has been deleted' }, }) const done = await leaseById('uo.spawner.maxcount').restore('3', { expected: '30', target: 'uid-1' }) assert.deepEqual(done, { ok: true }) }) test('drift is still drift, and is still not an error', async () => { // Unchanged from 11b and asserted again because targeting rewrote the whole // callable: core records drift as a distinct SUCCESSFUL outcome, so an error // here would put the row on the retry ladder and eventually report the lease // as vanished rather than as somebody having moved it. uoLinkClient.releaseLease = async () => ({ ok: true, status: 200, data: { kind: 'lease.drifted', current: '12' }, }) const drifted = await leaseById('uo.spawner.maxcount').restore('3', { expected: '30', target: 'uid-1' }) assert.deepEqual(drifted, { ok: false, drifted: true, current: '12' }) }) test('inForce reads the holds list, which is the only thing that can answer', async () => { // A catalog walk can enumerate the KEYS but never the holds on a targeted one // — there is no list of spawners to walk — so the frame carries every hold the // shard has, and this is what reads it. const lease = leaseById('uo.spawner.maxcount') assert.deepEqual(await lease.inForce({ target: 'uid-1' }), { ok: true, held: false }) frame.holds = [{ key: 'Spawner.MaxCount', target: 'uid-1', runId: '7' }] assert.deepEqual(await lease.inForce({ target: 'uid-1' }), { ok: true, held: true }) // ...and it is the hold on THIS target, not any hold on the key. A run holding // one spawner must not make every other spawner look leased. assert.deepEqual(await lease.inForce({ target: 'uid-2' }), { ok: true, held: false }) }) test('a shard that cannot answer is never read as "the lease is gone"', async () => { // Core's posture everywhere: "I could not ask" must not be recorded as "it is // gone", because the second orphans the row and stops teardown ever trying. uoLinkClient.getLeases = async () => ({ ok: false, status: 503, data: null }) const answer = await leaseById('uo.spawner.maxcount').inForce({ target: 'uid-1' }) assert.equal(answer.ok, false) }) test('the seasonal lease is a three-value enum over eight events', () => { // §G called `SeasonalEventSystem.GetEntry(type).Status` "a nine-value enum" and // had it backwards: `EventStatus` has three values, `EventType` has nine // entries — and one of those nine is excluded, so it is eight. const lease = leaseById('uo.seasonal.status') assert.equal(lease.type, 'string') assert.deepEqual(lease.values, ['Inactive', 'Active', 'Seasonal']) assert.equal(actions.SEASONAL_EVENTS.length, 8) // TreasuresOfTokuno reads its own era rather than this status, so leasing it // would apply cleanly and change nothing — §N10's "a capability that lies", // and the one instance no runtime probe can catch. assert.ok(!actions.SEASONAL_EVENTS.includes('TreasuresOfTokuno')) }) test('every targeted lease bounds what it can hold', () => { // §F requires a range on the numeric types because, unlike a cap, a bad lease // value is in force the moment it is applied. Restated over the five because // they are built by a shared factory: one missing bound would be missing in a // way no single declaration shows. for (const lease of actions.LEASES) { if (lease.id === 'uo.playercaps.skillcap') continue assert.ok(lease.maxDurationMs > 0, `${lease.id} has no duration bound`) if (lease.type === 'int' || lease.type === 'float') { assert.ok(Number.isFinite(lease.min) && Number.isFinite(lease.max), `${lease.id} has no range`) assert.ok(lease.min <= lease.max, `${lease.id} has min above max`) } if (lease.type === 'string') { assert.ok(Array.isArray(lease.values) && lease.values.length, `${lease.id} has no value set`) } } }) // ── The spawner source ───────────────────────────────────────────────────── test('the spawner source searches, and says so', async () => { // The first source with more entries than a dropdown holds: 6,707 spawn points // against MAX_OPTIONS' 2,000. A flat list would drop two thirds of the world // and say nothing about which two thirds. const source = actions.OPTION_SOURCES.find((s) => s.id === 'uo.options.spawners') assert.equal(source.searchable, true) const rows = await source.resolve({ q: 'orc' }) assert.equal(calls.spawners.q, 'orc') assert.equal(calls.spawners.limit, actions.SPAWNER_OPTIONS) // The value is the UniqueId, because it is the only name for one particular // spawner that exists off the shard. assert.deepEqual(rows[0], { value: 'uid-1', label: 'fel orc fort', group: 'Britain' }) // A nameless spawner still answers, labelled by its id. It is still a spawner // somebody may need to turn down, and dropping it would be a dropdown quietly // missing rows again. assert.deepEqual(rows[1], { value: 'uid-2', label: 'uid-2', group: 'Trammel' }) }) // ── The one-shots ────────────────────────────────────────────────────────── test('a grant sends a run and never a recipient list', async () => { // The shard has held this run's participation ledger since it opened, keyed by // the same serials core stores as `member_key`. Sending a list would put it on // the wire twice with a window in which the two disagree — and would have // needed a core surface handing a module core's own participants. const out = await byId('uo.item.grant').perform({ runId: 7, idempotencyKey: 'k', params: { item: 'gold', amount: 500, where: 'bank' }, }) assert.equal(out.ok, true) assert.deepEqual(calls.grant[0], { runId: 7, item: 'gold', amount: 500, hue: undefined, name: undefined, where: 'bank', idempotencyKey: 'k', }) assert.equal(out.detail.granted, 2) }) test('a grant that reached nobody is a success', async () => { // An event nobody attended still happened. Reported as a failure the run would // retry against a ledger that will be just as empty next time, and pause. The // shard draws the distinction that matters: a run it was never told to count // is a 404, which fails below. uoLinkClient.grantItem = async () => ({ ok: true, status: 200, data: { granted: 0, missed: [] } }) const out = await byId('uo.item.grant').perform({ runId: 7, idempotencyKey: 'k', params: { item: 'gold', amount: 1 }, }) assert.equal(out.ok, true) assert.equal(out.detail.granted, 0) uoLinkClient.grantItem = async () => ({ ok: false, status: 404, data: { reason: 'run 7 has no participation ledger open on this shard' }, }) const missing = await byId('uo.item.grant').perform({ runId: 7, idempotencyKey: 'k', params: { item: 'gold', amount: 1 }, }) assert.equal(missing.ok, false) // 404 is permanent: the ledger will not appear because we asked again. assert.equal(missing.retry, false) }) test('a non-stackable granted in quantity is refused before the wire', async () => { // Five cloaks would be five items — five chances to overflow a backpack // halfway through with no way to say which half landed. Refused here so the // author sees it on the form, and refused again on the shard because this copy // of the allowlist is the one that can be wrong. const out = await byId('uo.item.grant').perform({ runId: 7, idempotencyKey: 'k', params: { item: 'cloak', amount: 3 }, }) assert.equal(out.ok, false) assert.equal(out.retry, false) assert.match(out.error, /does not stack/) assert.equal(calls.grant.length, 0) const unknown = await byId('uo.item.grant').perform({ runId: 7, idempotencyKey: 'k', params: { item: 'castle', amount: 1 }, }) assert.equal(unknown.ok, false) assert.equal(unknown.retry, false) assert.equal(calls.grant.length, 0) }) test('a grant is retryable, and protocol 6 is the reason', async () => { // §G called a grant un-retryable because a lost acknowledgement and a grant // that never applied were the same event — the argument that made // `uo.broadcast` answer `retry: false` in Phase 9. An idempotency key closes // it: a repeat is answered by the original reply, so a retried grant cannot be // one winner receiving two. uoLinkClient.grantItem = async () => ({ ok: false, status: 503, data: null }) const out = await byId('uo.item.grant').perform({ runId: 7, idempotencyKey: 'k', params: { item: 'gold', amount: 1 }, }) assert.equal(out.ok, false) assert.notEqual(out.retry, false) // And the action declares itself irreversible, which is the honest class: the // world is altered and cannot be put back. assert.equal(byId('uo.item.grant').risk, 'irreversible') assert.equal(byId('uo.item.grant').reversible, 'none') }) test('a save refused for coming too soon is retried, not abandoned', async () => { // 429 is the shard's rate limit and is the one refusal on this plane that // waiting fixes. It is deliberately not in PERMANENT_STATUSES, so a phase // boundary is retried rather than dropped. assert.ok(!actions.PERMANENT_STATUSES.has(429)) uoLinkClient.saveWorld = async () => ({ ok: false, status: 429, data: { reason: 'this shard saves at most every 300 seconds, and the last save was 12 seconds ago' }, }) const out = await byId('uo.world.save').perform({ idempotencyKey: 'k' }) assert.equal(out.ok, false) assert.notEqual(out.retry, false) }) test('a save reports only that it started', async () => { // What actually happened rides `world.save.before`/`after` on the event stream. // Asserting anything more here would be asserting something the reply does not // know. const out = await byId('uo.world.save').perform({ idempotencyKey: 'k' }) assert.deepEqual(out, { ok: true, detail: { started: true } }) assert.deepEqual(calls.save[0], { idempotencyKey: 'k' }) })