Skip to content

test: improve coverage for Node orchestrator lifecycle (81%) #525

@tcoratger

Description

@tcoratger

Summary

src/lean_spec/subspecs/node/node.py has 81% test coverage. While test_node.py exists, database loading, signal handling, periodic logging, and the validator service wiring are not covered.

Coverage stats:

File Statements Covered Coverage
node/node.py 203 35 uncovered 81%

Uncovered lines: 207→247, 307–309, 312–313 (validator publish wrappers), 387, 428, 432, 445, 447 (run() with API/validator services), 463–469 (signal handlers), 480→exit, 484–511 (periodic logging), 528, 530 (_wait_shutdown with optional services)

What needs testing

Node.from_genesis() — Database path

Database loading (_try_load_from_database)

  • No database: Returns None
  • Empty database: No head root, returns None
  • Missing block/state: Head root exists but block or state missing, returns None
  • Missing checkpoints: Justified or finalized checkpoint missing, returns None
  • Successful load: All data present, returns reconstructed Store
  • Wall-clock time: Store time derived from wall clock, not just block slot
  • Genesis time fallback: Falls back to database-stored genesis_time when param is None

Validator wiring

  • With registry: ValidatorService created, publish wrappers wire block and attestation publishing
  • Without registry: ValidatorService is None, node runs in passive mode
  • Publish wrapper: Block wrapper calls both network_service.publish_block and sync_service.on_gossip_block
  • Attestation wrapper: Calls network_service.publish_attestation with computed subnet_id and sync_service.on_gossip_attestation

Node.run() — Service lifecycle

TaskGroup orchestration

  • All services start: Chain, network, and shutdown tasks created
  • API server: Task created only if api_server is not None
  • Validator service: Task created only if validator_service is not None
  • Database cleanup: Database closed in finally block

Signal handling (_install_signal_handlers)

  • Main thread: SIGINT and SIGTERM handlers installed
  • Non-main thread: ValueError/RuntimeError silently ignored

_log_justified_finalized_periodically()

  • Periodic logging: Logs justified/finalized slots at configured interval
  • Metrics update: Updates Prometheus gauges (slot, peers, head, justified, finalized)
  • Shutdown check: Exits loop when shutdown event is set
  • Validator count: Reports validator count (0 if no validator service)

_wait_shutdown()

  • Stop signal: Stops chain, network, and optionally API + validator services
  • Optional services: Handles None api_server and validator_service gracefully

stop() / is_running

  • Lifecycle: stop() sets shutdown event, is_running reflects state

Why this matters

  • Node reliability: The orchestrator wires everything together. Incorrect wiring causes silent failures
  • Database resume: Loading from database enables fast restarts. Bugs here mean nodes restart from genesis
  • Graceful shutdown: Signal handling prevents data corruption on Ctrl+C
  • Monitoring: Periodic logging and metrics are essential for production observability

How to test

Running tests with coverage

uv run pytest tests/lean_spec/subspecs/node/test_node.py -v \
  --cov=src/lean_spec/subspecs/node/node --cov-report=term-missing

Target: ≥90% line coverage.

Test file location

tests/lean_spec/subspecs/node/test_node.py  (extend existing)

Testing tips

  • For _try_load_from_database, mock Database interface methods to return None or valid data
  • Use install_signal_handlers=False in tests to avoid signal handler issues
  • For run(), use a short-lived EventSource and call node.stop() to terminate
  • Mock SlotClock and time_fn for deterministic time testing
  • Periodic logging can be tested by setting shutdown event after a short delay

Using Claude Code subagents

1. code-tester agent — Generate the tests

Extend tests in tests/lean_spec/subspecs/node/test_node.py. Cover _try_load_from_database with all failure paths (no db, empty db, missing block/state/checkpoints) and successful load, validator service wiring with publish wrappers, run() with optional API/validator services, _install_signal_handlers in non-main thread, and stop()/is_running lifecycle.

Workflow

  1. Start with _try_load_from_database tests (static method, easy to mock)
  2. Add validator wiring tests
  3. Add lifecycle tests (run, stop, signal handlers)
  4. 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