import { describe, it, after } from 'node:test'; import assert from 'node:assert/strict'; import { loadFleet, listServices } from '../src/load.js'; import { Supervisor } from '../src/supervisor.js'; import { Monitor } from '../src/monitor.js'; function treeOnly(healthBase) { const loaded = loadFleet(); for (const s of Object.values(loaded.services)) { s.enabled = s.id === 'tree-node'; if (s.id === 'tree-node') { s.min = 3; s.max = 6; s.keepFloor = true; s.ports = { healthBase }; } } return loaded; } describe('verae-fleet', () => { /** @type {Supervisor[]} */ const supervisors = []; after(async () => { for (const s of supervisors) await s.stopAll(); }); it('lists every service with a config file and central replica spec', () => { const loaded = loadFleet(); const rows = listServices(loaded); const ids = rows.map((r) => r.id); for (const need of [ 'nats', 'zappier-edge', 'middleware-http', 'job-poller', 'webhook-deliver', 'archive-aggregator', 'archive-worm', 'tree-node', 'zapier-simulator', 'zapier-platform-app', ]) { assert.ok(ids.includes(need), `missing ${need}`); } const tree = loaded.services['tree-node']; assert.equal(tree.min, 3); assert.equal(tree.keepFloor, true); assert.equal(tree.configPath, 'services/tree-node.json'); assert.equal(loaded.fleetPath, 'fleet.json'); }); it('starts the tree-node floor (3 available copies)', async () => { const sup = new Supervisor({ loaded: treeOnly(14600) }); supervisors.push(sup); await sup.startService('tree-node'); const st = sup.status().services['tree-node']; assert.equal(st.available, 3); assert.equal(st.belowFloor, false); assert.equal(st.instances.length, 3); }); it('pauses one tree-node and respawns so min available stays 3', async () => { const sup = new Supervisor({ loaded: treeOnly(14700) }); supervisors.push(sup); await sup.startService('tree-node'); await sup.pauseInstance('tree-node-0'); const st = sup.status().services['tree-node']; assert.ok(st.available >= 3, `available ${st.available}`); assert.ok(st.paused >= 1); assert.ok(st.running >= 4); assert.equal(st.belowFloor, false); }); it('restarts an unhealthy replica and keeps the floor', async () => { const sup = new Supervisor({ loaded: treeOnly(14800) }); supervisors.push(sup); await sup.startService('tree-node'); const before = sup.instances.get('tree-node-1').pid; await sup.markUnhealthy('tree-node-1'); const mon = new Monitor(sup, { intervalMs: 50 }); await mon.tick(); const rec = sup.instances.get('tree-node-1'); assert.ok(rec.pid); assert.notEqual(rec.pid, before); assert.equal(rec.healthy, true); assert.equal(sup.status().services['tree-node'].available, 3); }); it('stop+replace of a replica keeps min tree-nodes available', async () => { const sup = new Supervisor({ loaded: treeOnly(14900) }); supervisors.push(sup); await sup.startService('tree-node'); await sup.stopInstance('tree-node-2', { replace: true }); const st = sup.status().services['tree-node']; assert.equal(st.available, 3); assert.ok(st.instances.some((i) => i.id === 'tree-node-2' || i.id === 'tree-node-0')); }); });