// ── The live cap meter (EVENTS_PLAN.md Phase 13) ─────────────────────────── // // `events/price.js` answers what a plan would spend without dispatching a thing. // Three of the tests below protect a decision rather than a mechanism, and they // are the reason this file exists apart from `eventVerify.test.js`: // // • **A step core cannot price is reported, never counted as free.** All three // ways that happens — a dormant action, a `cost()` that broke its own // contract, and a dimension nobody declared — make the totals an UNDER-count, // and a meter an author trusts that reads lower than what will happen is // worse than no meter at all. // • **An undeclared dimension is still counted.** It is unenforceable, not // unknown: the action really will try to spend it, and the step is refused at // dispatch for that reason. Reporting it as costing nothing would hide both // facts at once. // • **The route dispatches nothing.** An action whose `perform()` would throw // prices perfectly well here, which is what makes the meter safe on a // debounce — `verify` puts a module and a sidecar behind every call and this // deliberately does not. // // The registry is the real one, staged and applied the way a module does it, for // `eventAuthorize.test.js`'s reason: an action that would not register is not one // this file has to survive. 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 price = require('../src/events/price') const settingsDb = require('../src/model/events/eventActionSettings.db') const db = require('../src/utils/db') after(() => db.close()) const originalSettings = { ...settingsDb } let settings beforeEach(() => { registries._reset() settings = new Map() settingsDb.byIds = async (ids) => new Map([...new Set(ids || [])].filter((i) => settings.has(i)).map((i) => [i, settings.get(i)])) }) afterEach(() => { Object.assign(settingsDb, originalSettings) registries._reset() }) const action = (id, over = {}) => ({ id, label: over.label || id, risk: over.risk || 'notify', reversible: over.reversible || 'none', params: over.params || [], ...(over.cost ? { cost: over.cost } : {}), async perform() { return over.perform ? over.perform() : { ok: true } }, }) const register = (entries, { owner = 'test', budgets = [] } = {}) => { const api = registries.stage(owner) api.registerEventActions(entries) if (budgets.length) api.registerEventBudgets(budgets.map((id) => ({ id, label: id, unit: 'count' }))) registries.apply(api.staged) } const setCaps = (id, caps) => settings.set(id, { action_id: id, enabled: 1, caps }) /** `{ phases: [...] }` out of a compact `[[step, step], [step]]`. */ const spec = (phases) => ({ phases: phases.map((steps, i) => ({ key: `phase${i + 1}`, steps: steps.map(([actionId, params = {}]) => ({ actionId, params })), })), }) const dimension = (report, id) => report.cost.find((c) => c.dimension === id) // ── The whole-plan total, which is the number the meter exists to show ────── test('adds a dimension up across every phase and compares it to the tightest cap', async () => { register( [ action('test.spawn', { cost: (p) => ({ 'test.creatures': Number(p.count) || 0 }) }), action('test.boss', { cost: () => ({ 'test.creatures': 1 }) }), ], { budgets: ['test.creatures'] }, ) setCaps('test.spawn', { 'test.creatures': 30 }) // The tightest cap wins: two actions spending one dimension have to agree on // one number, and a safety limit settles on the smaller. setCaps('test.boss', { 'test.creatures': 24 }) const report = await price.priceSpec( spec([ [['test.spawn', { count: 15 }]], [['test.spawn', { count: 15 }], ['test.boss', {}]], ]), ) assert.equal(report.ok, true) assert.equal(report.steps, 3) assert.equal(report.priced, 3) assert.deepEqual(dimension(report, 'test.creatures'), { dimension: 'test.creatures', total: 31, cap: 24, from: 'test.boss', over: true, }) }) test('a plan inside its cap is not over', async () => { register([action('test.spawn', { cost: (p) => ({ 'test.creatures': Number(p.count) || 0 }) })], { budgets: ['test.creatures'], }) setCaps('test.spawn', { 'test.creatures': 30 }) const report = await price.priceSpec(spec([[['test.spawn', { count: 12 }]]])) assert.equal(dimension(report, 'test.creatures').over, false) }) test('a dimension nobody caps comes back uncapped rather than missing', async () => { register([action('test.say', { cost: () => ({ 'test.broadcasts': 1 }) })], { budgets: ['test.broadcasts'] }) const report = await price.priceSpec(spec([[['test.say', {}]]])) assert.deepEqual(dimension(report, 'test.broadcasts'), { dimension: 'test.broadcasts', total: 1, cap: null, from: null, over: false, }) }) // ── The per-phase draw the timeline renders ──────────────────────────────── test('reports the draw per phase, in timeline order, including a phase that spends nothing', async () => { register( [ action('test.spawn', { cost: (p) => ({ 'test.creatures': Number(p.count) || 0 }) }), action('test.wait'), ], { budgets: ['test.creatures'] }, ) const report = await price.priceSpec( spec([ [['test.spawn', { count: 8 }]], [['test.wait', {}]], [['test.spawn', { count: 4 }], ['test.spawn', { count: 2 }]], ]), ) assert.deepEqual( report.phases.map((p) => ({ phase: p.phase, key: p.key, steps: p.steps, draw: p.draw })), [ { phase: 0, key: 'phase1', steps: 1, draw: [{ dimension: 'test.creatures', total: 8 }] }, // A phase whose steps cost nothing still appears, so the rollup and the // timeline have the same number of rows. { phase: 1, key: 'phase2', steps: 1, draw: [] }, { phase: 2, key: 'phase3', steps: 2, draw: [{ dimension: 'test.creatures', total: 6 }] }, ], ) }) test('a phase is addressed by its ordinal, so an unnamed one still meters', async () => { register([action('test.spawn', { cost: () => ({ 'test.creatures': 3 }) })], { budgets: ['test.creatures'] }) // The key a half-typed phase carries may not validate yet. A meter that could // only address a phase whose key is already legal would go blank exactly while // somebody is naming it. const report = await price.priceSpec({ phases: [{ steps: [{ actionId: 'test.spawn', params: {} }] }] }) assert.equal(report.phases[0].phase, 0) assert.equal(report.phases[0].key, null) assert.equal(dimension(report, 'test.creatures').total, 3) }) // ── The three ways a step cannot be priced ───────────────────────────────── test('a step naming an action nothing registers is reported, not silently free', async () => { register([action('test.spawn', { cost: () => ({ 'test.creatures': 5 }) })], { budgets: ['test.creatures'] }) const report = await price.priceSpec(spec([[['test.spawn', {}], ['uo.creature.spawn', {}]]])) assert.equal(report.steps, 2) assert.equal(report.priced, 1) assert.deepEqual(report.unpriced, [ { phase: 0, seq: 1, actionId: 'uo.creature.spawn', code: 'dormant', message: 'no module registers "uo.creature.spawn"', }, ]) }) test('a step with no action chosen yet is not a problem', async () => { register([action('test.spawn', { cost: () => ({ 'test.creatures': 5 }) })], { budgets: ['test.creatures'] }) // A form being filled in, not a plan with a hole in it. Reporting it would put // a red line on the screen for every step the moment it is added. const report = await price.priceSpec(spec([[['test.spawn', {}], ['', {}]]])) assert.deepEqual(report.unpriced, []) assert.equal(report.steps, 2) assert.equal(report.priced, 1) }) test('an action whose cost() breaks its own contract is unpriceable, not free', async () => { register( [ action('test.broken', { label: 'Broken', cost: () => { throw new Error('nope') }, }), ], { budgets: [] }, ) const report = await price.priceSpec(spec([[['test.broken', {}]]])) assert.equal(report.priced, 0) assert.equal(report.unpriced.length, 1) assert.equal(report.unpriced[0].code, 'unpriceable') assert.match(report.unpriced[0].message, /could not report what it costs/) }) test('an undeclared dimension is COUNTED and reported as unenforceable', async () => { // The split `authorize.undeclaredDimensions` makes, for the same reason: the // action really will try to spend it — the step is refused at dispatch for // exactly this — so the amount is true and the enforcement is what is missing. register([action('test.spawn', { cost: () => ({ 'test.creatures': 9 }) })], { budgets: [] }) const report = await price.priceSpec(spec([[['test.spawn', {}]]])) assert.equal(dimension(report, 'test.creatures').total, 9) assert.equal(report.priced, 1) assert.equal(report.unpriced[0].code, 'undeclared') assert.match(report.unpriced[0].message, /refused at dispatch/) }) // ── What makes it safe to call while somebody types ──────────────────────── test('prices without dispatching: an action whose perform() throws still meters', async () => { register( [ action('test.spawn', { cost: () => ({ 'test.creatures': 7 }), perform: () => { throw new Error('the shard is down') }, }), ], { budgets: ['test.creatures'] }, ) const report = await price.priceSpec(spec([[['test.spawn', {}]]])) assert.equal(dimension(report, 'test.creatures').total, 7) assert.deepEqual(report.unpriced, []) }) test('an empty plan prices to nothing rather than failing', async () => { const report = await price.priceSpec({}) assert.deepEqual(report, { ok: true, steps: 0, priced: 0, cost: [], phases: [], unpriced: [] }) }) // ── The paste guard, which is the spec's own and not a number invented here ── test('refuses a body over the spec size limits', async () => { const spawn = { actionId: 'test.spawn', params: {} } const tooManyPhases = { phases: Array.from({ length: 41 }, (_, i) => ({ key: `p${i}`, steps: [] })) } assert.deepEqual(await price.priceSpec(tooManyPhases), { ok: false, error: 'at most 40 phases' }) const tooManySteps = { phases: [{ key: 'p', steps: Array.from({ length: 101 }, () => spawn) }] } assert.deepEqual(await price.priceSpec(tooManySteps), { ok: false, error: 'at most 100 steps in one phase', }) // 40 x 100 is over MAX_STEPS while breaking neither of the two bounds above. const tooManyOverall = { phases: Array.from({ length: 40 }, (_, i) => ({ key: `p${i}`, steps: Array.from({ length: 100 }, () => spawn), })), } assert.deepEqual(await price.priceSpec(tooManyOverall), { ok: false, error: 'at most 500 steps in one definition', }) }) test('a step whose params are not an object is priced as no params rather than throwing', async () => { register([action('test.spawn', { cost: (p) => ({ 'test.creatures': Number(p.count) || 1 }) })], { budgets: ['test.creatures'], }) const report = await price.priceSpec({ phases: [{ key: 'p', steps: [{ actionId: 'test.spawn', params: ['not', 'an', 'object'] }] }], }) assert.equal(dimension(report, 'test.creatures').total, 1) })