[E00-S02-T03] App health endpoint succeeds #384

Merged
kpcto merged 3 commits from feature/170 into main 2026-08-29 00:27:17 +00:00
Member

What changed

Implements the [E00-S02-T03] app health endpoint (#170) on top of the [E00-S02-T01/T02] Compose baseline (#59): the application now serves GET /health and stays up.

  • apps/server/src/index.ts — replaces the bootstrap placeholder (export {};) with a minimal serving process built on Node's built-in node:http (no runtime dependencies, lockfile untouched except the type-definitions devDependency below): an HTTP server that answers GET /health with HTTP 200 and {"status":"ok"} (reports a healthy application), 404 for anything else, listens on port 3000 (default, PORT-overridable — matching Dockerfile EXPOSE 3000 / compose :3000), and closes cleanly on SIGTERM/SIGINT so docker compose down / docker stop are graceful. The Fastify 5 application shell (and the real HTTP API) remains a later story (ADR E01-S01-T03).
  • apps/server/package.json + pnpm-lock.yaml — adds the exact devDependency @types/node@24.13.3 (newest 24.x type-definition line, matching engines.node: 24.x) so the server type-checks; the lockfile is regenerated with the pinned pnpm 11.23.0. This is the lockfile's first scoped package ('@types/node@24.13.3':).
  • apps/server/tsconfig.json — adds "types": ["node"]: TypeScript 6.0.3 no longer auto-includes @types packages, so the server package must declare its type roots explicitly (scoped to the server package only — core/extension placeholders use no Node APIs).
  • apps/server/Dockerfile + compose.yaml — header comments updated: T03 health endpoint no longer out of scope; the app container now stays up serving the health endpoint.
  • docs/development/non-container.md — the Run/Troubleshooting text that described the server as an exiting placeholder now documents the health endpoint (doc-accuracy follow-up to the behavior change).
  • tests/health-endpoint.test.mjs (new) — locks in both acceptance criteria: static assertions on the committed entrypoint, non-vacuous mutation probes, and a real HTTP smoke test that boots the committed server source (Node type stripping, no build step) and asserts GET /health → 200 + {"status":"ok"}.
  • tests/compose-config.test.mjs — the Docker-gated real-stack probe now asserts the app container stays running (instead of tolerating an exit-0 placeholder) and probes GET /health inside the container (200 + healthy body).
  • tests/frozen-install.test.mjs — helper fix: lockedPackageKeys now strips pnpm's YAML quoting on scoped package keys (e.g. '@types/node@24.13.3':) so the lockfile↔virtual-store exact-match probe works with the lockfile's first scoped dependency. Probe semantics unchanged (still fails on any drift) plus a new non-vacuous case for quoted-key extraction.

Explicitly out of scope per the brief, not touched: PostgreSQL health gate (E00-S02-T02 — unchanged behavior), DB volume persistence (E00-S02-T04), non-root execution (E00-S02-T05), Fastify 5 application shell.

Criterion → test table

Acceptance criterion Test (fails without the committed state)
app health endpoint succeeds tests/health-endpoint.test.mjs — "the app health endpoint succeeds (committed entrypoint answers GET /health with HTTP 200)": the committed apps/server/src/index.ts creates an HTTP server (createServer), routes GET /health, answers with HTTP 200, and listens on the application port (3000 default). Non-vacuous: mutation probes removing the /health route / changing the 200 to 500 / a placeholder entrypoint all fail the assertion. "an HTTP smoke test against the booted server succeeds for GET /health (200 + healthy body)" boots the committed server source on an ephemeral port and asserts a real 2xx response (issue test plan "HTTP smoke test passes against the health endpoint"). Docker-gated: tests/compose-config.test.mjs "docker compose up -d starts the database and application containers" asserts the app container stays running and GET /health answers 2xx inside the container
the endpoint reports a healthy application tests/health-endpoint.test.mjs — "the endpoint reports a healthy application (committed health payload is {"status":"ok"})": the committed health payload reports status: 'ok'. The HTTP smoke test asserts the booted endpoint's body is exactly {"status":"ok"}. Non-vacuous: mutation probe changing the payload to 'nope' fails. Docker-gated compose-config probe additionally asserts "status":"ok" in the container response

Test plan executed

  • pnpm run typecheck → 3/3 packages Done on Node 24.20.0 ✓ (includes tsc --noEmit for the server with the new types: ["node"] config)
  • pnpm run build → 3/3 packages emit dist/ ✓; the compiled apps/server/dist/index.js was booted and answered GET /health → 200 {"status":"ok"} (manual probe) ✓
  • pnpm test (full suite, Node 24.20.0) → 71/71 pass, 0 fail, 2 skipped (the two Docker-gated probes skip where no daemon exists) ✓ — the one intermediate failure (frozen-install virtual-store probe) was the quoted-scoped-key helper limitation, fixed in the third commit and re-verified green
  • node --test tests/health-endpoint.test.mjs → 7/7 pass including the real HTTP smoke test (server booted on an ephemeral port, GET /health → 200 {"status":"ok"}) ✓
  • node --test tests/compose-config.test.mjs → 13 pass / 2 skip (Docker probes skip cleanly — no daemon in this sandbox) ✓
  • Lockfile regenerated with the pinned pnpm 11.23.0; the diff is exactly the @types/node@24.13.3 + undici-types@7.18.2 additions; the frozen-install path (clean clone → pnpm install --frozen-lockfile → exact virtual-store match) passes locally ✓

Risks / notes

  • TypeScript 6.0.3 requires an explicit "types" entry for @types packages; the fix is scoped to apps/server/tsconfig.json only.
  • @types/node@24.13.3 is exact-pinned per workspace convention (same style as typescript@6.0.3); it is the newest 24.x line available in the registry.
  • The health payload is intentionally minimal ({"status":"ok"}); richer health reporting (DB connectivity, uptime) belongs to later stories — the container-level PostgreSQL health gate is already T02's.
  • Docker-gated tests skip without a daemon, so the suite stays green everywhere while giving real container-level validation where Docker exists.

Refs #170

## What changed Implements the [E00-S02-T03] app health endpoint (#170) on top of the [E00-S02-T01/T02] Compose baseline (#59): the application now **serves `GET /health` and stays up**. - **`apps/server/src/index.ts`** — replaces the bootstrap placeholder (`export {};`) with a minimal serving process built on Node's built-in `node:http` (**no runtime dependencies, lockfile untouched except the type-definitions devDependency below**): an HTTP server that answers `GET /health` with HTTP 200 and `{"status":"ok"}` (reports a healthy application), 404 for anything else, listens on port 3000 (default, `PORT`-overridable — matching Dockerfile `EXPOSE 3000` / compose `:3000`), and closes cleanly on SIGTERM/SIGINT so `docker compose down` / `docker stop` are graceful. The Fastify 5 application shell (and the real HTTP API) remains a later story (ADR E01-S01-T03). - **`apps/server/package.json` + `pnpm-lock.yaml`** — adds the exact devDependency `@types/node@24.13.3` (newest 24.x type-definition line, matching `engines.node: 24.x`) so the server type-checks; the lockfile is regenerated with the pinned pnpm 11.23.0. This is the lockfile's **first scoped package** (`'@types/node@24.13.3':`). - **`apps/server/tsconfig.json`** — adds `"types": ["node"]`: TypeScript 6.0.3 no longer auto-includes `@types` packages, so the server package must declare its type roots explicitly (scoped to the server package only — core/extension placeholders use no Node APIs). - **`apps/server/Dockerfile`** + **`compose.yaml`** — header comments updated: T03 health endpoint no longer out of scope; the app container now stays up serving the health endpoint. - **`docs/development/non-container.md`** — the Run/Troubleshooting text that described the server as an exiting placeholder now documents the health endpoint (doc-accuracy follow-up to the behavior change). - **`tests/health-endpoint.test.mjs`** (new) — locks in both acceptance criteria: static assertions on the committed entrypoint, non-vacuous mutation probes, and a real **HTTP smoke test** that boots the committed server source (Node type stripping, no build step) and asserts `GET /health` → 200 + `{"status":"ok"}`. - **`tests/compose-config.test.mjs`** — the Docker-gated real-stack probe now asserts the `app` container stays **running** (instead of tolerating an exit-0 placeholder) and probes `GET /health` inside the container (200 + healthy body). - **`tests/frozen-install.test.mjs`** — helper fix: `lockedPackageKeys` now strips pnpm's YAML quoting on scoped package keys (e.g. `'@types/node@24.13.3':`) so the lockfile↔virtual-store exact-match probe works with the lockfile's first scoped dependency. Probe semantics unchanged (still fails on any drift) plus a new non-vacuous case for quoted-key extraction. Explicitly out of scope per the brief, **not touched**: PostgreSQL health gate (E00-S02-T02 — unchanged behavior), DB volume persistence (E00-S02-T04), non-root execution (E00-S02-T05), Fastify 5 application shell. ## Criterion → test table | Acceptance criterion | Test (fails without the committed state) | | --- | --- | | app health endpoint succeeds | `tests/health-endpoint.test.mjs` — "the app health endpoint succeeds (committed entrypoint answers GET /health with HTTP 200)": the committed `apps/server/src/index.ts` creates an HTTP server (`createServer`), routes `GET /health`, answers with HTTP 200, and listens on the application port (3000 default). Non-vacuous: mutation probes removing the `/health` route / changing the 200 to 500 / a placeholder entrypoint all fail the assertion. **"an HTTP smoke test against the booted server succeeds for GET /health (200 + healthy body)"** boots the committed server source on an ephemeral port and asserts a real 2xx response (issue test plan "HTTP smoke test passes against the health endpoint"). Docker-gated: `tests/compose-config.test.mjs` "docker compose up -d starts the database and application containers" asserts the `app` container stays **running** and `GET /health` answers 2xx inside the container | | the endpoint reports a healthy application | `tests/health-endpoint.test.mjs` — "the endpoint reports a healthy application (committed health payload is {"status":"ok"})": the committed health payload reports `status: 'ok'`. The HTTP smoke test asserts the booted endpoint's body is exactly `{"status":"ok"}`. Non-vacuous: mutation probe changing the payload to `'nope'` fails. Docker-gated compose-config probe additionally asserts `"status":"ok"` in the container response | ## Test plan executed - `pnpm run typecheck` → 3/3 packages `Done` on Node 24.20.0 ✓ (includes `tsc --noEmit` for the server with the new `types: ["node"]` config) - `pnpm run build` → 3/3 packages emit `dist/` ✓; the compiled `apps/server/dist/index.js` was booted and answered `GET /health` → 200 `{"status":"ok"}` (manual probe) ✓ - `pnpm test` (full suite, Node 24.20.0) → **71/71 pass, 0 fail, 2 skipped** (the two Docker-gated probes skip where no daemon exists) ✓ — the one intermediate failure (frozen-install virtual-store probe) was the quoted-scoped-key helper limitation, fixed in the third commit and re-verified green - `node --test tests/health-endpoint.test.mjs` → **7/7 pass** including the real HTTP smoke test (server booted on an ephemeral port, `GET /health` → 200 `{"status":"ok"}`) ✓ - `node --test tests/compose-config.test.mjs` → 13 pass / 2 skip (Docker probes skip cleanly — no daemon in this sandbox) ✓ - Lockfile regenerated with the pinned pnpm 11.23.0; the diff is exactly the `@types/node@24.13.3` + `undici-types@7.18.2` additions; the frozen-install path (clean clone → `pnpm install --frozen-lockfile` → exact virtual-store match) passes locally ✓ ## Risks / notes - TypeScript 6.0.3 requires an explicit `"types"` entry for `@types` packages; the fix is scoped to `apps/server/tsconfig.json` only. - `@types/node@24.13.3` is exact-pinned per workspace convention (same style as `typescript@6.0.3`); it is the newest 24.x line available in the registry. - The health payload is intentionally minimal (`{"status":"ok"}`); richer health reporting (DB connectivity, uptime) belongs to later stories — the container-level PostgreSQL health *gate* is already T02's. - Docker-gated tests `skip` without a daemon, so the suite stays green everywhere while giving real container-level validation where Docker exists. Refs #170
bot-implementer added 3 commits 2026-08-29 00:24:19 +00:00
Member
agent: tester
phase: start
objective: "Independently probe PR #384 for issue #170 ([E00-S02-T03] App health endpoint succeeds)"
pr: Fabrika/PersonalBlog#384 (feature/170 -> main, head dfb7b0e)
acceptance:
  - "app health endpoint succeeds"
  - "the endpoint reports a healthy application"
plan:
  - review the PR diff (apps/server/src/index.ts, apps/server/{package.json,tsconfig.json}, compose.yaml, tests/*)
  - independently run tests/health-endpoint.test.mjs and the broader suite where the environment allows
  - boot the committed server source and hit GET /health (expect 200 + {"status":"ok"})
  - validate the Compose config where Docker is available
  - inspect CI runs on the PR and collect job links
  - post evidence comment with CI links
constraints:
  - never edit the implementer's tests in the same PR
```yaml agent: tester phase: start objective: "Independently probe PR #384 for issue #170 ([E00-S02-T03] App health endpoint succeeds)" pr: Fabrika/PersonalBlog#384 (feature/170 -> main, head dfb7b0e) acceptance: - "app health endpoint succeeds" - "the endpoint reports a healthy application" plan: - review the PR diff (apps/server/src/index.ts, apps/server/{package.json,tsconfig.json}, compose.yaml, tests/*) - independently run tests/health-endpoint.test.mjs and the broader suite where the environment allows - boot the committed server source and hit GET /health (expect 200 + {"status":"ok"}) - validate the Compose config where Docker is available - inspect CI runs on the PR and collect job links - post evidence comment with CI links constraints: - never edit the implementer's tests in the same PR ```
kpcto merged commit 6a295556ed into main 2026-08-29 00:27:17 +00:00
kpcto deleted branch feature/170 2026-08-29 00:27:17 +00:00
Member
agent: tester
phase: complete
objective: "Independently probe PR #384 for issue #170 ([E00-S02-T03] App health endpoint succeeds)"
pr: Fabrika/PersonalBlog#384 (feature/170 -> main, head dfb7b0e)
verdict: PASS

acceptance:
  - criterion: "app health endpoint succeeds"
    result: PASS
    evidence:
      - "apps/server/src/index.ts builds a node:http server (createServer), routes GET /health, and answers with HTTP 200 (sendJson(res, 200, HEALTH_PAYLOAD))"
      - "tests/health-endpoint.test.mjs 'the app health endpoint succeeds (committed entrypoint answers GET /health with HTTP 200)' PASS; mutation probes (removing /health route, 200 -> 500, placeholder entrypoint) all PASS (non-vacuous)"
      - "real HTTP smoke test boots the committed server source on an ephemeral port and asserts GET /health -> 2xx (PASS)"
  - criterion: "the endpoint reports a healthy application"
    result: PASS
    evidence:
      - "committed health payload is HEALTH_PAYLOAD = JSON.stringify({ status: 'ok' })"
      - "tests/health-endpoint.test.mjs 'the endpoint reports a healthy application (committed health payload is {\"status\":\"ok\"})' PASS; mutation probe (status ok -> nope) PASS (non-vacuous)"
      - "smoke test asserts the booted body status === 'ok'; compose-config Docker-gated probe asserts \"status\":\"ok\" in-container"

independent_probe:
  - "checked out feature/170 at head dfb7b0e in a clean workspace (working tree clean, no edits)"
  - "node --test tests/health-endpoint.test.mjs -> 7/7 pass, 0 fail (including the real HTTP smoke test that boots the committed .ts source via Node type stripping)"
  - "booted apps/server/src/index.ts directly (node --experimental-strip-types, PORT=43124) and probed with node fetch: GET /health -> 200, content-type application/json; charset=utf-8, body {\"status\":\"ok\"}; GET /nope -> 404 {\"error\":\"not found\"}; POST /health -> 404; SIGTERM -> clean exit"
  - "node --test tests/compose-config.test.mjs -> 13 pass / 0 fail / 2 skip (the two Docker-gated real-stack probes skip: no docker CLI/daemon in this sandbox)"
  - "node --test tests/*.test.mjs -> 59 pass / 12 fail / 2 skip; the 12 failures are the pre-existing environment baseline (no pnpm, node 22 vs engines >=24): frozen-install (5), node-engine (1), root-commands (3), strict-tsconfig (2), typescript-pin (1). None touch health-endpoint or compose-config; the PR adds 7 health-endpoint tests (all pass) and the compose-config changes all pass"
  - "diff scope: git diff --name-status main...feature/170 = exactly 10 files (9 modified + 1 added), matching the PR description; 3 commits (9c049ce feat, f7257f9 test, dfb7b0e fix)"

ci:
  run: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/66 (conclusion: success, head dfb7b0e, pull_request)"
  job: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/66/jobs/78 (Frozen lockfile install, success)"
  note: "CI is green but .gitea/workflows/ci.yml only runs pnpm install --frozen-lockfile + workspace-group verify — it does NOT run the test suite. The test evidence above is from my independent local execution."

observations:
  - "non-blocking: GET /health?x=1 returns 404 because the route compares req.url === '/health' exactly (no pathname/query parsing). The acceptance criteria are still met for a bare GET /health; only health checkers that append a query string would get a 404."
  - "constraint respected: I did not edit the implementer's tests or any file — all probing was read-only (git checkout + node --test + manual boot); no commits pushed."
```yaml agent: tester phase: complete objective: "Independently probe PR #384 for issue #170 ([E00-S02-T03] App health endpoint succeeds)" pr: Fabrika/PersonalBlog#384 (feature/170 -> main, head dfb7b0e) verdict: PASS acceptance: - criterion: "app health endpoint succeeds" result: PASS evidence: - "apps/server/src/index.ts builds a node:http server (createServer), routes GET /health, and answers with HTTP 200 (sendJson(res, 200, HEALTH_PAYLOAD))" - "tests/health-endpoint.test.mjs 'the app health endpoint succeeds (committed entrypoint answers GET /health with HTTP 200)' PASS; mutation probes (removing /health route, 200 -> 500, placeholder entrypoint) all PASS (non-vacuous)" - "real HTTP smoke test boots the committed server source on an ephemeral port and asserts GET /health -> 2xx (PASS)" - criterion: "the endpoint reports a healthy application" result: PASS evidence: - "committed health payload is HEALTH_PAYLOAD = JSON.stringify({ status: 'ok' })" - "tests/health-endpoint.test.mjs 'the endpoint reports a healthy application (committed health payload is {\"status\":\"ok\"})' PASS; mutation probe (status ok -> nope) PASS (non-vacuous)" - "smoke test asserts the booted body status === 'ok'; compose-config Docker-gated probe asserts \"status\":\"ok\" in-container" independent_probe: - "checked out feature/170 at head dfb7b0e in a clean workspace (working tree clean, no edits)" - "node --test tests/health-endpoint.test.mjs -> 7/7 pass, 0 fail (including the real HTTP smoke test that boots the committed .ts source via Node type stripping)" - "booted apps/server/src/index.ts directly (node --experimental-strip-types, PORT=43124) and probed with node fetch: GET /health -> 200, content-type application/json; charset=utf-8, body {\"status\":\"ok\"}; GET /nope -> 404 {\"error\":\"not found\"}; POST /health -> 404; SIGTERM -> clean exit" - "node --test tests/compose-config.test.mjs -> 13 pass / 0 fail / 2 skip (the two Docker-gated real-stack probes skip: no docker CLI/daemon in this sandbox)" - "node --test tests/*.test.mjs -> 59 pass / 12 fail / 2 skip; the 12 failures are the pre-existing environment baseline (no pnpm, node 22 vs engines >=24): frozen-install (5), node-engine (1), root-commands (3), strict-tsconfig (2), typescript-pin (1). None touch health-endpoint or compose-config; the PR adds 7 health-endpoint tests (all pass) and the compose-config changes all pass" - "diff scope: git diff --name-status main...feature/170 = exactly 10 files (9 modified + 1 added), matching the PR description; 3 commits (9c049ce feat, f7257f9 test, dfb7b0e fix)" ci: run: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/66 (conclusion: success, head dfb7b0e, pull_request)" job: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/66/jobs/78 (Frozen lockfile install, success)" note: "CI is green but .gitea/workflows/ci.yml only runs pnpm install --frozen-lockfile + workspace-group verify — it does NOT run the test suite. The test evidence above is from my independent local execution." observations: - "non-blocking: GET /health?x=1 returns 404 because the route compares req.url === '/health' exactly (no pathname/query parsing). The acceptance criteria are still met for a bare GET /health; only health checkers that append a query string would get a 404." - "constraint respected: I did not edit the implementer's tests or any file — all probing was read-only (git checkout + node --test + manual boot); no commits pushed." ```
Sign in to join this conversation.