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
- Start with
_try_load_from_database tests (static method, easy to mock)
- Add validator wiring tests
- Add lifecycle tests (run, stop, signal handlers)
- Run
uvx tox -e all-checks to pass all quality checks
Summary
src/lean_spec/subspecs/node/node.pyhas 81% test coverage. Whiletest_node.pyexists, database loading, signal handling, periodic logging, and the validator service wiring are not covered.Coverage stats:
node/node.pyUncovered 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 pathDatabase loading (
_try_load_from_database)Validator wiring
network_service.publish_blockandsync_service.on_gossip_blocknetwork_service.publish_attestationwith computed subnet_id andsync_service.on_gossip_attestationNode.run()— Service lifecycleTaskGroup orchestration
api_serveris not Nonevalidator_serviceis not NoneSignal handling (
_install_signal_handlers)_log_justified_finalized_periodically()_wait_shutdown()stop()/is_runningstop()sets shutdown event,is_runningreflects stateWhy this matters
How to test
Running tests with coverage
Target: ≥90% line coverage.
Test file location
Testing tips
_try_load_from_database, mockDatabaseinterface methods to return None or valid datainstall_signal_handlers=Falsein tests to avoid signal handler issuesrun(), use a short-livedEventSourceand callnode.stop()to terminateSlotClockandtime_fnfor deterministic time testingUsing Claude Code subagents
1.
code-testeragent — Generate the testsWorkflow
_try_load_from_databasetests (static method, easy to mock)uvx tox -e all-checksto pass all quality checks