Black-box test harness for hotblocks + first conformance tests#84
Merged
Conversation
59dd0af to
6239612
Compare
c4adc4a to
08c17c3
Compare
The implementation-free behavioral specification (15 documents: state model, write/read paths, invariant catalog INV-*, liveness LIV-*, failure model FM-*, retention/space RS-*, performance SLI-*/HZ-*, observability OB-*, conformance plan CT-* with the reference-model oracle, HTTP binding IB-*, parameter registry P-*), plus the full spec-vs-implementation audit (audit-2026-07-12.md) that re-verified every gap-register entry against the code. Audit outcomes already folded into the normative text: - RP-5b: an anchored query at from == head+1 (fin+1 for finalized-only) with a mismatched parent conflicts immediately, before the head-wait -- the implementation was right, the reference model was wrong - RP-9: the coverage carrier is emission itself -- the coverage-end block is always emitted (header-only when unmatched); extra boundary markers are layout-dependent and exempt from INV-22 determinism - WP 2.5: a downward retention bound is an explicit, alarmed RESET (matching the implementation's intent); refused at boot for Pinned - WP-6: rollback may be batch-granular (chunk-boundary matching) - Gap register rewritten: GAP-6 demoted P0->P2 (runtime reclaim landed in PR #79), GAP-22 upgraded P2->P1 (a deep fork can silently replace the finalized prefix), GAP-2/4/8/9/11/20/21 corrected, GAP-23..37 added - Harness README: first four CT-4 scripts planned; the header-only-record open question resolved (it is the RP-9 carrier) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Codex <codex@openai.com>
Add a reusable source simulator, reference model, client driver, and black-box SUT lifecycle for hotblocks conformance testing. Cover happy-path ingestion across EVM, Solana, and Hyperliquid fills, plus unterminated JSONL recovery. Fix the JSONL final-line scan position, validate parent hashes across sparse numbering holes, and raise RLIMIT_NOFILE for the spawned test service so wide EVM schemas run under the default macOS descriptor limit. Co-Authored-By: Codex <codex@openai.com>
08c17c3 to
bd08622
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The service layer has no automated tests. Storage and query are covered; ingestion, the dataset controller, the API and liveness are not — which is why the last few production incidents were found in production. This adds the harness that makes them testable, plus the first two tests, and fixes the bug the harness found on its first run.
The bug it found (first commit)
LineStream::take_final_lineempties its buffer but leftunchecked_pospointing past the end of it. A source response whose JSONL body does not end with a newline therefore yields its last record and then panics on the next poll, out of bounds.The panic kills the ingest task: the buffered batch is lost, the dataset parks for a minute, and the source serves the same body on retry — so it crash-loops and that dataset never ingests again. Silent, apart from
ingest task panickedin the log.One line to fix; pinned by unit tests in
data-clientand end to end byct9_source_faults.The harness (
crates/hotblocks-harness)The system under test is the real binary, spawned as a child process and driven over HTTP. Nothing links against its internals: an assertion that cannot be made through the HTTP surface is an assertion a client could not rely on either — and owning the process is what will make the crash/restart and shutdown classes expressible later.
simmodeldrivercomparesutchainhyperliquid-fills— the cheapest kind to synthesize)A test reads like this:
A failing test prints the service's own log tail (with the HTTP access log filtered out — it is never the interesting part).
The tests
ct1_happy_path(~10 s) — cold ingest of a 50-block window with finality trailing the tip, then an anchored client tailing 20 more. Checks the structural chain, watermark bounds, provenance (what the source served is what is read back, payload included), append and finality monotonicity, response shape and completeness, anchored ancestry across responses, progress, range honesty, the bounded long-poll wait, and the per-dataset gauges.ct9_source_faults— a source whose last record is unterminated must not stall ingestion. Fails without the first commit.Notes for review
crates/hotblocks/tests/because only a test inside that package getsenv!("CARGO_BIN_EXE_sqd-hotblocks"). Everything reusable is in the harness crate, so a soak or benchmark runner can use it outsidecargo test.includeAllBlocksbecause coverage is not otherwise recoverable by a client from a filter-sparse response.INV-*/RP-*/CT-*identifiers cited on the assertions are its vocabulary. Those documents land separately.🤖 Generated with Claude Code