fix(core): the last fixes' core half (W1 W2 W5 W19)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 49s
PR Checks / client-build (pull_request) Successful in 1m4s
PR Checks / server-tests (pull_request) Successful in 6m6s

Stage 4 of docs/modules/rust/PLAN_LAST_FIXES.md, with the org lead's
2026-10-22 decisions.

W19: one module per site at boot, as at install. The first module
directory by name holds the site, whether or not it then loads; every
other is recorded startup_failed at the manifest stage, before a line of
its code is required, with a reason naming the module that holds the
site. Module-uo and Module-Rust side by side used to fail the second
with a slot collision. Loader, lifecycle, schema and event-contract
tests that loaded several fixture modules now load each case alone.

W1: a reconcile files a resource the game no longer has, and that is
past its `until`, as expired rather than orphaned, with the same log
line expired() writes. No MODULE_API bump: `until` and reconcile's
answer are unchanged.

W5: a dry run that passes on the draft is kept with the fingerprint of
the validated spec (three nullable columns on event_definitions).
Publish stamps the new version verified, with that dry run's reviewer
and time, when the spec it freezes has the same fingerprint, and writes
version.verified on the occurrences it re-pins. Verify answers
draftRecorded and publish answers verified; OpenAPI regenerated.

W2: the run console says "Phase p1: steps done, waiting to advance" and
"Why this phase has not advanced", not "has not started".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-22 00:38:34 -05:00
parent 9a1654fc54
commit f40dd765c3
18 changed files with 632 additions and 210 deletions

View File

