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

@@ -143,10 +143,9 @@ beforeEach(() => {
// ── The happy path ─────────────────────────────────────────────────────────
test('every module is recorded, booted with its ctx and marked started, in scan order', async () => {
test('the site\'s module is recorded, booted with its ctx and marked started', async () => {
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', log: file })
writeModule('bbb', { boot: '', log: file })
const loader = freshLoader(tmpRoot)
const model = fakeModel()
@@ -155,15 +154,35 @@ test('every module is recorded, booted with its ctx and marked started, in scan
// The reconcile order IS the design (§2.4): clear the last boot's outcomes
// first, so what is on display afterwards is what this boot did.
assert.equal(model.calls[0], 'beginBoot')
assert.deepEqual(model.calls.slice(1, 3), ['recordInstalled:aaa', 'recordInstalled:bbb'])
assert.deepEqual(noted(file), ['boot:aaa:ctx', 'boot:bbb:ctx'])
assert.equal(model.calls[1], 'recordInstalled:aaa')
assert.deepEqual(noted(file), ['boot:aaa:ctx'])
assert.equal(stateOf(loader, 'aaa').state, 'started')
assert.equal(model.rows.get('bbb').state, 'started')
assert.equal(model.rows.get('aaa').state, 'started')
// §2.4's metadata refresh: the row carries what the admin screen shows.
assert.equal(model.rows.get('aaa').name, 'Module aaa')
assert.equal(model.rows.get('aaa').version, '1.2.3')
})
test('a second module on the volume gets a failed row and is never booted', async () => {
// W19. Hand-copied beside the site's module, it still needs a row: without one
// Admin → Modules could not say why it is not running.
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', log: file })
writeModule('bbb', { boot: '', log: file })
const loader = freshLoader(tmpRoot)
const model = fakeModel()
await lifecycle.boot({ modules: loader, model })
assert.deepEqual(noted(file), ['boot:aaa:ctx'])
assert.equal(model.rows.get('aaa').state, 'started')
const refused = model.rows.get('bbb')
assert.equal(refused.state, 'startup_failed')
assert.equal(refused.failureStage, 'manifest')
assert.match(refused.failureReason, /already runs the module "aaa", and a site runs one module/)
assert.equal(refused.name, 'Module bbb')
})
test('a hand-placed directory gets a row with no provenance', async () => {
// §2.5 keeps a directory dropped on the volume by hand a supported install.
// Without a row it could never be disabled, and nothing could report it.
@@ -193,23 +212,19 @@ test('a module with no onBoot still reaches started', async () => {
// ── Failure is a state ─────────────────────────────────────────────────────
test('an onBoot that throws fails its own module and no one else', async () => {
test('an onBoot that throws is a failed state, not a failed boot', async () => {
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', log: file })
writeModule('bbb', { boot: 'throw new Error("cache warm-up failed")', log: file })
writeModule('ccc', { boot: '', log: file })
const loader = freshLoader(tmpRoot)
const model = fakeModel()
await lifecycle.boot({ modules: loader, model })
await assert.doesNotReject(() => lifecycle.boot({ modules: loader, model }))
assert.deepEqual(noted(file), ['boot:bbb:ctx'])
assert.equal(stateOf(loader, 'bbb').state, 'startup_failed')
assert.equal(stateOf(loader, 'bbb').stage, 'boot')
assert.match(stateOf(loader, 'bbb').reason, /cache warm-up failed/)
assert.equal(model.rows.get('bbb').failureStage, 'boot')
// The one that matters: the module AFTER the failure still booted.
assert.deepEqual(noted(file), ['boot:aaa:ctx', 'boot:bbb:ctx', 'boot:ccc:ctx'])
assert.equal(stateOf(loader, 'ccc').state, 'started')
})
test('a module whose onBoot failed keeps its URLs and answers 503', async () => {
@@ -333,25 +348,28 @@ test('a process that never scanned writes nothing at all', async () => {
// ── Shutdown ───────────────────────────────────────────────────────────────
test('shutdown runs started modules in reverse order and skips the rest', async () => {
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', shutdown: '', log: file })
writeModule('bbb', { boot: 'throw new Error("no")', shutdown: '', log: file })
writeModule('ccc', { boot: '', shutdown: '', log: file })
const loader = freshLoader(tmpRoot)
test('shutdown runs a started module and skips one whose boot failed', async () => {
for (const [boot, expected] of [
['', ['shutdown:aaa']],
// Its onBoot threw, so it has a half-built world that its onShutdown was
// never written to tear down.
['throw new Error("no")', []],
]) {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rg-lifecycle-'))
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot, shutdown: '', log: file })
const loader = freshLoader(tmpRoot)
await lifecycle.boot({ modules: loader, model: fakeModel() })
fs.writeFileSync(file, '') // only the shutdown half is under test
await lifecycle.shutdown({ modules: loader })
await lifecycle.boot({ modules: loader, model: fakeModel() })
fs.writeFileSync(file, '') // only the shutdown half is under test
await lifecycle.shutdown({ modules: loader })
// Reverse of boot order, and `bbb` absent: its onBoot threw, so it has a
// half-built world that its onShutdown was never written to tear down.
assert.deepEqual(noted(file), ['shutdown:ccc', 'shutdown:aaa'])
assert.deepEqual(noted(file).filter(Boolean), expected)
}
})
test('a hook that hangs costs its budget, not the shutdown', async () => {
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', shutdown: '', log: file })
writeModule('zzz', { boot: '', shutdown: 'await new Promise(() => {})', log: file })
const loader = freshLoader(tmpRoot)
@@ -361,15 +379,14 @@ test('a hook that hangs costs its budget, not the shutdown', async () => {
const started = Date.now()
await lifecycle.shutdown({ modules: loader, budgetMs: 50 })
// zzz never returns; it is abandoned and aaa still gets its turn. The
// alternative is a host where `systemctl stop` hangs until SIGKILL.
assert.deepEqual(noted(file), ['shutdown:zzz', 'shutdown:aaa'])
// zzz never returns, and is abandoned. The alternative is a host where
// `systemctl stop` hangs until SIGKILL.
assert.deepEqual(noted(file), ['shutdown:zzz'])
assert.ok(Date.now() - started < 2000, 'shutdown must not wait on a hung hook')
})
test('a hook that throws does not stop the ones behind it', async () => {
test('a hook that throws does not fail the shutdown', async () => {
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', shutdown: '', log: file })
writeModule('zzz', { boot: '', shutdown: 'throw new Error("close failed")', log: file })
const loader = freshLoader(tmpRoot)
@@ -377,7 +394,7 @@ test('a hook that throws does not stop the ones behind it', async () => {
fs.writeFileSync(file, '')
await assert.doesNotReject(() => lifecycle.shutdown({ modules: loader }))
assert.deepEqual(noted(file), ['shutdown:zzz', 'shutdown:aaa'])
assert.deepEqual(noted(file), ['shutdown:zzz'])
})
// ── stop(): the admin panel's Disable ──────────────────────────────────────
@@ -385,10 +402,9 @@ test('a hook that throws does not stop the ones behind it', async () => {
// Phase 4, §2.7.2 decision 3. Phase 2's disable moved a record and left the
// module running; the whole point of these tests is that it no longer does.
test('stop runs that one module\'s onShutdown and leaves the others alone', async () => {
test('stop runs the module\'s onShutdown and disables it', async () => {
const file = path.join(tmpRoot, 'log.txt')
writeModule('aaa', { boot: '', shutdown: '', log: file })
writeModule('bbb', { boot: '', shutdown: '', log: file })
const loader = freshLoader(tmpRoot)
const model = fakeModel()
@@ -398,10 +414,8 @@ test('stop runs that one module\'s onShutdown and leaves the others alone', asyn
const result = await lifecycle.stop('aaa', { modules: loader, model })
assert.deepEqual(result, { stopped: true, error: null })
// Only aaa. This is the difference from shutdown(), which runs everything.
assert.deepEqual(noted(file), ['shutdown:aaa'])
assert.equal(stateOf(loader, 'aaa').state, 'disabled')
assert.equal(stateOf(loader, 'bbb').state, 'started', 'the other module keeps running')
assert.equal(model.rows.get('aaa').state, 'disabled')
})