fix(events): give a lease's ledger row a reconcile path (Phase 11b)
A lease row had no reconcile path at all, and nothing failed to say so.
`cleanup.js` resolves a resource to the action of the step that made it, and for
a lease that action is `core.lease` -- a CORE action, on a path a module cannot
register anything on. So every `override` row came back `unanswered` for the life
of the run, and a lease the shard had quietly dropped (a config lease is
memory-only there, so a restart reverts it by design) stayed in the ledger as
live until teardown went hunting a baseline nobody was holding.
`core.lease` gains a `reconcile()`, and `registerEventLeases` gains an optional
`inForce()`: "does the game side still have any record of this hold?"
Deliberately not `read()` plus a comparison. A value that differs from what the
run applied is DRIFT, which teardown must deliver through `restore()` so the row
lands `drifted` with the current value beside it; a reconcile that inferred
absence from a changed value would orphan the row first and tell the operator the
lease vanished rather than that somebody moved it. Only an explicit
`{ ok: true, held: false }` takes a row out -- a throw, a timeout, an
unrecognised shape and a lease with no `inForce()` all leave the ledger alone.
MODULE_API_VERSION stays 1.10.0, amended in place.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -181,6 +181,7 @@ test('the catalog never carries a callable, whichever registration it came from'
|
||||
assert.equal(l.read, undefined)
|
||||
assert.equal(l.apply, undefined)
|
||||
assert.equal(l.restore, undefined)
|
||||
assert.equal(l.inForce, undefined)
|
||||
}
|
||||
for (const s of registries.allEventOptionSources()) assert.equal(s.resolve, undefined)
|
||||
})
|
||||
@@ -661,6 +662,48 @@ test('core registers the lease VERB and a module registers the lease', async ()
|
||||
assert.deepEqual(options.options, [{ value: 'demo.rate.gain', label: 'Gain rate', group: 'demo' }])
|
||||
})
|
||||
|
||||
test('inForce() is optional, and a fourth question rather than a fourth spelling of read()', () => {
|
||||
// Phase 11b. `read` is "what is it now", `apply` is "hold it here", `restore` is
|
||||
// "put it back and tell me if somebody moved it" -- and none of them answers
|
||||
// "does the game side still have any record of this hold?", which is what a
|
||||
// reconcile after an outage needs. A config lease is reverted by a shard restart
|
||||
// by design, so the answer changes without the value ever being written by us.
|
||||
//
|
||||
// Optional, because the fallback is core's posture everywhere: a lease that
|
||||
// cannot say leaves its ledger row alone.
|
||||
//
|
||||
// Three module IDs rather than three reloads of one: `loadModule` rewrites
|
||||
// `index.js` in place and clears the LOADER from the require cache, but not the
|
||||
// module file it goes on to require. A second load of the same id silently
|
||||
// re-registers the first source.
|
||||
const lease = (id, extra) => `module.exports = (ctx, api) => {
|
||||
api.registerEventLeases([{
|
||||
id: '${id}.rate.gain', label: 'Gain rate', type: 'float', min: 0.5, max: 5,
|
||||
maxDurationMs: 3600000,
|
||||
async read() { return { ok: true, value: 1 } },
|
||||
async apply() { return { ok: true } },
|
||||
async restore() { return { ok: true } },
|
||||
${extra}
|
||||
}])
|
||||
}`
|
||||
|
||||
const without = loadModule('leasea', lease('leasea', ''))
|
||||
assertRegistered(without)
|
||||
assert.equal(registries.eventLease('leasea.rate.gain').inForce, null)
|
||||
|
||||
const withIt = loadModule('leaseb', lease('leaseb', 'async inForce() { return { ok: true, held: true } },'))
|
||||
assertRegistered(withIt)
|
||||
assert.equal(typeof registries.eventLease('leaseb.rate.gain').inForce, 'function')
|
||||
|
||||
// And a declaration that is present but not callable fails the module rather
|
||||
// than being ignored -- the same posture the other three take. A module that
|
||||
// meant to answer and cannot is a module whose leases would silently never be
|
||||
// reconciled.
|
||||
const broken = loadModule('leasec', lease('leasec', "inForce: 'yes',"))
|
||||
assert.equal(broken.state, 'startup_failed')
|
||||
assert.match(broken.reason, /inForce must be a function/)
|
||||
})
|
||||
|
||||
test('core refuses a lease held longer than the module allows', async () => {
|
||||
// The bound is the MODULE's number and the enforcement is CORE's, which is the
|
||||
// §F split stated as one assertion. `retry: false` because a duration that is
|
||||
|
||||
Reference in New Issue
Block a user