@@ -478,7 +478,7 @@ test('a resource the module no longer has becomes orphaned, never reverted', asy
addResource({ ref: '0x2' })
const summary = await cleanup.reconcileModule('demo')
assert.deepEqual(summary, { asked: 2, inForce: 1, orphaned: 1, unanswered: 0 })
assert.deepEqual(summary, { asked: 2, inForce: 1, orphaned: 1, expired: 0, unanswered: 0 })
assert.equal([...store.rows.values()].find((r) => r.ref === '0x2').status, 'orphaned')
assert.equal([...store.rows.values()].find((r) => r.ref === '0x1').status, 'confirmed')
assert.ok(store.log.some((e) => e.kind === 'resource.orphaned'))
@@ -510,7 +510,7 @@ test('a module with no reconcile is not broken; core keeps believing its ledger'
addStep(1, 'demo.spawn')
addResource()
const summary = await cleanup.reconcileModule('demo')
assert.deepEqual(summary, { asked: 0, inForce: 0, orphaned: 0, unanswered: 1 })
assert.deepEqual(summary, { asked: 0, inForce: 0, orphaned: 0, expired: 0, unanswered: 1 })
assert.equal([...store.rows.values()][0].status, 'confirmed')
})
@@ -532,7 +532,7 @@ test('placeholders are not asked about, because there is nothing to ask yet', as
addStep(1, 'demo.spawn')
addResource({ kind: resourcesDb.STEP_KIND, ref: 'a'.repeat(40), status: 'pending' })
const summary = await cleanup.reconcileModule('demo')
assert.deepEqual(summary, { asked: 0, inForce: 0, orphaned: 0, unanswered: 0 })
assert.deepEqual(summary, { asked: 0, inForce: 0, orphaned: 0, expired: 0, unanswered: 0 })
assert.equal([...store.rows.values()][0].status, 'pending')
})
@@ -556,7 +556,7 @@ test('a lease row had no reconcile path at all until core.lease grew one', async
addResource({ kind: 'override', ref: 'demo.rate', payload: { baseline: 1, applied: 3 } })
const summary = await cleanup.reconcileModule('demo')
assert.deepEqual(summary, { asked: 1, inForce: 0, orphaned: 1, unanswered: 0 })
assert.deepEqual(summary, { asked: 1, inForce: 0, orphaned: 1, expired: 0, unanswered: 0 })
assert.equal([...store.rows.values()][0].status, 'orphaned')
})
@@ -566,7 +566,7 @@ test('a lease the shard still has a record of stays put', async () => {
addResource({ kind: 'override', ref: 'demo.rate', payload: { baseline: 1, applied: 3 } })
const summary = await cleanup.reconcileModule('demo')
assert.deepEqual(summary, { asked: 1, inForce: 1, orphaned: 0, unanswered: 0 })
assert.deepEqual(summary, { asked: 1, inForce: 1, orphaned: 0, expired: 0, unanswered: 0 })
assert.equal([...store.rows.values()][0].status, 'confirmed')
})
@@ -705,6 +705,61 @@ test('an expiry nobody ledgered, or a malformed one, is not an error', async ()
assert.equal(store.log.length, 0)
})
// W1 (Rust PLAN_LAST_FIXES): on a reconnect the module's reconcile can run
// before the game's backlog reports an expiry. A row past its `until` that the
// game no longer has is the deadline working, so it is filed expired at once
// rather than orphaned and then expired five seconds later.
test('a reconcile files a row gone past its until as expired, not orphaned', async () => {
stubExpiry()
registerAction({ reconcile: async () => ({ ok: true, inForce: [] }) })
addStep(1, 'demo.spawn')
const now = new Date('2026-10-22T14:06:35Z')
addResource({ kind: 'zone', ref: 'past', lease_until: new Date('2026-10-22T14:06:00Z') })
addResource({ kind: 'zone', ref: 'future', lease_until: new Date('2026-10-22T14:30:00Z') })
addResource({ kind: 'zone', ref: 'none' })
const summary = await cleanup.reconcileModule('demo', now)
assert.deepEqual(summary, { asked: 3, inForce: 0, orphaned: 2, expired: 1, unanswered: 0 })
const status = (ref) => [...store.rows.values()].find((r) => r.ref === ref).status
assert.equal(status('past'), 'expired')
// Before its deadline, or with none, gone is still nobody-was-looking.
assert.equal(status('future'), 'orphaned')
assert.equal(status('none'), 'orphaned')
assert.deepEqual(
store.log.filter((e) => e.detail.resource === 'zone:past').map((e) => e.kind),
['resource.expired'],
)
// The game's own word arriving afterwards finds nothing left to close.
assert.deepEqual(await cleanup.expireResource('demo', { kind: 'zone', ref: 'past' }), { expired: 0 })
assert.equal(store.log.filter((e) => e.detail.resource === 'zone:past').length, 1)
})
test('a row past its until that the game still has stays in force', async () => {
// A deadline is the game's promise, not core's clock: a zone the game has not
// erased yet is still up, whatever core's time says.
stubExpiry()
registerAction({ reconcile: async () => ({ ok: true, inForce: ['late'] }) })
addStep(1, 'demo.spawn')
addResource({ kind: 'zone', ref: 'late', lease_until: new Date('2026-10-22T14:00:00Z') })
const summary = await cleanup.reconcileModule('demo', new Date('2026-10-22T14:06:35Z'))
assert.equal(summary.inForce, 1)
assert.equal([...store.rows.values()][0].status, 'confirmed')
})
test('a reconcile that expires the last row of a finished run completes its cleanup', async () => {
stubExpiry()
registerAction({ reconcile: async () => ({ ok: true, inForce: [] }) })
addStep(1, 'demo.spawn')
addResource({ kind: 'zone', ref: 'x', lease_until: new Date('2026-10-22T14:00:00Z') })
store.cleanupStatus = 'incomplete'
await cleanup.reconcileModule('demo', new Date('2026-10-22T14:06:35Z'))
assert.equal(store.cleanupStatus, 'complete')
})
test('the last row of a finished run expiring completes an incomplete cleanup', async () => {
stubExpiry()
addResource({ kind: 'zone', ref: 'x' })