// ── Starting a run from a trigger (Rust PLAN_REDESIGNS §14.3.12, D370) ───── // // Two halves. The SPEC half: a definition's `starts` is validated the way a // phase gate is (one grammar, one dormant rule), with a cooldown, and core's own // `event.*` triggers refused. The EMIT half: `events/starts.observe()` decides // whether a firing starts a run, and this file proves what it decides against an // in-memory stand-in whose claim keeps the SQL's rules. The SQL itself — one // winner, the cooldown and the one-at-a-time check in one WHERE — is proved // against a real server in `eventRunnerSql.test.js`. 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 spec = require('../src/events/spec') const starts = require('../src/events/starts') const definitionsDb = require('../src/model/events/eventDefinitions.db') const startsDb = require('../src/model/events/eventTriggerStarts.db') const runsModel = require('../src/model/events/eventRuns.model') const db = require('../src/utils/db') after(() => db.close()) const TRIGGER = 'demo.bradley.destroyed' beforeEach(() => { registries._reset() registries.registerCore() const api = registries.stage('demo') api.registerEventTriggers([ { id: TRIGGER, label: 'Bradley destroyed', ceiling: 'everyone', variables: [ { name: 'server', type: 'string', required: true, example: 'main', description: 'The server.' }, { name: 'grid', type: 'string', required: false, example: 'G8', description: 'Where.' }, { name: 'kills', type: 'int', required: false, example: 1, description: 'How many.' }, ], }, ]) registries.apply(api.staged) }) afterEach(() => registries._reset()) const withStarts = (startsValue) => ({ schedule: { kind: 'manual' }, starts: startsValue, phases: [{ key: 'main', label: 'Main', steps: [] }], }) // ── The spec half ────────────────────────────────────────────────────────── test('a start is a gate\'s trigger and condition plus a cooldown, normalised', () => { const result = spec.validate( withStarts([{ on: TRIGGER, where: { variable: 'server', cmp: 'eq', value: 'main' }, cooldown: '120m' }]), ) assert.equal(result.ok, true, JSON.stringify(result.errors)) assert.deepEqual(result.spec.starts, [ { on: TRIGGER, where: { variable: 'server', cmp: 'eq', value: 'main' }, cooldown: '2h', dormant: false }, ]) }) test('the cooldown defaults to thirty minutes, and zero is a real choice', () => { const a = spec.validate(withStarts([{ on: TRIGGER }])) assert.equal(a.spec.starts[0].cooldown, '30m') assert.equal(a.spec.starts[0].where, null) const b = spec.validate(withStarts([{ on: TRIGGER, cooldown: 0 }])) assert.equal(b.spec.starts[0].cooldown, '0m') assert.equal(spec.parseCooldown('0m'), 0) }) test('a cooldown past seven days, or not a duration, is refused by name', () => { for (const cooldown of ['8d', '1h30m', 'soon', -5]) { const result = spec.validate(withStarts([{ on: TRIGGER, cooldown }])) assert.equal(result.ok, false, String(cooldown)) assert.match(result.errors[0], /^spec\.starts\[0\]\.cooldown:/) } assert.equal(spec.validate(withStarts([{ on: TRIGGER, cooldown: '7d' }])).ok, true) }) test('core\'s own event.* triggers cannot start a run', () => { const result = spec.validate(withStarts([{ on: 'event.run.started' }])) assert.equal(result.ok, false) assert.match(result.errors[0], /event system's own triggers/) }) test('the condition is checked against the trigger\'s declaration, path and all', () => { const result = spec.validate(withStarts([{ on: TRIGGER, where: { variable: 'sever', cmp: 'eq', value: 'main' } }])) assert.equal(result.ok, false) assert.ok(result.errors.some((e) => e.startsWith('spec.starts[0].where') && e.includes('sever')), result.errors.join('\n')) }) test('a start on a trigger nobody registers saves dormant and refuses to publish', () => { const result = spec.validate(withStarts([{ on: 'gone.trigger', where: { variable: 'x', cmp: 'eq', value: 1 } }])) assert.equal(result.ok, true) assert.equal(result.spec.starts[0].dormant, true) assert.deepEqual(result.spec.starts[0].where, { variable: 'x', cmp: 'eq', value: 1 }, 'kept, not dropped') assert.deepEqual(spec.publishable(result.spec), { ok: false, dormant: ['gone.trigger'] }) }) test('validate accepts its own output, and an empty list is not written', () => { const first = spec.validate(withStarts([{ on: TRIGGER, cooldown: '45m' }])) const second = spec.validate(first.spec) assert.deepEqual(second.spec, first.spec) assert.equal('starts' in spec.validate(withStarts([])).spec, false) }) test('bad shapes: not a list, too many, unknown keys', () => { assert.match(spec.validate(withStarts({ on: TRIGGER })).errors[0], /spec\.starts: expected an array/) const many = Array.from({ length: spec.MAX_STARTS + 1 }, () => ({ on: TRIGGER })) assert.match(spec.validate(withStarts(many)).errors[0], /at most/) assert.match(spec.validate(withStarts([{ on: TRIGGER, count: 2 }])).errors[0], /unknown key\(s\) count/) }) // ── The emit half ────────────────────────────────────────────────────────── const T0 = new Date('2026-10-16T20:00:00Z') let store const originals = { definitionsDb: { ...definitionsDb }, startsDb: { ...startsDb }, runsModel: { ...runsModel }, } // A run counts as going while it is not terminal — the SQL's own reading, with // "scheduled and due" folded in because a trigger-started run is due at once. const GOING = ['scheduled', 'starting', 'running', 'paused', 'ending'] function installStubs({ definitions }) { store = { definitions, runs: [], state: new Map(), creates: [], nextRun: 1 } definitionsDb.findStartingOn = async (triggerId) => store.definitions.filter((d) => (d.version_spec.starts || []).some((s) => s.on === triggerId)) const stateOf = (id) => { if (!store.state.has(id)) { store.state.set(id, { claimed_at: null, last_started_at: null, last_run_id: null, ignored_running: 0, ignored_cooldown: 0, ignored_unverified: 0, last_ignored_reason: null, }) } return store.state.get(id) } const going = (id) => store.runs.some((r) => r.definition_id === id && GOING.includes(r.status)) startsDb.forDefinition = async (id) => store.state.get(id) || null startsDb.isGoing = async (id) => going(id) startsDb.claim = async (id, { at, cooldownSeconds, now }) => { const s = stateOf(id) if (s.claimed_at && now - s.claimed_at < startsDb.CLAIM_STALE_MS) return false if (s.last_started_at && s.last_started_at > new Date(at.getTime() - cooldownSeconds * 1000)) return false if (going(id)) return false s.claimed_at = now return true } startsDb.started = async (id, { at, runId }) => { Object.assign(stateOf(id), { claimed_at: null, last_started_at: at, last_run_id: runId, ignored_running: 0, ignored_cooldown: 0, ignored_unverified: 0, }) } startsDb.release = async (id) => { stateOf(id).claimed_at = null } startsDb.ignore = async (id, reason) => { const s = stateOf(id) s[`ignored_${reason}`] += 1 s.last_ignored_reason = reason } runsModel.create = async (definitionId, opts, userId) => { store.creates.push({ definitionId, opts, userId }) const def = store.definitions.find((d) => d.id === definitionId) if (!def.version_verified_at) return { ok: false, code: 'unverified', errors: ['unverified'] } const existing = store.runs.find( (r) => r.definition_id === definitionId && r.scheduled_for.getTime() === opts.scheduledFor.getTime(), ) if (existing) return { ok: true, created: false, run: existing } const run = { id: store.nextRun++, definition_id: definitionId, status: 'scheduled', scheduled_for: opts.scheduledFor, origin: opts.origin } store.runs.push(run) return { ok: true, created: true, run } } } afterEach(() => { Object.assign(definitionsDb, originals.definitionsDb) Object.assign(startsDb, originals.startsDb) Object.assign(runsModel, originals.runsModel) }) const definition = (over = {}) => ({ id: 7, title: 'Launch Site Lockdown', grace_seconds: 900, version_verified_at: new Date('2026-10-01T00:00:00Z'), version_spec: { schedule: { kind: 'manual' }, starts: [{ on: TRIGGER, where: { variable: 'server', cmp: 'eq', value: 'main' }, cooldown: '30m', dormant: false }], phases: [{ key: 'main', label: 'Main', steps: [] }], }, ...over, }) const firing = (at, data = { server: 'main', grid: 'G8' }, extra = {}) => ({ triggerId: TRIGGER, occurredAt: at.toISOString(), subject: 'bradley-1', data, ...extra, }) const plus = (ms) => new Date(T0.getTime() + ms) test('a matching firing starts one run, now, with its origin', async () => { installStubs({ definitions: [definition()] }) const summary = await starts.observe(firing(T0, { server: 'main', grid: 'G8', stray: 'x' }), { now: plus(2_000) }) assert.deepEqual(summary, { definitions: 1, started: 1, ignored: 0 }) assert.equal(store.runs.length, 1) const [{ opts, userId }] = store.creates assert.equal(userId, null, 'no user started it') assert.equal(opts.source, 'trigger') assert.equal(opts.scheduledFor.toISOString(), T0.toISOString(), 'the firing\'s time, not the observer\'s') assert.deepEqual(opts.origin, { kind: 'trigger', triggerId: TRIGGER, label: 'Bradley destroyed', occurredAt: T0.toISOString(), subject: 'bradley-1', matched: 'server is "main"', // Declared variables only: `stray` is not something the trigger declares. variables: { server: 'main', grid: 'G8' }, }) assert.equal(store.state.get(7).last_run_id, 1) }) test('a firing that does not match the condition starts nothing and counts nothing', async () => { installStubs({ definitions: [definition()] }) const summary = await starts.observe(firing(T0, { server: 'staging' }), { now: plus(1_000) }) assert.deepEqual(summary, { definitions: 1, started: 0, ignored: 0 }) assert.equal(store.creates.length, 0) assert.equal(store.state.has(7), false) }) test('a second firing while the run is going starts nothing, and is counted as running', async () => { installStubs({ definitions: [definition({ version_spec: { ...definition().version_spec, starts: [{ ...definition().version_spec.starts[0], cooldown: '0m' }] } })] }) await starts.observe(firing(T0), { now: plus(1_000) }) const summary = await starts.observe(firing(plus(30_000)), { now: plus(31_000) }) assert.deepEqual(summary, { definitions: 1, started: 0, ignored: 1 }) assert.equal(store.runs.length, 1) assert.equal(store.state.get(7).ignored_running, 1) }) test('inside the cooldown, a firing starts nothing and is counted; after it, a second run', async () => { installStubs({ definitions: [definition()] }) await starts.observe(firing(T0), { now: plus(1_000) }) store.runs[0].status = 'completed' await starts.observe(firing(plus(10 * 60_000)), { now: plus(10 * 60_000) }) assert.equal(store.runs.length, 1) assert.equal(store.state.get(7).ignored_cooldown, 1) const summary = await starts.observe(firing(plus(31 * 60_000)), { now: plus(31 * 60_000) }) assert.equal(summary.started, 1) assert.equal(store.runs.length, 2) assert.equal(store.state.get(7).ignored_cooldown, 0, 'a new start zeroes the counts') }) test('one firing reported twice is one run, and the repeat is not counted', async () => { installStubs({ definitions: [definition()] }) await starts.observe(firing(T0), { now: plus(1_000) }) const summary = await starts.observe(firing(T0), { now: plus(1_500) }) assert.equal(summary.started, 0) assert.equal(summary.ignored, 0) assert.equal(store.runs.length, 1) assert.equal(store.state.get(7).ignored_running, 0) }) test('an unverified version is not started by a trigger, and is counted', async () => { installStubs({ definitions: [definition({ version_verified_at: null })] }) const summary = await starts.observe(firing(T0), { now: plus(1_000) }) assert.deepEqual(summary, { definitions: 1, started: 0, ignored: 1 }) assert.equal(store.creates.length, 0, 'not even tried') assert.equal(store.state.get(7).ignored_unverified, 1) }) test('a firing older than the grace window starts nothing', async () => { installStubs({ definitions: [definition({ grace_seconds: 60 })] }) const summary = await starts.observe(firing(T0), { now: plus(5 * 60_000) }) assert.equal(summary.started, 0) assert.equal(store.creates.length, 0) }) test('a firing stamped in the future starts the run now, not later', async () => { installStubs({ definitions: [definition()] }) await starts.observe(firing(plus(60 * 60_000)), { now: plus(1_000) }) assert.equal(store.creates[0].opts.scheduledFor.toISOString(), plus(1_000).toISOString()) }) test('core\'s event.* triggers are never looked up, and a dormant start never fires', async () => { installStubs({ definitions: [] }) let asked = 0 definitionsDb.findStartingOn = async () => { asked += 1 return [] } await starts.observe({ triggerId: 'event.run.started', data: {} }) assert.equal(asked, 0) installStubs({ definitions: [definition({ version_spec: { ...definition().version_spec, starts: [{ on: TRIGGER, where: null, cooldown: '30m', dormant: true }] } })], }) const summary = await starts.observe(firing(T0), { now: plus(1_000) }) assert.equal(summary.started, 0) }) test('two definitions on one trigger each decide for themselves', async () => { const other = definition({ id: 8, title: 'Other', version_spec: { ...definition().version_spec, starts: [{ on: TRIGGER, where: null, cooldown: '30m', dormant: false }] } }) installStubs({ definitions: [definition(), other] }) const summary = await starts.observe(firing(T0, { server: 'staging' }), { now: plus(1_000) }) assert.deepEqual(summary, { definitions: 2, started: 1, ignored: 0 }) assert.equal(store.runs[0].definition_id, 8) }) test('a create that throws gives the claim back and is not fatal to the others', async () => { const other = definition({ id: 8 }) installStubs({ definitions: [definition(), other] }) const create = runsModel.create runsModel.create = async (id, ...rest) => { if (id === 7) throw new Error('boom') return create(id, ...rest) } const summary = await starts.observe(firing(T0), { now: plus(1_000) }) assert.equal(summary.started, 1) assert.equal(store.state.get(7).claimed_at, null) }) test('an oversized payload keeps only the variables the condition named', () => { const big = 'x'.repeat(starts.MAX_ORIGIN_BYTES) const origin = starts.originOf( { triggerId: TRIGGER, data: { server: 'main', grid: big } }, registries.eventTrigger(TRIGGER), { where: { variable: 'server', cmp: 'eq', value: 'main' } }, T0, ) assert.deepEqual(origin.variables, { server: 'main', _truncated: true }) }) test('the editor\'s state line, and its absence', () => { assert.equal(starts.describeState(null), null) assert.deepEqual( starts.describeState({ last_started_at: T0, last_run_id: 4, ignored_running: 2, ignored_cooldown: 1, ignored_unverified: 0 }), { lastStartedAt: T0, lastRunId: 4, ignoredRunning: 2, ignoredCooldown: 1, ignoredUnverified: 0, lastIgnoredAt: null, lastIgnoredReason: null, }, ) })