// ── A live run's progress lines (MODULE_API 1.12.0) ──────────────────────── // // `events/progress.js`: core asks each step whose action declares `progress()` // how it stands, and publishes two shapes it never interprets. What is worth // testing is the contract's edges: // // • only the two shapes pass, a count and a percentage, each with a label; // anything else is dropped, never repaired // • a module that throws, answers late or is uninstalled costs a line, not the // page: `forRun` never rejects // • a step is asked with ITS ledger rows only, still held, and the reader's id // • one answer per run and reader for the cache window // // Stubbed at the `.db` layer and the registry, like `eventPublic.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 progress = require('../src/events/progress') const registries = require('../src/modules/registries') const stepsDb = require('../src/model/events/eventRunSteps.db') const resourcesDb = require('../src/model/events/eventRunResources.db') const db = require('../src/utils/db') after(() => db.close()) const originals = { steps: { ...stepsDb }, resources: { ...resourcesDb } } let steps let ledger beforeEach(() => { registries._reset() progress._reset() steps = [] ledger = [] stepsDb.listForRun = async () => steps resourcesDb.forRun = async () => ledger }) afterEach(() => { Object.assign(stepsDb, originals.steps) Object.assign(resourcesDb, originals.resources) registries._reset() progress._reset() }) const register = (entries) => { const api = registries.stage('demo') api.registerEventActions(entries) registries.apply(api.staged) } const action = (over = {}) => ({ id: 'demo.npc.place', label: 'Place', risk: 'change', reversible: 'ledger', perform: async () => ({ ok: true }), revert: async () => ({ ok: true }), ...over, }) const step = (id, actionId = 'demo.npc.place', params = {}) => ({ id, action_id: actionId, params, idempotency_key: `key-${id}`, }) const RUN = { id: 7, scope: '' } test('only a count or a percent, each with a label, is a line', () => { assert.deepEqual(progress.toLine({ label: 'Bandits', left: 3, of: 8 }, 'a'), { label: 'Bandits', left: 3, of: 8 }) assert.deepEqual(progress.toLine({ label: 'The Juggernaut', percent: 61.6 }, 'a'), { label: 'The Juggernaut', percent: 62 }) assert.equal(progress.toLine(null, 'a'), null) assert.equal(progress.toLine('3 of 8', 'a'), null) assert.equal(progress.toLine({ left: 3, of: 8 }, 'a'), null, 'no label') assert.equal(progress.toLine({ label: 'x'.repeat(progress.MAX_LABEL + 1), left: 1, of: 1 }, 'a'), null) assert.equal(progress.toLine({ label: 'Both', left: 1, of: 2, percent: 50 }, 'a'), null) assert.equal(progress.toLine({ label: 'More left than placed', left: 9, of: 8 }, 'a'), null) assert.equal(progress.toLine({ label: 'Nothing placed', left: 0, of: 0 }, 'a'), null) assert.equal(progress.toLine({ label: 'Half', left: 1.5, of: 3 }, 'a'), null) assert.equal(progress.toLine({ label: 'Over', percent: 101 }, 'a'), null) assert.equal(progress.toLine({ label: 'Text', percent: '50' }, 'a'), null) // Extra keys do not ride along: core publishes what it validated. assert.deepEqual(progress.toLine({ label: 'B', left: 0, of: 1, secret: 'x' }, 'a'), { label: 'B', left: 0, of: 1 }) }) test('each step is asked with its own held rows and the reader, in step order', async () => { const seen = [] register([ action({ progress: async (env) => { seen.push(env) return { label: `Step ${env.stepId}`, left: env.resources.length, of: 2 } }, }), ]) steps = [step(1), step(2)] ledger = [ { step_id: 1, kind: 'npc', ref: 'a', payload: null, status: 'confirmed' }, { step_id: 1, kind: 'npc', ref: 'b', payload: null, status: 'reverted' }, { step_id: 2, kind: 'npc', ref: 'c', payload: null, status: 'confirmed' }, { step_id: 2, kind: 'npc', ref: 'd', payload: null, status: 'pending' }, { step_id: 2, kind: resourcesDb.STEP_KIND, ref: '2', payload: null, status: 'confirmed' }, ] const lines = await progress.forRun(RUN, { viewer: { userId: 42 } }) assert.deepEqual(lines, [ { label: 'Step 1', left: 1, of: 2 }, { label: 'Step 2', left: 2, of: 2 }, ]) assert.deepEqual(seen[0].resources, [{ kind: 'npc', ref: 'a', payload: null }]) assert.deepEqual(seen[0].viewer, { userId: 42 }) assert.equal(seen[0].idempotencyKey, 'key-1') }) test('a step whose action has no progress() is not asked, and says nothing', async () => { register([action(), action({ id: 'demo.zone.open', reversible: 'none', revert: undefined })]) steps = [step(1), step(2, 'demo.zone.open'), step(3, 'demo.uninstalled.thing')] assert.deepEqual(await progress.forRun(RUN), []) }) test('a module that throws, answers late or answers nonsense costs a line, never the page', async () => { register([ action({ progress: async (env) => { if (env.stepId === 1) throw new Error('sidecar down') if (env.stepId === 2) return new Promise(() => {}) if (env.stepId === 3) return { label: 'Bad', left: -1, of: 2 } return { label: 'Good', percent: 40 } }, }), ]) steps = [step(1), step(2), step(3), step(4)] const started = Date.now() const lines = await progress.forRun(RUN) assert.deepEqual(lines, [{ label: 'Good', percent: 40 }]) assert.ok(Date.now() - started < progress.PROGRESS_BUDGET_MS + 1500, 'the deadline held') }) test('a database that fails is an empty list, not a rejection', async () => { register([action({ progress: async () => ({ label: 'x', left: 1, of: 1 }) })]) stepsDb.listForRun = async () => { throw new Error('pool closed') } assert.deepEqual(await progress.forRun(RUN), []) }) test('one answer per run and reader within the window', async () => { let asked = 0 register([ action({ progress: async () => { asked++ return { label: 'Bandits', left: 3, of: 8 } }, }), ]) steps = [step(1)] let t = 1_000_000 const now = () => t await progress.forRun(RUN, { now }) await progress.forRun(RUN, { now }) assert.equal(asked, 1, 'cached for the same reader') await progress.forRun(RUN, { viewer: { userId: 5 }, now }) assert.equal(asked, 2, 'a different reader is asked for') t += progress.CACHE_MS await progress.forRun(RUN, { now }) assert.equal(asked, 3, 'asked again once the window passed') })