fix(core): the last fixes' core half (W1 W2 W5 W19)
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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user