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

@@ -152,6 +152,12 @@ function installStubs() {
Object.assign(existing, { state: 'ready', current_version_id: versionId, updated_by: userId })
}
}
definitionsDb.markDraftVerified = async (id, { at = new Date(), userId = null, hash }) => {
const existing = store.definitions.get(id)
if (existing) {
Object.assign(existing, { draft_verified_at: at, draft_verified_by: userId, draft_verified_hash: hash })
}
}
definitionsDb.archive = async (id, userId) => {
const existing = store.definitions.get(id)
if (existing) Object.assign(existing, { state: 'archived', updated_by: userId })
@@ -998,6 +1004,106 @@ test('a draft is verified against its working spec, and the pass is not recorded
assert.equal(res.body.recorded, false)
assert.equal(res.body.report.ok, true)
assert.equal(res.body.report.steps, 1)
// Kept for publish to compare against instead (W5).
assert.equal(res.body.draftRecorded, true)
})
// W5 (Rust PLAN_LAST_FIXES): an author who dry-ran the draft and then published
// found v1 "not dry run", and its scheduled and trigger starts held, until they
// ran the same check again.
test('a dry run that passed on the draft counts for the version published from it', async () => {
const { body } = await createDraft()
const id = body.event.id
await call(ctrl.verify, { user: EDITOR, params: { id: String(id) } })
const passedAt = store.definitions.get(id).draft_verified_at
const published = await call(ctrl.publish, { user: ADMIN, params: { id: String(id) } })
assert.equal(published.statusCode, 200)
assert.equal(published.body.verified, true)
// The reviewer and time are the dry run's, not the publisher's and now.
const version = store.versions.get(published.body.versionId)
assert.equal(version.verified_by, EDITOR.id)
assert.equal(version.verified_at, passedAt)
const after = await call(ctrl.get, { user: ADMIN, params: { id: String(id) } })
assert.ok(after.body.event.currentVersionVerifiedAt)
})
test('an edit after the draft dry run means nothing carries over', async () => {
const { body } = await createDraft()
const id = body.event.id
await call(ctrl.verify, { user: EDITOR, params: { id: String(id) } })
await call(ctrl.update, {
user: EDITOR,
params: { id: String(id) },
body: draftBody({
spec: {
schedule: { kind: 'manual' },
phases: [{ key: 'main', label: 'Main', steps: [announceStep('The gates open at midnight.')] }],
},
}),
})
const published = await call(ctrl.publish, { user: ADMIN, params: { id: String(id) } })
assert.equal(published.body.verified, false)
assert.equal(store.versions.get(published.body.versionId).verified_at, null)
})
test('a title edit after the draft dry run still carries it over, because the plan is the same', async () => {
// The fingerprint is the spec's, and the title is not part of the plan a dry
// run checks.
const { body } = await createDraft()
const id = body.event.id
await call(ctrl.verify, { user: EDITOR, params: { id: String(id) } })
await call(ctrl.update, { user: EDITOR, params: { id: String(id) }, body: { title: 'Renamed' } })
const published = await call(ctrl.publish, { user: ADMIN, params: { id: String(id) } })
assert.equal(published.body.verified, true)
})
test('a draft dry run with findings keeps nothing to carry over', async () => {
const { body } = await createDraft()
const id = body.event.id
await call(ctrl.saveAction, { user: ADMIN, body: { actionId: 'core.announce', enabled: false } })
const res = await call(ctrl.verify, { user: EDITOR, params: { id: String(id) } })
assert.equal(res.body.report.ok, false)
assert.equal(res.body.draftRecorded, false)
await call(ctrl.saveAction, { user: ADMIN, body: { actionId: 'core.announce', enabled: true } })
const published = await call(ctrl.publish, { user: ADMIN, params: { id: String(id) } })
assert.equal(published.body.verified, false)
})
test('the carried pass is written on the occurrences that publish re-pins', async () => {
// v1 published and scheduled, then v2 published from a draft that was
// dry-run first: the held occurrence moves to v2 and says why it may start.
const { body } = await createDraft()
const id = body.event.id
await call(ctrl.publish, { user: ADMIN, params: { id: String(id) } })
await call(ctrl.startRun, { user: ADMIN, params: { id: String(id) }, body: { scheduledFor: '2099-11-01T18:00:00.000Z' } })
// A ready definition dry-runs its published version, so the draft pass is
// planted the way a dry run on the draft would have left it.
const edited = draftBody({
spec: {
schedule: { kind: 'manual' },
phases: [{ key: 'main', label: 'Main', steps: [announceStep('Later.')] }],
},
})
await call(ctrl.update, { user: EDITOR, params: { id: String(id) }, body: edited })
const spec = require('../src/events/spec')
const saved = store.definitions.get(id).spec
const checked = spec.validate(saved, { knownActionIds: spec.actionIdsIn(saved) })
store.definitions.get(id).draft_verified_hash = spec.fingerprint(checked.spec)
store.definitions.get(id).draft_verified_by = EDITOR.id
const published = await call(ctrl.publish, { user: ADMIN, params: { id: String(id) } })
assert.equal(published.body.repinned, 1)
assert.equal(published.body.verified, true)
const line = store.log.find((e) => e.kind === 'version.verified')
assert.ok(line, 'the re-pinned occurrence says it may now start')
assert.equal(line.detail.fromDraft, true)
assert.equal(line.detail.versionId, published.body.versionId)
})
test('a ready definition is verified against the version that would actually run, and the pass IS recorded', async () => {