111 lines
3.5 KiB
JavaScript
111 lines
3.5 KiB
JavaScript
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');
|
|
}
|
|
});
|