import { test } from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); function listen(handler) { return new Promise((resolve) => { const s = http.createServer(handler); s.listen(0, '127.0.0.1', () => resolve(s)); }); } test('keep restarts a crashed unit and leaves a paused unit down', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'keep-lab-')); const dummy = path.join(tmp, 'dummy.js'); fs.writeFileSync( dummy, `import http from 'node:http'; const port = Number(process.env.PORT); let paused = false; http.createServer((req, res) => { const u = new URL(req.url, 'http://127.0.0.1'); const body = JSON.stringify({ ok: !paused, paused, pid: process.pid }); if (u.pathname === '/pause' && req.method === 'POST') paused = true; if (u.pathname === '/resume' && req.method === 'POST') paused = false; res.writeHead(paused ? 503 : 200, { 'content-type': 'application/json' }); res.end(body); }).listen(port, '127.0.0.1'); `, ); const port = await new Promise((resolve) => { const s = http.createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); }); const keepPort = port + 1; const units = { bind: '127.0.0.1', port: keepPort, intervalMs: 400, runtimeDir: tmp, units: [ { id: 'dummy-0', health: `http://127.0.0.1:${port}/health`, cwd: tmp, command: [process.execPath, dummy], env: { PORT: String(port) }, }, ], }; const unitsFile = path.join(tmp, 'units.json'); fs.writeFileSync(unitsFile, JSON.stringify(units)); const child = spawn(process.execPath, ['src/keep.js'], { cwd: root, env: { ...process.env, KEEP_UNITS: unitsFile, KEEP_STATE: path.join(tmp, 'state'), KEEP_PORT: String(keepPort) }, stdio: ['ignore', 'pipe', 'pipe'], }); const waitHealth = async (url, wantOk) => { for (let i = 0; i < 25; i += 1) { try { const r = await fetch(url); const b = await r.json(); if (Boolean(b.ok) === wantOk) return b; } catch { if (!wantOk) return { ok: false }; } await new Promise((r) => setTimeout(r, 200)); } throw new Error(`timeout ${url} wantOk=${wantOk}`); }; try { await waitHealth(`http://127.0.0.1:${keepPort}/health`, true); const up = await waitHealth(`http://127.0.0.1:${port}/health`, true); assert.equal(up.ok, true); const pid1 = up.pid; process.kill(pid1, 'SIGKILL'); const up2 = await waitHealth(`http://127.0.0.1:${port}/health`, true); assert.notEqual(up2.pid, pid1); await fetch(`http://127.0.0.1:${keepPort}/intent/dummy-0`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ state: 'stop' }), }); await new Promise((r) => setTimeout(r, 800)); try { process.kill(up2.pid, 'SIGKILL'); } catch { /* already stopped */ } await new Promise((r) => setTimeout(r, 900)); let down = false; try { await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(400) }); } catch { down = true; } assert.equal(down, true); } finally { child.kill('SIGTERM'); } });