// ── The dry run (EVENTS_PLAN.md Phase 6, EVENTS.md §I) ───────────────────── // // *"Materialise the steps, dispatch each with `verify: true`, report what would // happen and what it would cost against the caps."* // // **The finding worth the most is the one no other path can make.** Every // per-step check here is also made at save or at dispatch; the TOTAL is not. // Three steps each spawning 15 under a cap of 30 pass every individual check and // breach the cap on the third — at two in the morning, unattended, with the world // half-changed. Adding the costs up across the whole version is the thing only a // look at the plan as a whole can do, and it is why a dry run is worth more than // the sum of its step checks. // // The second property this file holds is the one §I states outright: **`verify: // true` must change nothing and must answer honestly.** So the actions here // record what they were asked and assert that the flag arrived — a dry run that // silently dispatched for real is the single worst bug this feature could ship, // and it would look exactly like a passing test otherwise. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const registries = require('../src/modules/registries') const { verifySpec } = require('../src/events/verify') const settingsDb = require('../src/model/events/eventActionSettings.db') const db = require('../src/utils/db') after(() => db.close()) const originals = { ...settingsDb } let store let seen beforeEach(() => { registries._reset() store = new Map() seen = [] settingsDb.byIds = async (ids) => new Map([...new Set(ids || [])].filter((i) => store.has(i)).map((i) => [i, store.get(i)])) settingsDb.get = async (id) => store.get(id) || null }) afterEach(() => { Object.assign(settingsDb, originals) registries._reset() }) const register = (entries, owner = 'test') => { const api = registries.stage(owner) api.registerEventActions(entries) registries.apply(api.staged) } const setSetting = (id, enabled, caps = {}) => store.set(id, { action_id: id, enabled: enabled ? 1 : 0, caps }) /** An action that records every envelope it is handed and then answers `answer`. */ const recorder = (id, over = {}) => ({ id, label: over.label || id, risk: over.risk || 'notify', reversible: 'none', version: over.version || 1, params: over.params || [], ...(over.cost ? { cost: over.cost } : {}), async perform(envelope) { seen.push({ id, ...envelope }) if (over.throws) throw new Error(over.throws) return over.answer || { ok: true } }, }) const spec = (steps) => ({ phases: [{ key: 'one', steps }] }) const step = (actionId, params = {}, extra = {}) => ({ actionId, params, ...extra }) const ADMIN = { id: 1, role: 'admin' } const EDITOR = { id: 2, role: 'editor' } test('every step is dispatched with verify true, and nothing is asked to act', async () => { // §I's promise, held as an assertion about the envelope each action received. register([recorder('test.tell'), recorder('test.wait', { risk: 'inspect' })]) const report = await verifySpec(spec([step('test.tell', { body: 'x' }), step('test.wait')]), { user: ADMIN }) assert.equal(report.ok, true) assert.equal(report.steps, 2) assert.deepEqual(report.findings, []) assert.equal(seen.length, 2) for (const envelope of seen) assert.equal(envelope.verify, true) }) test('the cost of the whole plan is added up across steps, and a total over the cap is a finding', async () => { // The one check that only exists here. Each of the three steps fits under 30 on // its own; together they do not. register([recorder('test.spawn', { risk: 'change', cost: (p) => ({ 'x.creatures': p.count }) })]) setSetting('test.spawn', true, { 'x.creatures': 30 }) const report = await verifySpec( spec([ step('test.spawn', { count: 15 }), step('test.spawn', { count: 15 }), step('test.spawn', { count: 15 }), ]), { user: ADMIN }, ) assert.equal(report.ok, false) const total = report.findings.find((f) => f.code === 'cap-total') assert.ok(total, 'the whole-plan total must be its own finding') assert.match(total.message, /asks for 45 of "x.creatures" across all its steps, and this deployment allows 30/) assert.deepEqual(report.cost, [ { dimension: 'x.creatures', total: 45, cap: 30, from: 'test.spawn', over: true }, ]) }) test('a plan that fits reports its cost without a finding', async () => { register([recorder('test.spawn', { risk: 'change', cost: (p) => ({ 'x.creatures': p.count }) })]) setSetting('test.spawn', true, { 'x.creatures': 30 }) const report = await verifySpec(spec([step('test.spawn', { count: 12 }), step('test.spawn', { count: 8 })]), { user: ADMIN, }) assert.equal(report.ok, true) assert.deepEqual(report.cost, [{ dimension: 'x.creatures', total: 20, cap: 30, from: 'test.spawn', over: false }]) }) test('an uncapped dimension is reported with its total and no cap', async () => { // Worth showing rather than hiding: "this event will spawn 40 creatures and // nothing bounds that" is exactly what an operator opening the switchboard // wants to have seen first. register([recorder('test.spawn', { risk: 'change', cost: () => ({ 'x.creatures': 40 }) })]) setSetting('test.spawn', true, {}) const report = await verifySpec(spec([step('test.spawn')]), { user: ADMIN }) assert.equal(report.ok, true) assert.deepEqual(report.cost, [{ dimension: 'x.creatures', total: 40, cap: null, from: null, over: false }]) }) test('a step naming an action nobody registers is a dormant finding, and the rest are still checked', async () => { // Reported rather than thrown, so an author sees EVERY problem in one pass. A // verification that stopped at the first finding would make fixing a // twelve-step definition twelve round trips. register([recorder('test.tell')]) const report = await verifySpec(spec([step('gone.away'), step('test.tell')]), { user: ADMIN }) assert.equal(report.ok, false) assert.equal(report.findings.length, 1) assert.equal(report.findings[0].code, 'dormant') assert.equal(report.findings[0].actionId, 'gone.away') assert.equal(seen.length, 1, 'the registered step is still dispatched') }) test('the caller’s own role is what the report answers against', async () => { // The value of doing it here: an editor is told a step needs an administrator // at the moment they can still do something about it, rather than at the moment // it does not run. register([recorder('test.change', { risk: 'change', label: 'Change things' })]) setSetting('test.change', true) const asEditor = await verifySpec(spec([step('test.change')]), { user: EDITOR }) assert.equal(asEditor.ok, false) assert.equal(asEditor.findings[0].code, 'role') const asAdmin = await verifySpec(spec([step('test.change')]), { user: ADMIN }) assert.equal(asAdmin.ok, true) }) test('a disabled action is a finding, and its step is never dispatched', async () => { // The order matters: dispatching a disabled action under `verify: true` would // change nothing, but it would call code the deployment has switched off, and // "we only ran it to ask whether we could run it" is not a defence anyone wants // to make. register([recorder('test.change', { risk: 'change' })]) const report = await verifySpec(spec([step('test.change')]), { user: ADMIN }) assert.equal(report.findings[0].code, 'disabled') assert.equal(seen.length, 0) }) test('the module’s own refusal is a finding in the module’s words', async () => { // The half core cannot compute: whether the landmark exists, whether the // creature is on the allowlist, whether the shard is reachable at all. register([recorder('test.spawn', { answer: { ok: false, retry: false, error: 'no such landmark "Bratain"' } })]) const report = await verifySpec(spec([step('test.spawn')]), { user: ADMIN }) assert.equal(report.ok, false) assert.equal(report.findings[0].code, 'refused') assert.match(report.findings[0].message, /no such landmark/) }) test('an action that throws under verify is a finding, not a 500 on the author’s screen', async () => { register([recorder('test.spawn', { throws: 'exploded' })]) const report = await verifySpec(spec([step('test.spawn')]), { user: ADMIN }) assert.equal(report.ok, false) assert.equal(report.findings[0].code, 'refused') assert.match(report.findings[0].message, /exploded/) }) test('a step authored against an older action version is a WARNING, not an error', async () => { // §F: a bump makes the editor render a warning rather than refusing to run. The // level is the whole point — a warning does not hold a scheduled start, and an // error does. register([recorder('test.tell', { version: 3 })]) const report = await verifySpec(spec([step('test.tell', {}, { actionVersion: 1 })]), { user: ADMIN }) assert.equal(report.ok, true, 'a drift warning must not fail the dry run') assert.equal(report.findings[0].level, 'warning') assert.equal(report.findings[0].code, 'version-drift') assert.match(report.findings[0].message, /authored against version 1/) }) test('a finding says which phase and which step it is about', async () => { // A twelve-step definition needs the finding anchored, or the author is left // reading the message and counting rows. register([recorder('test.tell')]) const twoPhases = { phases: [ { key: 'open', steps: [step('test.tell')] }, { key: 'close', steps: [step('test.tell'), step('gone.away')] }, ], } const report = await verifySpec(twoPhases, { user: ADMIN }) assert.equal(report.steps, 3) assert.deepEqual( report.findings.map((f) => ({ phase: f.phase, seq: f.seq })), [{ phase: 'close', seq: 1 }], ) }) test('an empty spec verifies clean and costs nothing', async () => { const report = await verifySpec({ phases: [] }, { user: ADMIN }) assert.deepEqual(report, { ok: true, steps: 0, findings: [], cost: [] }) })