/** * App health endpoint test — locks in the [E00-S02-T03] `GET /health` * endpoint for the workspace server. * * Acceptance criteria covered (each test fails without the committed state): * - "app health endpoint succeeds" → the committed * `apps/server/src/index.ts` creates an HTTP server (`node:http` * `createServer`), listens on the application port (default 3000, * `PORT`-overridable) and answers `GET /health` with HTTP 200. The * "HTTP smoke test" boots the committed server source (Node type * stripping, no build step) on an ephemeral port and makes a real * `GET /health` request, asserting a 2xx response. * - "the endpoint reports a healthy application" → the `/health` response * body is JSON reporting a healthy application (`{"status":"ok"}`), * asserted statically against the committed source and by the smoke test. * * Run: `node --test tests/health-endpoint.test.mjs` * (node:test — built into Node >= 18; no dependencies, lockfile untouched.) */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { once } from 'node:events'; import { createServer as createNetServer } from 'node:net'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); /** The committed server entrypoint under test. */ const SERVER_SRC = 'apps/server/src/index.ts'; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // --------------------------------------------------------------------------- // Static assertions on the committed server entrypoint // --------------------------------------------------------------------------- /** * Asserts the committed server entrypoint answers `GET /health` with HTTP 200 * and reports a healthy application. Fails fast on a missing/placeholder * entrypoint; the mutation probes below prove the assertions are non-vacuous. */ function assertHealthEndpointSource(src) { assert.match( src, /createServer\(/, 'the server entrypoint must create an HTTP server (node:http createServer)', ); assert.match( src, /'\/health'/, "the server entrypoint must route the health endpoint (GET /health)", ); assert.match( src, /sendJson\(res, 200/, 'the health route must answer with HTTP 200 (app health endpoint succeeds)', ); assert.match( src, /status:\s*'ok'/, "the health payload must report a healthy application ({\"status\":\"ok\"})", ); assert.match( src, /server\.listen\(/, 'the server entrypoint must start listening (server.listen)', ); assert.match( src, /3000/, 'the server must default to the application port 3000 (Dockerfile EXPOSE / compose :3000)', ); } // --------------------------------------------------------------------------- // Boot helpers for the HTTP smoke test (Node type stripping, no build step) // --------------------------------------------------------------------------- /** * How the current Node executes TypeScript sources: `default` (>= 23.6, type * stripping on by default), `strip-types-flag` (>= 22.6 via * `--experimental-strip-types`) or `null` (cannot run .ts at all). The * workspace pins engines.node to 24.x, where type stripping is stable. */ function tsExecMode() { const [major, minor] = process.versions.node.split('.').map(Number); if (major > 23 || (major === 23 && minor >= 6)) return 'default'; if (major === 22 && minor >= 6) return 'strip-types-flag'; return null; } /** Reserves an ephemeral TCP port, then releases it for the child to bind. */ function reservePort() { return new Promise((resolve, reject) => { const probe = createNetServer(); probe.once('error', reject); probe.listen(0, '127.0.0.1', () => { const address = probe.address(); const port = typeof address === 'object' && address !== null ? address.port : 0; probe.close(() => resolve(port)); }); }); } /** * Boots the committed server source on `port`. Returns `{ child, stderr }`; * the child writes its stderr into the `stderr()` closure for diagnostics. */ function bootServer(port) { const args = tsExecMode() === 'strip-types-flag' ? ['--experimental-strip-types', SERVER_SRC] : [SERVER_SRC]; const child = spawn(process.execPath, args, { cwd: REPO_ROOT, env: { ...process.env, PORT: String(port) }, stdio: ['ignore', 'ignore', 'pipe'], }); let stderr = ''; child.stderr.on('data', (chunk) => { stderr += String(chunk); }); return { child, stderr: () => stderr }; } /** * Polls `GET /health` until it answers or the child exits / the deadline * passes (poll with timeout — no flaky sleeps). */ async function waitForHealth(port, child, stderr) { const deadline = Date.now() + 10_000; let lastError = ''; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error( `the server exited before answering GET /health (code ${child.exitCode}): ${stderr().trim()}`, ); } try { return await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1_000), }); } catch (err) { lastError = err instanceof Error ? err.message : String(err); await delay(100); } } throw new Error( `GET /health did not answer within 10s (last error: ${lastError}; server stderr: ${stderr().trim()})`, ); } // --------------------------------------------------------------------------- // Criterion tests // --------------------------------------------------------------------------- test('the app health endpoint succeeds (committed entrypoint answers GET /health with HTTP 200)', () => { assertHealthEndpointSource(read(SERVER_SRC)); }); test('the endpoint reports a healthy application (committed health payload is {"status":"ok"})', () => { const src = read(SERVER_SRC); assert.match( src, /status:\s*'ok'/, "the health payload must report a healthy application ({\"status\":\"ok\"})", ); }); test('an HTTP smoke test against the booted server succeeds for GET /health (200 + healthy body)', async (t) => { if (!tsExecMode()) { t.skip( `Node ${process.versions.node} cannot execute TypeScript sources; the workspace pins engines.node to 24.x (type stripping is stable there)`, ); return; } const port = await reservePort(); const { child, stderr } = bootServer(port); try { const response = await waitForHealth(port, child, stderr); assert.equal( response.status, 200, `GET /health must succeed with HTTP 200 (got ${response.status})`, ); const body = await response.json(); assert.equal( body.status, 'ok', 'the health endpoint must report a healthy application ({"status":"ok"})', ); } finally { child.kill('SIGTERM'); await Promise.race([once(child, 'exit'), delay(2_000)]); if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); } }); // --------------------------------------------------------------------------- // Non-vacuous probes — the assertions above really do fail on violations // --------------------------------------------------------------------------- test('removing the /health route makes the health-endpoint criterion fail (mutation probe)', () => { const src = read(SERVER_SRC); const withoutRoute = src.replace(/'\/health'/, "'/nope'"); assert.notEqual(withoutRoute, src, 'the mutation must actually replace the /health route'); assert.throws(() => assertHealthEndpointSource(withoutRoute), /\/health/); }); test('removing the HTTP 200 makes the health-endpoint criterion fail (mutation probe)', () => { const src = read(SERVER_SRC); const without200 = src.replace(/sendJson\(res, 200/, 'sendJson(res, 500'); assert.notEqual(without200, src, 'the mutation must actually change the health status code'); assert.throws(() => assertHealthEndpointSource(without200), /HTTP 200/); }); test('removing the healthy report makes the healthy-report criterion fail (mutation probe)', () => { const src = read(SERVER_SRC); const withoutOk = src.replace(/status:\s*'ok'/, "status: 'nope'"); assert.notEqual(withoutOk, src, 'the mutation must actually change the health payload'); assert.throws(() => assertHealthEndpointSource(withoutOk), /healthy application/); }); test('a placeholder entrypoint (no server at all) fails the health-endpoint criterion (mutation probe)', () => { assert.throws(() => assertHealthEndpointSource('export {};\n'), /createServer/); });