// ── The resource ledger's write half (EVENTS_PLAN.md Phase 8) ────────────── // // §D's two rules, and rule 1 is the one this file exists for: **a resource is // recorded BEFORE it is confirmed.** The obstacle it works around is that a // spawn's serial does not exist until the module answers, so what goes in before // the dispatch is a placeholder keyed by the step's idempotency key — and the // property worth a test is that the placeholder SURVIVES an answer that never // comes, because that is the case where recording afterwards would have lost the // object for ever. // // The other rules here are about what core will and will not write down on a // module's say-so. Every one of them is fail-closed in a specific direction: // // • the reserved `@step` kind is core's and a module may not claim it // • an `override` must name a lease core knows how to give back, or core would // be recording something it has no way to restore // • a duplicate is "already recorded", not an error — a retry re-sends the same // idempotency key and a module may honestly report the same resources twice // • a badly shaped resource is dropped and LOGGED, never a failed step: the // step changed the world, and turning bookkeeping into a retry would re-run // a world write that already happened // // The db layer is stubbed with a store that enforces `uq_evres_target`, because // that refusal is behaviour the callers branch on rather than an implementation // detail. The SQL itself is `eventRunnerSql.test.js`'s, against a real MariaDB. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const registries = require('../src/modules/registries') const ledger = require('../src/events/ledger') const resourcesDb = require('../src/model/events/eventRunResources.db') const runsDb = require('../src/model/events/eventRuns.db') const db = require('../src/utils/db') after(() => db.close()) const HELD = ['pending', 'confirmed', 'reverting'] let store const originals = { resourcesDb: { ...resourcesDb }, runsDb: { ...runsDb } } beforeEach(() => { registries._reset() store = { rows: new Map(), next: 1, cleanupStatus: 'not_required' } resourcesDb.reserve = async ({ runId, stepId = null, owner, kind, ref, payload = null, leaseUntil = null, memberKey = null }) => { const holder = [...store.rows.values()].find( (r) => r.owner_module === owner && r.kind === kind && r.ref === ref && HELD.includes(r.status), ) if (holder) return { ok: false, code: 'held', holder: { run_id: holder.run_id, status: holder.status } } const id = store.next++ store.rows.set(id, { id, run_id: runId, step_id: stepId, owner_module: owner, kind, ref, payload, lease_until: leaseUntil, status: 'pending', revert_attempts: 0, last_error: null, member_key: memberKey, }) return { ok: true, id } } resourcesDb.confirm = async (id) => { const r = store.rows.get(id) if (!r || r.status !== 'pending') return false r.status = 'confirmed' return true } resourcesDb.resolvePlaceholder = async (id) => { const r = store.rows.get(id) if (!r || r.kind !== resourcesDb.STEP_KIND) return false r.status = 'reverted' return true } resourcesDb.findByTarget = async (owner, kind, ref) => [...store.rows.values()].reverse().find((r) => r.owner_module === owner && r.kind === kind && r.ref === ref) || null runsDb.setCleanupStatus = async (id, to, from = null) => { if (from && !from.includes(store.cleanupStatus)) return false store.cleanupStatus = to return true } }) afterEach(() => { Object.assign(resourcesDb, originals.resourcesDb) Object.assign(runsDb, originals.runsDb) registries._reset() }) const RUN = { id: 7 } const STEP = { id: 42, phase: 'invasion', seq: 0, idempotency_key: 'a'.repeat(40) } const action = (over = {}) => ({ id: 'demo.spawn', owner: 'demo', label: 'Spawn', risk: 'change', reversible: 'ledger', budgetMs: 1000, ...over, }) const rows = () => [...store.rows.values()] // ── Which actions ledger at all ──────────────────────────────────────────── test('only the two reversible classes core has to come back for are ledgered', () => { // `none` is gone once done, `self` undoes itself. Neither has anything core // could revert, and giving one a placeholder would put a row in the ledger that // teardown could never resolve — the exact reason `core.announce` is declared // `none` rather than `ledger`. assert.equal(ledger.ledgers(action({ reversible: 'ledger' })), true) assert.equal(ledger.ledgers(action({ reversible: 'override' })), true) assert.equal(ledger.ledgers(action({ reversible: 'none' })), false) assert.equal(ledger.ledgers(action({ reversible: 'self' })), false) }) test('only a ledger action gets a placeholder; an override reserves its own target', async () => { // The asymmetry is the design. A spawn's ref is unknown until the module // answers, so the placeholder stands in for it; a lease's target is the lease // id the step already names, so `core.lease` writes the real row before it // touches the world — which is rule 1 in a stronger form, and the only place // the two-events-one-target refusal can happen before the world has changed. assert.equal(typeof (await ledger.reserveStep(RUN, STEP, action())), 'number') assert.equal(await ledger.reserveStep(RUN, STEP, action({ id: 'demo.b', reversible: 'override' })), null) assert.equal(await ledger.reserveStep(RUN, STEP, action({ id: 'demo.c', reversible: 'none' })), null) assert.equal(rows().length, 1) assert.equal(rows()[0].kind, '@step') assert.equal(rows()[0].ref, STEP.idempotency_key) }) test('the first ledger row is what makes a run dirty, and only from not_required', async () => { assert.equal(store.cleanupStatus, 'not_required') await ledger.reserveStep(RUN, STEP, action()) assert.equal(store.cleanupStatus, 'pending') // A run whose sweep has already finished must not be walked back to `pending` // by a late row: only a human's cleanup re-opens it, and it does so // deliberately and with an actor on the log line. store.cleanupStatus = 'complete' await ledger.recordAnswer({ run: RUN, step: { ...STEP, id: 43, idempotency_key: 'b'.repeat(40) }, action: action(), placeholderId: null, resources: [{ kind: 'creature', ref: '0x1' }], }) assert.equal(store.cleanupStatus, 'complete') }) // ── Rule 1 ───────────────────────────────────────────────────────────────── test('a lost acknowledgement leaves the placeholder standing, which is the whole point', async () => { const placeholderId = await ledger.reserveStep(RUN, STEP, action()) // The dispatch timed out: no answer, so `recordAnswer` is never reached. This // is the case rule 1 exists for — record afterwards and the object the module // may well have created is invisible to cleanup for ever. assert.equal(rows()[0].status, 'pending') assert.equal(rows()[0].payload.action, 'demo.spawn') assert.ok(placeholderId) }) test('a retry reuses its own placeholder rather than writing a second', async () => { // An idempotency key is minted once per step and does not vary by attempt (§E), // so the second attempt's insert collides with the first attempt's row. Finding // it already there is the correct answer, and a second row would be a second // thing for cleanup to revert. const first = await ledger.reserveStep(RUN, STEP, action()) const second = await ledger.reserveStep(RUN, STEP, action()) assert.equal(first, second) assert.equal(rows().length, 1) }) test('the placeholder is resolved once the real rows exist', async () => { const placeholderId = await ledger.reserveStep(RUN, STEP, action()) const out = await ledger.recordAnswer({ run: RUN, step: STEP, action: action(), placeholderId, resources: [ { kind: 'creature', ref: '0x40001234' }, { kind: 'creature', ref: '0x40001235' }, ], }) assert.equal(out.recorded, 2) assert.deepEqual(out.rejected, []) assert.equal(store.rows.get(placeholderId).status, 'reverted') assert.deepEqual( rows().filter((r) => r.kind === 'creature').map((r) => [r.ref, r.status]), [['0x40001234', 'confirmed'], ['0x40001235', 'confirmed']], ) }) test('an action that ledgers and reports nothing still resolves its placeholder', async () => { // "I made nothing" is a real answer. Holding the placeholder open for it would // make cleanup call `revert()` on every terminal path, for ever, for a step that // has nothing to give back. const placeholderId = await ledger.reserveStep(RUN, STEP, action()) const out = await ledger.recordAnswer({ run: RUN, step: STEP, action: action(), placeholderId, resources: [] }) assert.equal(out.recorded, 0) assert.equal(store.rows.get(placeholderId).status, 'reverted') }) test('a module reporting the same resources twice produces one row', async () => { // The database is what makes recording idempotent: `uq_evres_target` refuses // the second insert and this file reads that as "already recorded". Without it // a retry against a module that honestly re-reports its work would double every // row cleanup then has to revert. const args = { run: RUN, step: STEP, action: action(), placeholderId: null, resources: [{ kind: 'creature', ref: '0x1' }] } await ledger.recordAnswer(args) const again = await ledger.recordAnswer(args) assert.equal(again.recorded, 0) assert.deepEqual(again.rejected, []) assert.equal(rows().filter((r) => r.kind === 'creature').length, 1) }) test('a target another RUN holds is rejected by name rather than silently skipped', async () => { await ledger.recordAnswer({ run: { id: 1 }, step: STEP, action: action(), placeholderId: null, resources: [{ kind: 'creature', ref: '0x1' }], }) const out = await ledger.recordAnswer({ run: { id: 2 }, step: { ...STEP, id: 99 }, action: action(), placeholderId: null, resources: [{ kind: 'creature', ref: '0x1' }], }) assert.equal(out.recorded, 0) assert.match(out.rejected.join('\n'), /already held by run 1/) }) // ── What core will not write down ────────────────────────────────────────── test('a module may not claim core\'s reserved kind', () => { // A module that could write a `@step` row could make its own step's placeholder // look resolved — which is the one row whose survival is the safety property. const bad = ledger.normalise({ kind: '@step', ref: 'x' }, 'demo.spawn') assert.equal(bad.ok, false) assert.match(bad.reason, /reserved kind/) }) test('an override must name a lease core knows how to give back', () => { // Core restores an `override` through the LEASE registry — that is the split §F // draws — so a ref naming nothing registered is a resource core would be // recording with no way to undo it. Refusing to record it is the fail-closed // direction: rule 2 is a promise core must not make and then break. assert.equal(ledger.normalise({ kind: 'override', ref: 'demo.rate' }, 'demo.x').ok, false) const api = registries.stage('demo') api.registerEventLeases([ { id: 'demo.rate', label: 'Rate', type: 'float', min: 0.5, max: 5, maxDurationMs: 3_600_000, read: async () => ({ ok: true, value: 1 }), apply: async () => ({ ok: true }), restore: async () => ({ ok: true }), }, ]) registries.apply(api.staged) assert.equal(ledger.normalise({ kind: 'override', ref: 'demo.rate' }, 'demo.x').ok, true) }) test('every bad shape is refused, and none of them is a retry', () => { // A badly shaped resource is the module's mistake rather than the world's, and // it will be just as badly shaped on the second attempt. They are dropped and // reported; the STEP still counts as done, because it is — something happened // in the world, and refusing to record it would be the one outcome worse than // recording it imperfectly. const bad = [ null, 'a string', ['an array'], { ref: 'x' }, // no kind { kind: 'creature' }, // no ref { kind: 'creature', ref: 'x'.repeat(200) }, { kind: 'k'.repeat(80), ref: 'x' }, { kind: 'creature', ref: 'x', memberKey: 'm'.repeat(200) }, { kind: 'creature', ref: 'x', until: 'not a date' }, ] for (const entry of bad) { assert.equal(ledger.normalise(entry, 'demo.spawn').ok, false, JSON.stringify(entry)) } }) test('a bad resource never fails the step it came from', async () => { const placeholderId = await ledger.reserveStep(RUN, STEP, action()) const out = await ledger.recordAnswer({ run: RUN, step: STEP, action: action(), placeholderId, resources: [{ kind: 'creature', ref: '0x1' }, { nonsense: true }], }) assert.equal(out.recorded, 1) assert.equal(out.rejected.length, 1) // And the placeholder is still resolved: the good row exists, and leaving the // placeholder open would ask the module to undo the step a second time. assert.equal(store.rows.get(placeholderId).status, 'reverted') }) test('a lease deadline and a member key ride through verbatim', async () => { const until = new Date('2026-09-04T00:00:00Z') await ledger.recordAnswer({ run: RUN, step: STEP, action: action(), placeholderId: null, resources: [{ kind: 'reward', ref: 'item-1', memberKey: 'Darrow', until, payload: { cliloc: 1234 } }], }) const row = rows()[0] assert.equal(row.member_key, 'Darrow') assert.equal(row.lease_until.getTime(), until.getTime()) assert.deepEqual(row.payload, { cliloc: 1234 }) // Opaque: core stores what the module said and never interprets it, which is // `ctx.teams.activity.push`'s exact treatment one registry along. assert.equal(row.kind, 'reward') assert.equal(row.owner_module, 'demo') })