---
phase: 05-deploy
plan: 07
type: execute
wave: 1
depends_on: []
files_modified:
  - apps/server/test/health.test.ts
autonomous: true
requirements: [DEP-05]
tags: [health, draining-state, fly-check, vitest, sigterm]
must_haves:
  truths:
    - "apps/server/test/health.test.ts exercises makeHealthHandler() in three states: ok (200), starting (503), draining (503)"
    - "Test asserts the JSON shape returned by makeHealthHandler matches HealthStatus interface verbatim ({status, ws_ready, rooms_loaded})"
    - "Draining-state test simulates Phase 4 sigterm.ts setDraining() flow: when status === 'draining', response is 503 with status: 'draining'"
    - "Tests carry [unit->REQ-DEP-05] tags so trace:check counts the unit-stage closure"
  artifacts:
    - path: "apps/server/test/health.test.ts"
      provides: "Unit tests for makeHealthHandler (extends Phase 4 health.ts coverage)"
      min_lines: 60
      contains: "[unit->REQ-DEP-05]|makeHealthHandler|status: 'ok'|status: 'draining'|status: 'starting'|503|200"
  key_links:
    - from: "apps/server/test/health.test.ts"
      to: "apps/server/src/health.ts (Phase 4 endpoint)"
      via: "import { makeHealthHandler } from"
      pattern: "from '../src/health"
---

<objective>
Add unit tests covering `apps/server/src/health.ts` in all three documented states (ok / starting / draining). Closes DEP-05 [unit] stage; complements DEP-05 [doc] from Plan 02 (fly.{staging,prod}.toml http_check) and the implicit DEP-05 [impl] already in Phase 4.

Purpose: Phase 4 produced `health.ts` but never tested the draining path. Phase 5 adds the test so trace:check shows DEP-05 unit-stage closed, and so any future regression in `setDraining()` semantics fails CI.
Output: `apps/server/test/health.test.ts`. Standalone vitest file using the same pattern as `apps/server/test/log.test.ts` / `room-key.test.ts`.
</objective>

<execution_context>
@$HOME/.ccs/instances/bigscreen/get-shit-done/workflows/execute-plan.md
@$HOME/.ccs/instances/bigscreen/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-deploy/05-CONTEXT.md
@.planning/phases/05-deploy/05-RESEARCH.md
@apps/server/src/health.ts
@apps/server/src/sigterm.ts
@apps/server/test/log.test.ts
@apps/server/test/room-key.test.ts

<interfaces>
<!-- apps/server/src/health.ts (verbatim, 17 lines): -->
<!--   export interface HealthStatus { status: 'ok' | 'starting' | 'draining'; ws_ready: boolean; rooms_loaded: number; } -->
<!--   export function makeHealthHandler(get: () => HealthStatus): (req, res) => void -->
<!--   Handler returns 200 if status==='ok', else 503; body is the get() result as JSON. -->

