Skip to content

test: improve coverage for discovery/service.py (61% → 90%+) #495

@tcoratger

Description

@tcoratger

Summary

src/lean_spec/subspecs/networking/discovery/service.py is at 61% coverage (86 uncovered statements, 9 uncovered branches). The existing tests cover construction and static methods but miss all the async networking logic — the core of the Discovery v5 service.

Coverage stats:

File Statements Missed Branches Br. Missed Coverage
discovery/service.py 258 86 92 9 61%

Existing test file: tests/lean_spec/subspecs/networking/discovery/test_service.py (49 tests passing)

What's already covered

  • Service construction and configuration
  • Some static/sync helper methods

What needs testing

1. find_node() — Kademlia iterative lookup (lines 204-239)

This is the core discovery algorithm and is completely untested.

  • Happy path: Lookup finds nodes at target distance, returns sorted LookupResult
  • No initial candidates: Returns empty result when routing table has no close nodes
  • Parallel querying: Verify LOOKUP_PARALLELISM candidates are queried concurrently
  • Iterative deepening: Newly discovered nodes feed into subsequent rounds
  • Result ordering: Final result sorted by XOR distance to target, capped at K_BUCKET_SIZE

2. _query_node() — FINDNODE query (lines 358-362)

  • Normal distance: Sends FINDNODE with computed log2 distance
  • Zero distance: Falls back to distances [1, 2, 3]
  • Return format: Returns (enr_list, node_id, distances) tuple

3. _ping_node() — Ping and bond (lines 367-370)

  • Successful pong: Adds bond to cache, returns True
  • No response: Returns False, no bond added

4. _handle_message() / _process_message() — Incoming message dispatch (lines 379, 389-397)

  • Message routing: Each discovery message type dispatched to correct handler
  • Unknown message: Handled gracefully

5. FINDNODE handler — Routing table ENR responses (lines 480-481)

  • Distance 0: Returns own ENR
  • Distance 1-256: Returns ENRs from corresponding routing table buckets
  • Response size limit: Capped at max_nodes_response

6. Background loops (lines 553-583)

  • _refresh_loop(): Performs random-target lookups periodically
  • _revalidation_loop(): Pings random node, removes on failure
  • _cleanup_loop(): Calls bond_cache.cleanup_expired()
  • Error resilience: All loops catch and log exceptions without crashing

7. _process_discovered_enr() — ENR processing and validation (lines 652-704)

This is security-critical and completely untested.

  • Valid ENR: Parsed, added to routing table and seen dict, address registered
  • Invalid ENR: Logged and skipped (missing required fields)
  • No node ID: Logged and skipped
  • Distance mismatch: Dropped (prevents routing table poisoning from malicious peers)
  • Own ENR: Skipped (don't add self to routing table)
  • Already seen: Skipped (dedup within a single lookup)
  • Parse failure: ValueError caught and logged
  • Unexpected error: Generic exception caught and logged

Why this matters

  • Core protocol logic: find_node() is the fundamental peer discovery algorithm — if it's broken, nodes can't find each other
  • Security properties: _process_discovered_enr() implements anti-poisoning checks (distance verification). Without tests, a regression could allow routing table attacks
  • Async correctness: The background loops and parallel queries involve subtle concurrency. Timing bugs are easy to introduce and hard to catch without tests
  • Network resilience: Error handling in all async paths must be tested to ensure the service doesn't crash on malformed responses

How to test

Running tests with coverage

uv run pytest tests/lean_spec/subspecs/networking/discovery/test_service.py -v \
  --cov=src/lean_spec/subspecs/networking/discovery/service --cov-report=term-missing

Target: ≥90% line and branch coverage (up from 61%).

Verifying with the coverage gate

uvx tox -e coverage-gate

Testing tips

  • Mock _transport: The transport layer (send_findnode, send_ping, send_response, get_node_address, register_enr, register_node_address) should be mocked to isolate service logic from UDP I/O
  • Mock _routing_table: Control what closest_nodes() and nodes_at_distance() return
  • Use pytest-asyncio for all async tests
  • Background loops: Use short intervals or asyncio.wait_for with timeout to test loop behavior without waiting for real timers
  • ENR fixtures: Create valid/invalid ENR objects for _process_discovered_enr() tests. Check existing discovery test fixtures for patterns
  • The existing test_service.py already has 49 tests — extend it rather than creating a new file

Using Claude Code subagents

If you're tackling this issue with Claude Code, use these agents for an efficient workflow:

1. code-tester agent — Generate the tests

Use this agent to scaffold the missing coverage. Prompt example:

Add tests to tests/lean_spec/subspecs/networking/discovery/test_service.py to cover the uncovered async methods. Focus on: find_node() iterative lookup, _query_node(), _ping_node(), _process_discovered_enr() validation logic (distance mismatch, invalid ENR, own ENR, dedup), and the three background loops (_refresh_loop, _revalidation_loop, _cleanup_loop). Mock the transport and routing table. Use pytest-asyncio.

2. doc-writer agent — Document new test groups

After tests are written, use this agent to add documentation explaining the testing strategy. Prompt example:

Add documentation to the new test classes explaining what security properties are being validated, especially for _process_discovered_enr() anti-poisoning checks and the Kademlia lookup algorithm.

Workflow

  1. Read existing test_service.py to understand current patterns and fixtures
  2. Run code-tester to generate tests for uncovered methods
  3. Run coverage, iterate until ≥90%
  4. Run doc-writer to document new test groups
  5. Run uvx tox -e all-checks to pass all quality checks

Metadata

Metadata

Assignees

Labels

good first issueGood for newcomerstestsScope: Changes to the spec tests

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions