Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.26.0
Beacon Unit Test Design
Why This Exists
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.
Rule 1: Classify Every Code Path Before Testing It
Every function in Beacon falls into one of three categories:
Deterministic (test directly, no mocks)
These functions take inputs and return outputs with no external dependencies. They are always fast, always repeatable, and should have high coverage.
| Function | Location | What makes it deterministic |
|---|---|---|
haversine_miles |
models.py |
Pure trigonometry |
PatientProfile.summary |
models.py |
String formatting from a dataclass |
_evaluate_deterministic |
agents/eligibility.py |
Operator comparison (<=, >=, ==, !=, in, not_in, between) |
_resolve_patient_value |
agents/eligibility.py |
Dict/field lookup via static key map |
_compute_overall |
agents/eligibility.py |
Precedence rule: FAIL > UNKNOWN > PASS |
_flatten_and_rank |
trials_api.py |
API response reshaping + haversine sort |
_months_from_date |
agents/intake.py |
Date arithmetic (freeze datetime.today) |
_resolve_months |
agents/intake.py |
Dict key fallback logic |
lookup_disease_profile |
prompts.py |
File read + string matching (uses real data files on disk) |
Rule:
_evaluate_deterministicis 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.
I/O-Bound (mock the network, test the logic around it)
These functions call external services. The external call is mocked; the logic that wraps it (retry, pagination, parameter construction) is what we test.
| Function | External dependency | Mock tool |
|---|---|---|
geocode_zip |
OpenStreetMap Nominatim (httpx) | pytest-httpx |
search_trials_api |
ClinicalTrials.gov API (httpx) | pytest-httpx |
Rule: Use
pytest-httpx(notresponses, nothttpretty). The codebase useshttpxdirectly. Other HTTP mock libraries intercept at a different layer and will silently miss these calls.
LLM-Orchestrated (mock the client, test the loop logic)
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?
| Function | Location | What the test verifies |
|---|---|---|
run_intake_agent |
agents/intake.py |
identify_disease and submit_profile tool routing |
stream_intake_turn |
agents/intake.py |
token/text/profile/reset_stream event sequence |
run_research_agent |
agents/research.py |
search tool loop: 1 call, 2 calls, API error path |
stream_research_agent |
agents/research.py |
status/token/done event sequence |
run_eligibility_check |
agents/eligibility.py |
parse β deterministic split β LLM assess orchestration |
bulk_parse_and_strip |
agents/eligibility.py |
bulk parse + deterministic filter, strip fields |
_parse_criteria |
agents/eligibility.py |
tool-forced call, struct β dataclass conversion |
_assess_llm |
agents/eligibility.py |
tool-forced call, output β CriterionAssessment mapping |
Rule: Never call the live Anthropic API from tests. Mock
client.messages.createusingside_effectlists to simulate multi-turn tool loops. For streaming, mockclient.messages.streamwith a context-manager-compatible fake that yields tokens and returns a final message.
Rule 2: What Not to Test
Some parts of the codebase have no unit-testable logic and should be explicitly excluded from coverage requirements.
| File | Reason to skip |
|---|---|
app.py |
Gradio requires a running server; UI event wiring is not unit-testable |
prompts.py system prompt strings |
Static text constants β content changes should not break tests |
config.py |
Two string constants, no logic |
translations.py |
String constant tables, no logic |
tools.py |
Data definitions (dicts loaded from JSON), validated by Anthropic at runtime |
beacon_logging.py |
Logger wiring, pure side-effects |
clinical_trials_guru.py |
LangGraph graph construction β integration concern, not unit concern |
main.py |
CLI entry point wrapper |
Rule 3: Test Structure Mirrors Module Structure
tests/
βββ conftest.py # shared fixtures + Anthropic mock builders
βββ test_models.py
βββ test_prompts.py
βββ test_trials_api.py
βββ agents/
βββ __init__.py
βββ test_eligibility.py
βββ test_intake.py
βββ test_research.py
conftest.py is the foundation. It must define:
- A
PatientProfilefixture covering the common ALS patient case - Module-level factory functions (not fixtures) for constructing Anthropic mock objects: text blocks, tool-use blocks, complete message responses
- A
FakeStreamclass that implements theclient.messages.stream()context manager interface (hastext_streamiterator andget_final_message()) - A
mock_clientfixture returning a bareMagicMockAnthropic client
Rule 4: The LLM Loop Testing Pattern
The while-True tool-use loops in intake, research, and eligibility are the most structurally complex code in the project. The test pattern is:
- Configure
mock_client.messages.create.side_effectas an ordered list of mock responses - First response(s) return
stop_reason="tool_use"with tool-use blocks - Final response returns
stop_reason="end_turn"with a text block - Assert: the function returned the expected value AND
mock_client.messages.create.call_countequals the expected number of loop iterations
This verifies both the happy path and that the loop terminates correctly.
Rule 5: Coverage Targets
| Layer | Target | Rationale |
|---|---|---|
models.py |
95%+ | Pure functions, no excuse for gaps |
agents/eligibility.py |
85%+ | Highest business-criticality |
agents/intake.py |
75%+ | Date helpers are pure; streaming has irreducible mock complexity |
trials_api.py |
75%+ | Flatten logic is pure; HTTP retries covered by httpx mock |
prompts.py |
80%+ | Lookup function covered; prompt strings excluded |
agents/research.py |
65%+ | Loop logic covered; streaming harder to fully exercise |
| Overall | ~75% | Reasonable first-pass guardrail |
Implementation Stages
Stages are ordered by dependency and confidence-building. Each stage is independently verifiable.
Stage 1 β Foundation
Deliverable: tests/conftest.py with all shared fixtures and mock builders.
Why first: Every other test file imports from here. Nothing else can be built without it.
Stage 2 β Pure Functions (no mocks)
Deliverable: tests/test_models.py, tests/test_prompts.py
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.
Stage 3 β Deterministic Eligibility Logic
Deliverable: tests/agents/test_eligibility.py covering _evaluate_deterministic, _resolve_patient_value, and _compute_overall
Why third: Highest-value tests in the project. Fully deterministic. No mocks needed. Establishes the criterion/assessment fixture patterns used by later stages.
Stage 4 β HTTP Layer
Deliverable: tests/test_trials_api.py
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.
Stage 5 β LLM Orchestration (eligibility)
Deliverable: tests/agents/test_eligibility.py extended with run_eligibility_check and bulk_parse_and_strip
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.
Stage 6 β LLM Orchestration (intake)
Deliverable: tests/agents/test_intake.py
Why sixth: Covers date helpers with freezegun, then introduces the FakeStream pattern for stream_intake_turn. Tests all three event types: token, text, profile.
Stage 7 β LLM Orchestration (research)
Deliverable: tests/agents/test_research.py
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.
Dependencies
pytest>=8.0
pytest-mock>=3.14
pytest-httpx>=0.35
freezegun>=1.5
pytest-cov
pyproject.toml should exclude the files listed in Rule 2 from coverage reporting.