const test = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); // A stand-in for the MQTT client, so no network is involved. const clients = []; const live = require('../lib/printerLive'); live.setConnector((url, options) => { const c = new EventEmitter(); Object.assign(c, { url, options, connected: false, reconnecting: false, published: [], ended: false, subscribe: (_topic, cb) => cb(null), publish: (topic, body) => c.published.push({ topic, body: JSON.parse(body) }), end: () => { c.ended = true; }, reconnect: () => {}, }); clients.push(c); return c; }); const report = (print) => Buffer.from(JSON.stringify({ print })); const printerRow = { id: 1, host: '10.0.0.5', serial: 'SER1', access_code: 'code1' }; const tray = (id, type, colour) => ({ id: String(id), tray_type: type, tray_color: colour, tray_uuid: type ? `U${id}` : '0' }); test('live status merges partial reports, and a tray change syncs spools once', async (t) => { t.after(() => live.stopAll()); const synced = []; live.setTraysHandler(async (id, trays) => { synced.push({ id, trays }); return { updated: trays.length }; }); live.reconcile([printerRow, { id: 2, host: '', serial: 'X', access_code: 'y' }]); assert.equal(clients.length, 1, 'a printer missing its address isn’t connected'); const c = clients[0]; assert.equal(c.options.password, 'code1'); c.connected = true; c.emit('connect'); assert.equal(c.published[0].topic, 'device/SER1/request'); assert.equal(c.published[0].body.pushing.command, 'pushall'); c.emit('message', 'device/SER1/report', report({ gcode_state: 'RUNNING', mc_percent: 10, nozzle_temper: 220, ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF'), tray(1, '', '')] }] }, })); c.emit('message', 'device/SER1/report', report({ mc_percent: 11 })); const snap = live.snapshot(1); assert.equal(snap.connected, true); assert.equal(snap.status.state, 'RUNNING'); assert.equal(snap.status.progress, 11); assert.equal(snap.status.nozzle, 220, 'kept from the earlier report'); assert.deepEqual(snap.loaded.map((l) => [l.slot, l.empty, l.colour, l.tagged]), [[0, false, '#FF0000', true], [1, true, null, false]]); assert.equal(JSON.stringify(snap).includes('code1'), false, 'no access code in a snapshot'); // "Test & sync" asks again and syncs straight away. const pending = live.refresh(1, 3000); c.emit('message', 'device/SER1/report', report({ ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF'), tray(1, '', '')] }] } })); const result = await pending; assert.equal(result.connected, true); assert.equal(result.sync.updated, 2); assert.equal(synced.length, 1); assert.equal(c.published.at(-1).body.pushing.command, 'pushall'); }); test('a busy printer’s steady reports don’t hold back a spool sync', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); t.after(() => live.stopAll()); clients.length = 0; const synced = []; live.setTraysHandler(async (id) => { synced.push(id); return {}; }); live.reconcile([{ ...printerRow, id: 5, serial: 'SER5' }]); const c = clients[0]; c.connected = true; c.emit('connect'); c.emit('message', 't', report({ ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF')] }] } })); for (let i = 0; i < 10; i += 1) { t.mock.timers.tick(1000); c.emit('message', 't', report({ mc_percent: i })); } assert.equal(synced.length, 1, 'synced once, five seconds after the change'); // The same trays again change nothing. c.emit('message', 't', report({ ams: { ams: [{ id: '0', tray: [tray(0, 'PLA', 'FF0000FF')] }] } })); t.mock.timers.tick(6000); assert.equal(synced.length, 1); }); test('the tray feeding the toolhead is worked out from tray_now', (t) => { t.after(() => live.stopAll()); clients.length = 0; live.setTraysHandler(async () => ({})); live.reconcile([{ ...printerRow, id: 7, serial: 'SER7' }, { ...printerRow, id: 8, serial: 'SER8' }]); const [a, b] = clients; const units = (ids) => ({ ams: ids.map((id) => ({ id: String(id), tray: [tray(0, 'PLA', 'FF0000FF')] })) }); // Two AMS units numbered 0 and 1: tray 6 is the second unit's third slot. a.emit('message', 't', report({ ams: { ...units([0, 1]), tray_now: '6' } })); assert.deepEqual(live.snapshot(7).status.active, { unit: 1, slot: 2 }); // A later report changing only tray_now still counts. a.emit('message', 't', report({ ams: { tray_now: '254' } })); assert.deepEqual(live.snapshot(7).status.active, { external: true }); a.emit('message', 't', report({ ams: { tray_now: '255' } })); assert.equal(live.snapshot(7).status.active, null); // An AMS lite numbered 16 (as on the A2L): tray 1 is its second slot. b.emit('message', 't', report({ ams: { ...units([16]), tray_now: '1' } })); assert.deepEqual(live.snapshot(8).status.active, { unit: 16, slot: 1 }); }); test('changing a printer’s code reopens its connection; removing it closes it', () => { clients.length = 0; live.reconcile([printerRow]); live.reconcile([printerRow]); assert.equal(clients.length, 1, 'no change, no new connection'); live.reconcile([{ ...printerRow, access_code: 'code2' }]); assert.equal(clients.length, 2); assert.equal(clients[0].ended, true); live.reconcile([]); assert.equal(clients[1].ended, true); assert.deepEqual(live.snapshot(1), { live: false }); });