Tasks: US4 — Backend-Aware Health Checks¶
Input: Design documents from /docs/product-specs/024-claude-serve-deploy/
Prerequisites: plan.md (required), spec.md (required), data-model.md (required), contracts/health-endpoint.md (required)
Tests: TDD approach — write tests FIRST, verify they FAIL, then implement.
User Story: An infrastructure operator monitors deployed Claude agents. The health check endpoint validates not just that the server process is running, but that Claude backend prerequisites are satisfied: Node.js available, credentials accessible, SDK subprocess can be spawned. Enables load balancers/orchestrators to route traffic only to genuinely ready instances.
Format: [ID] [P?] [Story] Description¶
- [P]: Can run in parallel (different files, no dependencies)
- [US4]: All user story tasks belong to User Story 4
- Include exact file paths in descriptions
Path Conventions¶
- Single project:
src/holodeck/,tests/at repository root
Phase 1: Setup¶
Purpose: Verify US1 dependencies are in place and understand the current health endpoint implementation.
- T001 Verify US1 has added
backend_ready: bool = Trueandbackend_diagnostics: list[str] = []fields toHealthResponseinsrc/holodeck/serve/models.py. If fields are missing, add them per data-model.md contract. Confirm the model serializes correctly with default values (existing consumers see no change) - T002 Verify
validate_nodejs()insrc/holodeck/lib/backends/validators.pyexists and is importable. Verifyvalidate_credentials()in the same file exists and is importable. These are the validators that health checks will reuse - T003 Verify the current health endpoint in
src/holodeck/serve/server.pyreturnsHealthResponsewithstatus,agent_name,agent_ready,active_sessions,uptime_seconds. Understand the_register_health_endpoints()method structure
Phase 2: Foundational — US1 Dependencies¶
Purpose: Ensure the HealthResponse model changes from US1 are in place. US1 adds the fields; US4 populates them with live checks.
- T004 If US1 has not yet added
backend_readyandbackend_diagnosticstoHealthResponseinsrc/holodeck/serve/models.py, add them now: backend_ready: bool = Field(default=True, description="Whether backend prerequisites are satisfied")backend_diagnostics: list[str] = Field(default_factory=list, description="Diagnostic messages (empty when healthy)")-
Confirm existing serialization tests still pass (new fields have defaults, so backward-compatible)
-
T005 Verify that
ProviderEnum.ANTHROPICis accessible fromsrc/holodeck/models/llm.pyfor provider detection in health check logic. Confirmself.agent_config.model.provideris the correct path to read the provider fromAgentServer
Checkpoint: HealthResponse model has backend_ready and backend_diagnostics fields. Validators are importable. Provider detection path is confirmed.
Phase 3: US4 Implementation — Backend Health Check Logic¶
Purpose: Implement check_backend_health() on AgentServer, wire it into the health endpoint, and add TTL-based caching to avoid per-request overhead.
3A: Core Health Check Method¶
Implement
check_backend_health()that reuses existing validators and returns backend readiness status.
Tests (TDD — write FIRST, verify they FAIL)¶
-
T006 [P] [US4] Write unit test
test_check_backend_health_healthy_when_anthropic_prerequisites_metintests/unit/serve/test_server.py— create anAgentServerwithagent_config.model.provider == ProviderEnum.ANTHROPIC. Mockvalidate_nodejs()to succeed (no exception). Mockvalidate_credentials()to return a valid env dict. Callcheck_backend_health(). Assert returns(True, [])— backend_ready is True, diagnostics list is empty -
T007 [P] [US4] Write unit test
test_check_backend_health_unhealthy_when_nodejs_missingintests/unit/serve/test_server.py— create anAgentServerwithagent_config.model.provider == ProviderEnum.ANTHROPIC. Mockvalidate_nodejs()to raiseConfigError. Callcheck_backend_health(). Assert returns(False, ["Node.js not found on PATH (required for Claude Agent SDK)"])— backend_ready is False, diagnostics contains Node.js error message -
T008 [P] [US4] Write unit test
test_check_backend_health_degraded_when_credentials_invalidintests/unit/serve/test_server.py— create anAgentServerwithagent_config.model.provider == ProviderEnum.ANTHROPIC. Mockvalidate_nodejs()to succeed. Mockvalidate_credentials()to raiseConfigErrorwith credential message. Callcheck_backend_health(). Assert returns(False, ["...credential error message..."])— backend_ready is False, diagnostics contains credential error -
T009 [P] [US4] Write unit test
test_check_backend_health_skipped_for_non_anthropic_providerintests/unit/serve/test_server.py— create anAgentServerwithagent_config.model.provider == ProviderEnum.OPENAI. Callcheck_backend_health(). Assert returns(True, [])— non-Anthropic providers skip backend validation entirely, always report healthy -
T010 [P] [US4] Write unit test
test_check_backend_health_collects_multiple_failuresintests/unit/serve/test_server.py— create anAgentServerwithagent_config.model.provider == ProviderEnum.ANTHROPIC. Mockvalidate_nodejs()to raiseConfigError. Mockvalidate_credentials()to also raiseConfigError. Callcheck_backend_health(). Assert returns(False, [node_error_msg, credential_error_msg])— both diagnostics collected, not short-circuited on first failure
Implementation¶
- T011 [US4] Implement
check_backend_health()method onAgentServerinsrc/holodeck/serve/server.py: - Signature:
async def check_backend_health(self) -> tuple[bool, list[str]] - If
self.agent_config.model.provider != ProviderEnum.ANTHROPIC, return(True, []) - For Anthropic provider, call
validate_nodejs()andvalidate_credentials(self.agent_config.model)inside try/except blocks - Collect ALL diagnostic messages (do not short-circuit on first failure)
- Return
(backend_ready, diagnostics)wherebackend_ready = len(diagnostics) == 0 - Import
validate_nodejsandvalidate_credentialsfromholodeck.lib.backends.validators - Import
ConfigErrorfromholodeck.lib.errorsfor the try/except blocks around validator calls - Import
ProviderEnumfromholodeck.models.llm - Run tests T006–T010 and verify they PASS
3B: TTL-Based Caching¶
Cache health check results for 30 seconds to avoid per-request subprocess overhead from
node --versioncalls.
Tests (TDD)¶
-
T012 [P] [US4] Write unit test
test_check_backend_health_caches_result_for_30_secondsintests/unit/serve/test_server.py— create anAgentServerwith Anthropic provider. Mock validators to succeed. Callcheck_backend_health()twice within 30 seconds (mocktime.monotonic()). Assert validators are called only once (cached result returned on second call) -
T013 [P] [US4] Write unit test
test_check_backend_health_cache_expires_after_30_secondsintests/unit/serve/test_server.py— create anAgentServerwith Anthropic provider. Mock validators to succeed. Callcheck_backend_health(). Advance mocked time by 31 seconds. Callcheck_backend_health()again. Assert validators are called twice (cache expired, re-checked) -
T014 [P] [US4] Write unit test
test_check_backend_health_cache_updates_on_state_changeintests/unit/serve/test_server.py— callcheck_backend_health()with validators succeeding (cached as healthy). Advance time past TTL. Change mock sovalidate_nodejs()now raisesConfigError. Callcheck_backend_health()again. Assert result changes from healthy to unhealthy with diagnostics
Implementation¶
- T015 [US4] Add TTL-based caching to
check_backend_health()insrc/holodeck/serve/server.py: - Add instance attributes in
__init__():self._backend_health_cache: tuple[bool, list[str]] | None = Noneandself._backend_health_cache_time: float = 0.0 - Add class constant:
_BACKEND_HEALTH_TTL: float = 30.0 - In
check_backend_health(), checktime.monotonic() - self._backend_health_cache_time < self._BACKEND_HEALTH_TTL— if within TTL, return cached result - Otherwise, run validators, cache result with current timestamp, return result
- Import
timeat module level - Run tests T012–T014 and verify they PASS
3C: Health Endpoint Wiring¶
Wire
check_backend_health()into the/healthand/health/agentendpoints. Map results to status values: healthy, degraded, unhealthy.
Tests (TDD)¶
-
T016 [P] [US4] Write unit test
test_health_endpoint_returns_backend_ready_true_when_healthyintests/unit/serve/test_server.py— createAgentServerwith Anthropic provider, all prerequisites met. UseTestClientto callGET /health. Assert response JSON containsbackend_ready: true,backend_diagnostics: [],status: "healthy" -
T017 [P] [US4] Write unit test
test_health_endpoint_returns_unhealthy_when_nodejs_missingintests/unit/serve/test_server.py— createAgentServerwith Anthropic provider,validate_nodejs()mocked to fail. UseTestClientto callGET /health. Assert response JSON containsbackend_ready: false,backend_diagnostics: ["...node error..."],status: "unhealthy" -
T018 [P] [US4] Write unit test
test_health_endpoint_returns_degraded_when_credentials_invalidintests/unit/serve/test_server.py— createAgentServerwith Anthropic provider, Node.js available but credentials invalid. UseTestClientto callGET /health. Assert response JSON containsbackend_ready: false,status: "degraded",backend_diagnosticsincludes credential error -
T019 [P] [US4] Write unit test
test_health_endpoint_backward_compatible_for_non_anthropicintests/unit/serve/test_server.py— createAgentServerwith OpenAI provider. UseTestClientto callGET /health. Assert response JSON containsbackend_ready: true,backend_diagnostics: []. Verify existing fields (status,agent_name,agent_ready,active_sessions,uptime_seconds) are still present and correct -
T020 [P] [US4] Write unit test
test_health_agent_endpoint_also_includes_backend_fieldsintests/unit/serve/test_server.py— createAgentServerwith Anthropic provider. UseTestClientto callGET /health/agent. Assert response JSON containsbackend_readyandbackend_diagnosticsfields (same behavior as/health)
Implementation¶
- T021 [US4] Modify
_register_health_endpoints()insrc/holodeck/serve/server.py: - In the
health()handler, callawait self.check_backend_health()to get(backend_ready, diagnostics) - Determine status: if
not self.is_readyornot backend_readyand diagnostics indicate hard failure (Node.js missing) →"unhealthy"; ifnot backend_readyand diagnostics indicate soft failure (credentials) →"degraded"; otherwise →"healthy" - Pass
backend_ready=backend_readyandbackend_diagnostics=diagnosticstoHealthResponse - Apply same logic to
health_agent()handler - Status determination logic:
"unhealthy"when Node.js is missing (critical dependency);"degraded"when credentials are invalid (server can accept requests but will fail on invocation);"healthy"when all checks pass - Run tests T016–T020 and verify they PASS
3D: Status Determination Logic¶
Define clear rules for mapping diagnostic types to status values.
Tests (TDD)¶
-
T022 [P] [US4] Write unit test
test_status_unhealthy_when_server_not_ready_regardless_of_backendintests/unit/serve/test_server.py— createAgentServerwhereis_readyis False. CallGET /health. Assertstatus: "unhealthy"even if backend checks pass (server-level unready overrides backend health) -
T023 [P] [US4] Write unit test
test_status_unhealthy_when_both_nodejs_and_credentials_failintests/unit/serve/test_server.py— createAgentServerwith Anthropic provider. Mock bothvalidate_nodejs()andvalidate_credentials()to fail. CallGET /health. Assertstatus: "unhealthy"(Node.js failure escalates to unhealthy) -
T024 [P] [US4] Write unit test
test_status_degraded_only_when_credentials_fail_but_nodejs_presentintests/unit/serve/test_server.py— createAgentServerwith Anthropic provider. Mockvalidate_nodejs()to succeed,validate_credentials()to fail. CallGET /health. Assertstatus: "degraded"(not unhealthy — server can still accept requests for potential credential refresh scenarios)
Implementation¶
- T025 [US4] Extract status determination into a private helper
_determine_health_status()insrc/holodeck/serve/server.py: - Signature:
def _determine_health_status(self, backend_ready: bool, diagnostics: list[str]) -> str - If
not self.is_ready→ return"unhealthy" - If
backend_ready→ return"healthy" - If any diagnostic contains "Node.js" (critical runtime dependency missing) → return
"unhealthy" - Otherwise (credential issues only) → return
"degraded" - Update
_register_health_endpoints()to use this helper - Run tests T022–T024 and verify they PASS
Checkpoint: Health endpoint returns backend_ready, backend_diagnostics, and correct status values. Caching prevents per-request overhead. Backward-compatible for non-Anthropic providers.
Phase 4: Polish¶
Purpose: Code quality and final validation.
- T026 Run
make formatto format all modified/new files with Black + Ruff - T027 Run
make lintand fix any Ruff + Bandit violations insrc/holodeck/serve/server.py,src/holodeck/serve/models.py, and test files - T028 Run
make type-checkand fix any MyPy errors insrc/holodeck/serve/server.py,src/holodeck/serve/models.py - T029 Run full test suite
make testto verify no regressions - T030 Verify acceptance scenarios from user story:
- Scenario 1: Claude-backed serve instance with all prerequisites met — health endpoint returns
{"status": "healthy", "backend_ready": true, "backend_diagnostics": []} - Scenario 2: Claude-backed serve instance where Node.js becomes unavailable — health endpoint returns
{"status": "unhealthy", "backend_ready": false, "backend_diagnostics": ["Node.js not found..."]} - Scenario 3: Claude-backed serve instance where credentials are invalid/expired — health endpoint returns
{"status": "degraded", "backend_ready": false, "backend_diagnostics": ["...credential error..."]}
Dependencies & Execution Order¶
External Dependencies (MUST be complete before US4 begins)¶
- US1:
HealthResponsemodel changes insrc/holodeck/serve/models.py(addsbackend_readyandbackend_diagnosticsfields). If US1 is not complete, Phase 2 (T004) handles adding the fields as a prerequisite - US1:
validators.pyenhancements insrc/holodeck/lib/backends/validators.py(enhancedvalidate_nodejs()with version check). US4 reuses these validators — they must exist and be importable
Phase Dependencies¶
Phase 1 (Setup: T001–T003)
↓
Phase 2 (Foundational: T004–T005)
↓
Phase 3A (Core health check: T006–T011)
↓
Phase 3B (TTL caching: T012–T015) ─── depends on 3A
↓
Phase 3C (Endpoint wiring: T016–T021) ─── depends on 3A, 3B
↓
Phase 3D (Status logic: T022–T025) ─── depends on 3C
↓
Phase 4 (Polish: T026–T030)
Parallel Execution Opportunities¶
Within each phase, [P]-tagged tasks can run in parallel:
- Phase 3A tests: T006, T007, T008, T009, T010 can run in parallel (same file, independent tests)
- Phase 3B tests: T012, T013, T014 can run in parallel (same file, independent tests)
- Phase 3C tests: T016, T017, T018, T019, T020 can run in parallel (same file, independent tests)
- Phase 3D tests: T022, T023, T024 can run in parallel (same file, independent tests)
- Phase 4 quality checks: T026, T027, T028 can run in parallel (independent tools)
Cross-Phase Parallelism¶
- 3A and 3B tests can be written in parallel (both are in
test_server.pybut test different methods) - 3C and 3D tests can be written in parallel (test different aspects of the endpoint)
Implementation Strategy¶
- Start with HealthResponse model verification (Phase 1–2) — confirm US1 has laid the groundwork or add fields if needed
- Build check_backend_health() first (Phase 3A) — this is the core logic that all other phases depend on
- Add caching immediately (Phase 3B) — health endpoints are polled frequently; caching prevents per-request subprocess calls to
node --version - Wire into endpoints (Phase 3C) — connect the method to actual HTTP responses
- Refine status determination (Phase 3D) — extract and test the healthy/degraded/unhealthy classification logic
Key Design Decisions¶
- Reuse existing validators:
validate_nodejs()andvalidate_credentials()fromvalidators.pyare called at both startup (US1) and runtime health checks (US4). No duplication. - Collect all failures: Health check does NOT short-circuit on first failure. All validators run, and all diagnostics are returned so operators see the full picture in one request.
- 30-second TTL cache: Prevents
node --versionsubprocess overhead on every health poll. Cache is per-instance, invalidates naturally after TTL. - Three-tier status:
healthy(all good),degraded(functional but credential issues),unhealthy(critical dependency missing or server not ready). Consumers checkingstatus == "healthy"correctly treat both degraded and unhealthy as not-healthy. - Backward-compatible: New fields have defaults. Non-Anthropic providers always return
backend_ready: true, backend_diagnostics: [].
Key Files Modified/Created¶
| File | Phase | Changes |
|---|---|---|
src/holodeck/serve/models.py |
Phase 2 | Verify/add backend_ready, backend_diagnostics fields on HealthResponse |
src/holodeck/serve/server.py |
Phase 3A–3D | Add check_backend_health(), _determine_health_status(), TTL cache attributes, modify _register_health_endpoints() |
tests/unit/serve/test_server.py |
Phase 3A–3D | Add 19 new unit tests for backend health check logic, caching, endpoint wiring, and status determination |
Notes¶
- [P] tasks = independent tests within the same phase, can run in parallel
- [US4] label maps all user story tasks to User Story 4
- TDD: Verify tests fail before implementing
- Commit after each sub-phase (3A, 3B, 3C, 3D) for clean git history
- Stop at any checkpoint to validate progress
- The health check method is async to support potential future async validators, even though current validators are synchronous
- Status determination is extracted into a helper for testability and to keep endpoint handlers clean