<!-- Phase 4 sigterm.ts contract: setDraining(true) flips internal state; the get() callback in index.ts reflects this in HealthStatus.status. -->
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: health.test.ts — three-state unit coverage with mocked Express req/res</name>
  <files>apps/server/test/health.test.ts</files>
  <read_first>
    - apps/server/src/health.ts (entire file — interface + handler factory)
    - apps/server/test/log.test.ts (vitest pattern + import paths in this workspace)
    - apps/server/test/room-key.test.ts (mock-Express pattern reference)
    - apps/server/src/sigterm.ts (setDraining contract — health 'draining' state path)
    - .planning/phases/05-deploy/05-VALIDATION.md row 5-07-01 (extends Phase 4 health.test.ts — but no health.test.ts exists yet; this plan creates it fresh)
  </read_first>
  <behavior>
    - Test file uses vitest `describe`/`it`/`expect` like log.test.ts.
    - First line comment: `// [unit->REQ-DEP-05]`
    - Construct a fake `Response` object with `.status(n)` returning a `.json(body)` chainable. Capture both calls.
    - Three test cases:
      1. `status: 'ok', ws_ready: true, rooms_loaded: 3` → response.status called with 200; response.json receives the same object
      2. `status: 'starting', ws_ready: false, rooms_loaded: 0` → response.status called with 503; response.json receives the same object
      3. `status: 'draining', ws_ready: true, rooms_loaded: 3` → response.status called with 503; response.json receives the same object
    - Fourth test: get callback is invoked once per request (not memoized) — verify by mutating returned state between two calls and observing different responses.
  </behavior>
  <action>
    Create `apps/server/test/health.test.ts` with exactly:
    ```typescript
    // [unit->REQ-DEP-05]
    // apps/server/test/health.test.ts
    // Source: 05-PLAN-07. Extends DEP-05 unit coverage to all three documented
    // health states. Phase 4 shipped health.ts impl; Phase 5 lands the unit gate.
    import { describe, it, expect } from 'vitest';
    import type { Request, Response } from 'express';
    import { makeHealthHandler, type HealthStatus } from '../src/health.js';

    function fakeRes() {
      const calls: { status?: number; body?: unknown } = {};
      const res = {
        status(n: number) {
          calls.status = n;
          return {
            json(b: unknown) {
              calls.body = b;
              return res as unknown as Response;
            },
          };
        },
      } as unknown as Response;
      return { res, calls };
    }

    describe('makeHealthHandler', () => {
      it('returns 200 + ok body when status === "ok"', () => {
        const status: HealthStatus = { status: 'ok', ws_ready: true, rooms_loaded: 3 };
        const handler = makeHealthHandler(() => status);
        const { res, calls } = fakeRes();
        handler({} as Request, res);
        expect(calls.status).toBe(200);
        expect(calls.body).toEqual({ status: 'ok', ws_ready: true, rooms_loaded: 3 });
      });

      it('returns 503 + starting body when status === "starting"', () => {
        const status: HealthStatus = { status: 'starting', ws_ready: false, rooms_loaded: 0 };
        const handler = makeHealthHandler(() => status);
        const { res, calls } = fakeRes();
        handler({} as Request, res);
        expect(calls.status).toBe(503);
        expect(calls.body).toEqual({ status: 'starting', ws_ready: false, rooms_loaded: 0 });
      });

      it('returns 503 + draining body when status === "draining" (Phase 4 sigterm.ts setDraining path)', () => {
        const status: HealthStatus = { status: 'draining', ws_ready: true, rooms_loaded: 3 };
        const handler = makeHealthHandler(() => status);
        const { res, calls } = fakeRes();
        handler({} as Request, res);
        expect(calls.status).toBe(503);
        expect(calls.body).toEqual({ status: 'draining', ws_ready: true, rooms_loaded: 3 });
      });

      it('invokes the get() callback on every request (no memoization)', () => {
        let mutable: HealthStatus = { status: 'starting', ws_ready: false, rooms_loaded: 0 };
        const handler = makeHealthHandler(() => mutable);

        const a = fakeRes();
        handler({} as Request, a.res);
        expect(a.calls.status).toBe(503);

        mutable = { status: 'ok', ws_ready: true, rooms_loaded: 1 };

        const b = fakeRes();
        handler({} as Request, b.res);
        expect(b.calls.status).toBe(200);
        expect(b.calls.body).toEqual({ status: 'ok', ws_ready: true, rooms_loaded: 1 });
      });
    });
    ```
  </action>
  <verify>
    <automated>pnpm --filter @rebno/server test -- health.test.ts --run</automated>
  </verify>
  <acceptance_criteria>
    - File exists at `apps/server/test/health.test.ts`
    - Line 1 contains `[unit->REQ-DEP-05]`
    - Tests cover three documented status states (`ok`, `starting`, `draining`) with correct HTTP codes (200, 503, 503) and body shape
    - Tests the no-memoization invariant via mutation between calls
    - `pnpm --filter @rebno/server test -- health.test.ts --run` exits 0
    - `grep -c '\[<unit>->REQ-DEP-05\]' apps/server/test/health.test.ts` returns ≥ 1
  </acceptance_criteria>
  <done>
    Tests run green; verify command exits 0; trace tag present.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Fly proxy → /health | Fly check probe is the only consumer in Phase 5; staging-invite middleware MUST NOT gate /health (Plan 10 verifies) |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-DEP-HEALTH-503-FLAP | D (Health flapping triggers Fly LB removal) | health handler | accept-with-test | Three-state unit test pins the contract; flapping would manifest as a different state machine in get(), out of scope for this plan |
| T-DEP-HEALTH-LEAK | I (Internal state leaked via /health body) | response body | mitigate | Body shape is HealthStatus = {status, ws_ready, rooms_loaded} only; no tokens or PII. Test asserts exact shape |
| T-DEP-DRAINING-INVISIBLE | D (Drain state not surfaced) | sigterm.ts setDraining → health get() callback | mitigate | Test 3 explicitly exercises 'draining' → 503; ensures Phase 4 setDraining wire stays connected |
</threat_model>

<verification>
- File exists with all acceptance criteria
- Tests run green
- (Cross-plan) `pnpm trace:check` after this plan + Plan 02 will report DEP-05 unit + doc closed; impl is already from Phase 4
- (Cross-plan) Plan 14 verify-phase-5 runs full server test suite; this test is part of it
</verification>

<success_criteria>
- DEP-05 [unit] tag present and counted by trace:check
- All four test cases pass
- No mock framework added (uses inline fake object — matches log.test.ts style)
</success_criteria>

<output>
After completion, create `.planning/phases/05-deploy/05-07-SUMMARY.md` capturing:
- Three states + body shape pinned
- No regression risk to Phase 4 health.ts (this plan only ADDS tests)
- Note: Fly Volume mount + auto_stop verification (additional /health acceptance) lives in Plan 11 soak (live `curl https://rebno-staging.fly.dev/health`) and Plan 13 RESTORE.md drill
</output>

## Validation
Updates 05-VALIDATION.md row `5-07-01` (REQ-DEP-05 / /health reports {status, ws_ready, rooms_loaded}) → automated command becomes `pnpm --filter @rebno/server test -- health.test.ts --run`. Status flips to ✅ green when test passes. Live integration verified during Plan 11 soak via Fly check probe.
