Spaces:
Sleeping
Sleeping
KevinIsInCoding Claude Sonnet 4.6 commited on
Commit Β·
b720671
1
Parent(s): b4e34e7
docs: add unit test design document
Browse filesDefines testing strategy, module coverage targets, and 7-stage
implementation order for Beacon's test suite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- design/unit-test-design.md +172 -0
design/unit-test-design.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Beacon Unit Test Design
|
| 2 |
+
|
| 3 |
+
## Why This Exists
|
| 4 |
+
|
| 5 |
+
Beacon is now stable. Unit tests are the guardrail against regressions as new features land. The core design challenge is that the system mixes two fundamentally different kinds of logic β deterministic computation and LLM orchestration β which require different testing strategies.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Rule 1: Classify Every Code Path Before Testing It
|
| 10 |
+
|
| 11 |
+
Every function in Beacon falls into one of three categories:
|
| 12 |
+
|
| 13 |
+
### Deterministic (test directly, no mocks)
|
| 14 |
+
|
| 15 |
+
These functions take inputs and return outputs with no external dependencies. They are always fast, always repeatable, and should have high coverage.
|
| 16 |
+
|
| 17 |
+
| Function | Location | What makes it deterministic |
|
| 18 |
+
|----------|----------|----------------------------|
|
| 19 |
+
| `haversine_miles` | `models.py` | Pure trigonometry |
|
| 20 |
+
| `PatientProfile.summary` | `models.py` | String formatting from a dataclass |
|
| 21 |
+
| `_evaluate_deterministic` | `agents/eligibility.py` | Operator comparison (<=, >=, ==, !=, in, not_in, between) |
|
| 22 |
+
| `_resolve_patient_value` | `agents/eligibility.py` | Dict/field lookup via static key map |
|
| 23 |
+
| `_compute_overall` | `agents/eligibility.py` | Precedence rule: FAIL > UNKNOWN > PASS |
|
| 24 |
+
| `_flatten_and_rank` | `trials_api.py` | API response reshaping + haversine sort |
|
| 25 |
+
| `_months_from_date` | `agents/intake.py` | Date arithmetic (freeze `datetime.today`) |
|
| 26 |
+
| `_resolve_months` | `agents/intake.py` | Dict key fallback logic |
|
| 27 |
+
| `lookup_disease_profile` | `prompts.py` | File read + string matching (uses real data files on disk) |
|
| 28 |
+
|
| 29 |
+
> **Rule:** `_evaluate_deterministic` is the single highest-value test target in the codebase. It is the only place where the system makes an automated pass/fail judgment about a patient's eligibility. Every operator Γ pass/fail combination must be covered.
|
| 30 |
+
|
| 31 |
+
### I/O-Bound (mock the network, test the logic around it)
|
| 32 |
+
|
| 33 |
+
These functions call external services. The external call is mocked; the logic that wraps it (retry, pagination, parameter construction) is what we test.
|
| 34 |
+
|
| 35 |
+
| Function | External dependency | Mock tool |
|
| 36 |
+
|----------|-------------------|-----------|
|
| 37 |
+
| `geocode_zip` | OpenStreetMap Nominatim (httpx) | `pytest-httpx` |
|
| 38 |
+
| `search_trials_api` | ClinicalTrials.gov API (httpx) | `pytest-httpx` |
|
| 39 |
+
|
| 40 |
+
> **Rule:** Use `pytest-httpx` (not `responses`, not `httpretty`). The codebase uses `httpx` directly. Other HTTP mock libraries intercept at a different layer and will silently miss these calls.
|
| 41 |
+
|
| 42 |
+
### LLM-Orchestrated (mock the client, test the loop logic)
|
| 43 |
+
|
| 44 |
+
These functions call `client.messages.create()` or `client.messages.stream()`. The LLM response is unpredictable; the orchestration logic around it is not. What we test: does the function correctly handle a `tool_use` stop reason, route to the right tool, append messages in the right format, and loop until `end_turn`?
|
| 45 |
+
|
| 46 |
+
| Function | Location | What the test verifies |
|
| 47 |
+
|----------|----------|----------------------|
|
| 48 |
+
| `run_intake_agent` | `agents/intake.py` | identify_disease and submit_profile tool routing |
|
| 49 |
+
| `stream_intake_turn` | `agents/intake.py` | token/text/profile/reset_stream event sequence |
|
| 50 |
+
| `run_research_agent` | `agents/research.py` | search tool loop: 1 call, 2 calls, API error path |
|
| 51 |
+
| `stream_research_agent` | `agents/research.py` | status/token/done event sequence |
|
| 52 |
+
| `run_eligibility_check` | `agents/eligibility.py` | parse β deterministic split β LLM assess orchestration |
|
| 53 |
+
| `bulk_parse_and_strip` | `agents/eligibility.py` | bulk parse + deterministic filter, strip fields |
|
| 54 |
+
| `_parse_criteria` | `agents/eligibility.py` | tool-forced call, struct β dataclass conversion |
|
| 55 |
+
| `_assess_llm` | `agents/eligibility.py` | tool-forced call, output β CriterionAssessment mapping |
|
| 56 |
+
|
| 57 |
+
> **Rule:** Never call the live Anthropic API from tests. Mock `client.messages.create` using `side_effect` lists to simulate multi-turn tool loops. For streaming, mock `client.messages.stream` with a context-manager-compatible fake that yields tokens and returns a final message.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## Rule 2: What Not to Test
|
| 62 |
+
|
| 63 |
+
Some parts of the codebase have no unit-testable logic and should be explicitly excluded from coverage requirements.
|
| 64 |
+
|
| 65 |
+
| File | Reason to skip |
|
| 66 |
+
|------|---------------|
|
| 67 |
+
| `app.py` | Gradio requires a running server; UI event wiring is not unit-testable |
|
| 68 |
+
| `prompts.py` system prompt strings | Static text constants β content changes should not break tests |
|
| 69 |
+
| `config.py` | Two string constants, no logic |
|
| 70 |
+
| `translations.py` | String constant tables, no logic |
|
| 71 |
+
| `tools.py` | Data definitions (dicts loaded from JSON), validated by Anthropic at runtime |
|
| 72 |
+
| `beacon_logging.py` | Logger wiring, pure side-effects |
|
| 73 |
+
| `clinical_trials_guru.py` | LangGraph graph construction β integration concern, not unit concern |
|
| 74 |
+
| `main.py` | CLI entry point wrapper |
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## Rule 3: Test Structure Mirrors Module Structure
|
| 79 |
+
|
| 80 |
+
```
|
| 81 |
+
tests/
|
| 82 |
+
βββ conftest.py # shared fixtures + Anthropic mock builders
|
| 83 |
+
βββ test_models.py
|
| 84 |
+
βββ test_prompts.py
|
| 85 |
+
βββ test_trials_api.py
|
| 86 |
+
βββ agents/
|
| 87 |
+
βββ __init__.py
|
| 88 |
+
βββ test_eligibility.py
|
| 89 |
+
βββ test_intake.py
|
| 90 |
+
βββ test_research.py
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
`conftest.py` is the foundation. It must define:
|
| 94 |
+
- A `PatientProfile` fixture covering the common ALS patient case
|
| 95 |
+
- Module-level factory functions (not fixtures) for constructing Anthropic mock objects: text blocks, tool-use blocks, complete message responses
|
| 96 |
+
- A `FakeStream` class that implements the `client.messages.stream()` context manager interface (has `text_stream` iterator and `get_final_message()`)
|
| 97 |
+
- A `mock_client` fixture returning a bare `MagicMock` Anthropic client
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## Rule 4: The LLM Loop Testing Pattern
|
| 102 |
+
|
| 103 |
+
The while-True tool-use loops in intake, research, and eligibility are the most structurally complex code in the project. The test pattern is:
|
| 104 |
+
|
| 105 |
+
1. Configure `mock_client.messages.create.side_effect` as an ordered list of mock responses
|
| 106 |
+
2. First response(s) return `stop_reason="tool_use"` with tool-use blocks
|
| 107 |
+
3. Final response returns `stop_reason="end_turn"` with a text block
|
| 108 |
+
4. Assert: the function returned the expected value AND `mock_client.messages.create.call_count` equals the expected number of loop iterations
|
| 109 |
+
|
| 110 |
+
This verifies both the happy path and that the loop terminates correctly.
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## Rule 5: Coverage Targets
|
| 115 |
+
|
| 116 |
+
| Layer | Target | Rationale |
|
| 117 |
+
|-------|--------|-----------|
|
| 118 |
+
| `models.py` | 95%+ | Pure functions, no excuse for gaps |
|
| 119 |
+
| `agents/eligibility.py` | 85%+ | Highest business-criticality |
|
| 120 |
+
| `agents/intake.py` | 75%+ | Date helpers are pure; streaming has irreducible mock complexity |
|
| 121 |
+
| `trials_api.py` | 75%+ | Flatten logic is pure; HTTP retries covered by httpx mock |
|
| 122 |
+
| `prompts.py` | 80%+ | Lookup function covered; prompt strings excluded |
|
| 123 |
+
| `agents/research.py` | 65%+ | Loop logic covered; streaming harder to fully exercise |
|
| 124 |
+
| **Overall** | **~75%** | Reasonable first-pass guardrail |
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## Implementation Stages
|
| 129 |
+
|
| 130 |
+
Stages are ordered by dependency and confidence-building. Each stage is independently verifiable.
|
| 131 |
+
|
| 132 |
+
### Stage 1 β Foundation
|
| 133 |
+
**Deliverable:** `tests/conftest.py` with all shared fixtures and mock builders.
|
| 134 |
+
**Why first:** Every other test file imports from here. Nothing else can be built without it.
|
| 135 |
+
|
| 136 |
+
### Stage 2 β Pure Functions (no mocks)
|
| 137 |
+
**Deliverable:** `tests/test_models.py`, `tests/test_prompts.py`
|
| 138 |
+
**Why second:** No mocking required. Establishes that pytest is wired correctly. Disease profile lookup (`lookup_disease_profile`) reads real data files and also validates that the `data/diseases/` JSON registry is structurally intact.
|
| 139 |
+
|
| 140 |
+
### Stage 3 β Deterministic Eligibility Logic
|
| 141 |
+
**Deliverable:** `tests/agents/test_eligibility.py` covering `_evaluate_deterministic`, `_resolve_patient_value`, and `_compute_overall`
|
| 142 |
+
**Why third:** Highest-value tests in the project. Fully deterministic. No mocks needed. Establishes the criterion/assessment fixture patterns used by later stages.
|
| 143 |
+
|
| 144 |
+
### Stage 4 β HTTP Layer
|
| 145 |
+
**Deliverable:** `tests/test_trials_api.py`
|
| 146 |
+
**Why fourth:** Introduces `pytest-httpx`. `_flatten_and_rank` is pure dict logic (no mock). `search_trials_api` uses `httpx_mock` to verify aggFilters param construction, pagination, and retry behavior.
|
| 147 |
+
|
| 148 |
+
### Stage 5 β LLM Orchestration (eligibility)
|
| 149 |
+
**Deliverable:** `tests/agents/test_eligibility.py` extended with `run_eligibility_check` and `bulk_parse_and_strip`
|
| 150 |
+
**Why fifth:** First introduction of LLM mocking. Eligibility is the clearest to mock because the tool choice is forced and the input/output shapes are structured.
|
| 151 |
+
|
| 152 |
+
### Stage 6 β LLM Orchestration (intake)
|
| 153 |
+
**Deliverable:** `tests/agents/test_intake.py`
|
| 154 |
+
**Why sixth:** Covers date helpers with `freezegun`, then introduces the `FakeStream` pattern for `stream_intake_turn`. Tests all three event types: token, text, profile.
|
| 155 |
+
|
| 156 |
+
### Stage 7 β LLM Orchestration (research)
|
| 157 |
+
**Deliverable:** `tests/agents/test_research.py`
|
| 158 |
+
**Why last:** Most complex mock setup (patches three collaborators: `search_trials_api`, `_flatten_and_rank`, `bulk_parse_and_strip`). Benefits from patterns established in Stages 5β6.
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Dependencies
|
| 163 |
+
|
| 164 |
+
```
|
| 165 |
+
pytest>=8.0
|
| 166 |
+
pytest-mock>=3.14
|
| 167 |
+
pytest-httpx>=0.35
|
| 168 |
+
freezegun>=1.5
|
| 169 |
+
pytest-cov
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
`pyproject.toml` should exclude the files listed in Rule 2 from coverage reporting.
|