feat(events): enablement, per-run caps and mayInvoke (Phase 6)
Two new tables — event_action_settings (the deployment switchboard) and event_run_budget (what a run has spent and the most it may) — plus verified_at and verified_by on event_versions. The whole authorisation decision moves behind one function, events/authorize.js: role, enablement, cap, and the shard's own switch named as the layer core deliberately does not duplicate. Three routes, none moved: GET/PUT /admin/events/actions (admin in both directions) and POST /admin/events/:id/verify (admin, editor — a dry run dispatches nothing). Four decisions, settled by the org lead 2026-09-03: - The default-off line falls between inspect and change, not between notify and inspect. Read literally, §K shipped core.wait disabled. The same line is the role floor. - The tightest cap wins where two actions spend one dimension, pinned into the run at creation with the action it came from. - A refusal follows the step's on_failure and takes health to degraded — its own status and its own log kind, because a refusal is not an outage. - The verify gate is enforced for scheduled starts only: a human pressing Start now is the review the gate exists to require. Derived and flagged for review: a dry run fails rather than warns on a disabled action or an over-cap plan, and the unattended path does not re-check the starter's role. +111 tests (1921/1847/73/1 — the one failure pre-existing and environmental), including a 403 walk over the real router and two concurrent spends against one cap on a real MariaDB. The live walk found two defects, both fixed here: the run console route dropped the budget it was handed, and the role refusal used a plural verb over a one-item list. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
This commit is contained in:
@@ -32,6 +32,11 @@ const definitionsDb = require('../src/model/events/eventDefinitions.db')
|
||||
const runsDb = require('../src/model/events/eventRuns.db')
|
||||
const stepsDb = require('../src/model/events/eventRunSteps.db')
|
||||
const logDb = require('../src/model/events/eventRunLog.db')
|
||||
// Phase 6: `runsModel.create` prices the version against the switchboard and
|
||||
// seeds the run's budget, so expansion now reaches two more tables. Unstubbed
|
||||
// they are a ten-second ECONNREFUSED per occurrence.
|
||||
const settingsDb = require('../src/model/events/eventActionSettings.db')
|
||||
const budgetDb = require('../src/model/events/eventRunBudget.db')
|
||||
const versionsDb = require('../src/model/events/eventVersions.db')
|
||||
const db = require('../src/utils/db')
|
||||
|
||||
@@ -55,6 +60,8 @@ for (const [name, mod] of [
|
||||
['stepsDb', stepsDb],
|
||||
['logDb', logDb],
|
||||
['versionsDb', versionsDb],
|
||||
['settingsDb', settingsDb],
|
||||
['budgetDb', budgetDb],
|
||||
]) {
|
||||
originals[name] = { mod, fns: { ...mod } }
|
||||
}
|
||||
@@ -88,6 +95,14 @@ function addDefinition(id, overrides = {}) {
|
||||
definition_id: id,
|
||||
version: 1,
|
||||
spec: definition.spec,
|
||||
// Verified by default (Phase 6). §K holds a scheduled occurrence of a version
|
||||
// nobody has dry-run, so an unverified fixture would make every test in this
|
||||
// file assert nothing about recurrence and everything about that one gate.
|
||||
// The gate has its own test below, where an occurrence is what is being
|
||||
// measured rather than what is in the way.
|
||||
verified_at: new Date('2026-08-01T00:00:00Z'),
|
||||
verified_by: 1,
|
||||
...(overrides.version || {}),
|
||||
})
|
||||
return definition
|
||||
}
|
||||
@@ -152,6 +167,11 @@ function installStubs() {
|
||||
|
||||
Object.assign(stepsDb, { materialisePhase: async () => [] })
|
||||
Object.assign(logDb, { write: async (line) => { store.log.push(line); return 1 } })
|
||||
|
||||
// Phase 6. No stored switch anywhere in this file: an empty switchboard is a
|
||||
// fresh deployment, and expansion is not what this file is measuring.
|
||||
Object.assign(settingsDb, { byIds: async () => new Map(), get: async () => null })
|
||||
Object.assign(budgetDb, { seed: async () => 0, forRun: async () => [] })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user