qa/corpus: Corpus Protocol + SqliteShardCorpus + SidecarBucketCorpus

First step toward DRYing the local-vs-cloud retrieval diff. Defines a
minimal Corpus Protocol (Hit, ChunkRow, fts_body, fts_title, fts_phrase,
chunks_for_doc, snapshot_root) and ships two adapters:

  SqliteShardCorpus    wraps a connect_query() connection. Implements
                       fts_body (via _to_fts5 sanitizer + chunks_fts
                       MATCH) and chunks_for_doc + snapshot_root.
                       fts_title / fts_phrase raise NotSupportedError
                       (those routes will move out of query.py inline
                       SQL in the next refactor pass).

  SidecarBucketCorpus  wraps a MultiShardSidecarCorpus. fts_body
                       delegates to the sidecar BM25 + title-boost +
                       extras-penalty + title-relevance pipeline.
                       chunks_for_doc walks the shard's apsw conn via
                       the bucket VFS, reusing warm page cache.
                       Title / phrase routes raise NotSupportedError
                       until the sidecar format ships those indexes.

Together: query() future-refactor takes a Corpus parameter, runs the
routes the adapter supports, skips NotSupportedError, falls through.
Every quality fix (today: possessive stem, accent fold, numeral fold,
extras-penalty title-boost, title-relevance filter) lands once and
both backends consume it through the protocol.

Tests (20 passing, three layers):

  tests/test_qa_corpus.py             (unit) — Hit/ChunkRow invariants,
    Protocol conformance (runtime_checkable isinstance), NotSupportedError
    on unimplemented routes, fts_body sanitization (no FTS5 syntax leak),
    stopword-only query → [], chunks_for_doc idx-ascending shape.

  tests/test_qa_corpus_integration.py (integration) — real corpus →
    both adapters → assert recall (target doc in top-K of both) and
    byte-identical chunks_for_doc decoding. Catches tokenizer/stem/fold
    drift between FTS5's unicode61 and the sidecar's NFKD+ASCII path.

  tests/test_qa_corpus_functional.py  (functional) — full retrieval +
    StubClient LLM + verify_claim_lattice pipeline routed via the
    protocol for both adapters; asserts shared audit_mode + primary
    source agreement. Prototype of what the post-refactor query() does.

Side fix: MultiShardSidecarCorpus title-relevance filter now falls
open when the title tokenizes to zero content tokens (single-char or
all-stopword titles like "A" or "I" shouldn't be dropped just because
the filter side has nothing to match on).
This commit is contained in:
russell@unturf.com 2026-05-30 22:28:46 -04:00
parent ae842555d8
commit 1f8952ab19
No known key found for this signature in database
5 changed files with 1026 additions and 0 deletions

221
tests/test_qa_corpus.py Normal file
View file

@ -0,0 +1,221 @@
"""Unit tests for arborist.qa.corpus — adapter contract conformance.
What this asserts:
* Both adapters implement the Corpus Protocol (runtime_checkable)
* Hit + ChunkRow are frozen and hashable
* NotSupportedError raised by routes adapters don't implement
* SqliteShardCorpus.fts_body sanitizes natural-language input
(no FTS5 syntax leaks through punctuation)
* Empty / stopword-only queries return [] rather than raising
What this does NOT assert (covered in integration / functional):
* Cross-adapter result agreement
* Per-chunk content decompression equivalence
* End-to-end LLM + verifier behaviour
"""
from __future__ import annotations
import sqlite3
from typing import Iterator
import pytest
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.corpus import (
ChunkRow,
Corpus,
Hit,
NotSupportedError,
SidecarBucketCorpus,
SqliteShardCorpus,
)
from arborist.source import Source
from arborist.store import connect
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
class _FakeSource(Source):
source_type = "test"
def __init__(self, ds): self.ds = ds
def iter_documents(self) -> Iterator[Document]:
yield from self.ds
def _doc(uri: str, content: str, title: str | None = None) -> Document:
return Document(
uri=uri, content=content, source_type="test",
title=title or uri.split("/")[-1],
)
CORPUS_DOCS = [
_doc("test://anarchism", (
"Anarchism is a political philosophy that promotes a stateless society. "
* 6
+ "It seeks to abolish authority. " * 6
), title="Anarchism"),
_doc("test://capital", (
"The eight forms of capital include living, social, intellectual. " * 6
), title="Capital"),
]
@pytest.fixture
def shard_conn(tmp_path):
db = tmp_path / "corpus.db"
c = connect(db)
ingest_source(c, _FakeSource(CORPUS_DOCS))
yield c
c.close()
# ---------------------------------------------------------------------------
# Data-class invariants
# ---------------------------------------------------------------------------
def test_hit_is_frozen_and_equals_by_value():
h1 = Hit("root1", "uri1", "title1", 1.5)
h2 = Hit("root1", "uri1", "title1", 1.5)
with pytest.raises(Exception):
h1.score = 2.0 # type: ignore[misc]
assert h1 == h2 # value equality
# extras carries a dict so Hit isn't hashable by design — callers
# dedup by (document_root, ...) tuples, not by put-in-a-set.
def test_chunk_row_is_frozen():
c = ChunkRow("root", 0, "hex", "content")
with pytest.raises(Exception):
c.idx = 1 # type: ignore[misc]
def test_hit_extras_defaults_empty():
h = Hit("a", "b", "c", 0.0)
assert h.extras == {}
# ---------------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------------
def test_sqlite_shard_corpus_satisfies_protocol(shard_conn):
corpus = SqliteShardCorpus(shard_conn)
assert isinstance(corpus, Corpus)
assert corpus.name == "sqlite-shard"
assert corpus.higher_is_better is False
def test_sidecar_bucket_corpus_satisfies_protocol():
# Doesn't need a live bucket — runtime_checkable Protocol only
# checks method presence, not call signatures.
class _StubMulti:
manifest = type("M", (), {"shards": [], "snapshot_root": None})()
def fts_search(self, q, *, limit=8): return []
def conn_for_shard(self, url): raise KeyError(url)
corpus = SidecarBucketCorpus(_StubMulti())
assert isinstance(corpus, Corpus)
assert corpus.name == "sidecar-bucket"
assert corpus.higher_is_better is True
# ---------------------------------------------------------------------------
# fts_body sanitization (no FTS5-syntax leaks)
# ---------------------------------------------------------------------------
def test_sqlite_fts_body_handles_natural_language(shard_conn):
"""Punctuation like '?' and '-' would crash raw FTS5. The adapter
must sanitize via _to_fts5 (mirrors cloud sanitizer)."""
corpus = SqliteShardCorpus(shard_conn)
# Question mark + apostrophe must not crash.
hits = corpus.fts_body("what is anarchism?", limit=5)
assert any("Anarchism" in h.title for h in hits)
def test_sqlite_fts_body_stopword_only_query_returns_empty(shard_conn):
"""All-stopword queries reduce to empty FTS5 expressions; the
adapter must short-circuit to [] rather than passing '' to MATCH."""
corpus = SqliteShardCorpus(shard_conn)
hits = corpus.fts_body("the a an of with", limit=5)
assert hits == []
def test_sqlite_fts_body_returns_hit_shape(shard_conn):
corpus = SqliteShardCorpus(shard_conn)
hits = corpus.fts_body("anarchism", limit=2)
assert len(hits) >= 1
for h in hits:
assert isinstance(h, Hit)
assert h.document_root and h.document_uri and h.title
assert isinstance(h.score, float)
# ---------------------------------------------------------------------------
# Routes that should NotSupportedError
# ---------------------------------------------------------------------------
def test_sqlite_fts_title_not_supported(shard_conn):
corpus = SqliteShardCorpus(shard_conn)
with pytest.raises(NotSupportedError):
corpus.fts_title("anarchism")
def test_sqlite_fts_phrase_not_supported(shard_conn):
corpus = SqliteShardCorpus(shard_conn)
with pytest.raises(NotSupportedError):
corpus.fts_phrase(["anarchism is a"])
def test_sidecar_fts_title_not_supported():
class _StubMulti:
manifest = type("M", (), {"shards": [], "snapshot_root": None})()
def fts_search(self, q, *, limit=8): return []
def conn_for_shard(self, url): raise KeyError(url)
corpus = SidecarBucketCorpus(_StubMulti())
with pytest.raises(NotSupportedError):
corpus.fts_title("anarchism")
def test_sidecar_fts_phrase_not_supported():
class _StubMulti:
manifest = type("M", (), {"shards": [], "snapshot_root": None})()
def fts_search(self, q, *, limit=8): return []
def conn_for_shard(self, url): raise KeyError(url)
corpus = SidecarBucketCorpus(_StubMulti())
with pytest.raises(NotSupportedError):
corpus.fts_phrase(["a b c"])
# ---------------------------------------------------------------------------
# chunks_for_doc shape + ordering
# ---------------------------------------------------------------------------
def test_sqlite_chunks_for_doc_returns_idx_ascending(shard_conn):
corpus = SqliteShardCorpus(shard_conn)
hits = corpus.fts_body("anarchism", limit=1)
assert hits
chunks = corpus.chunks_for_doc(hits[0].document_root)
assert len(chunks) >= 1
indices = [c.idx for c in chunks]
assert indices == sorted(indices)
for c in chunks:
assert isinstance(c, ChunkRow)
assert c.content # decompressed, non-empty
assert c.leaf_hash # ingest sets this
def test_sqlite_snapshot_root_hex(shard_conn):
corpus = SqliteShardCorpus(shard_conn)
root = corpus.snapshot_root()
assert isinstance(root, str)
assert len(root) == 64
int(root, 16) # well-formed hex

View file

@ -0,0 +1,265 @@
"""Functional tests — full retrieval → context → LLM → verify, both adapters.
Builds the smallest plausible "ask-shaped" pipeline that the future
Corpus-aware query() refactor will collapse into:
hits = corpus.fts_body(question, limit=k)
chunks = []
for h in hits:
chunks.extend(corpus.chunks_for_doc(h.document_root, limit=1))
evidence_map = build_evidence_map([...chunks-as-evidence-dicts...])
prompt = system + EVIDENCE + question + reminder
answer = chat.chat_completion(...)
verdict = verify_claim_lattice(answer, evidence_map)
This module drives that pipeline through SqliteShardCorpus AND
SidecarBucketCorpus with a StubClient (deterministic answer) and
asserts both reach the same audit_mode + cite the same primary
source_root.
It's the SMALLEST canary for the "DRY the cloud + local diff" work:
when the refactor moves query() onto the protocol, this test gates
both adapters in one go.
"""
from __future__ import annotations
import os
import shutil
import sqlite3
import threading
from http.server import ThreadingHTTPServer
from arborist.wallet._range_http_server import RangeHandler as _RangeHandler
from pathlib import Path
from typing import Iterator
import pytest
apsw = pytest.importorskip("apsw")
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.client import StubClient
from arborist.qa.corpus import SidecarBucketCorpus, SqliteShardCorpus
from arborist.qa.evidence import (
build_evidence_map,
render_evidence_map,
)
from arborist.qa.prompts import (
CLAIM_LATTICE_GROUNDING_REMINDER,
CLAIM_LATTICE_SYSTEM_PROMPT,
)
from arborist.qa.verify import verify_claim_lattice
from arborist.source import Source
from arborist.store import connect
from arborist.wallet.bucket import BucketManifest, MultiShardSidecarCorpus
from arborist.wallet.sidecar import build_sidecar
# ---------------------------------------------------------------------------
# Fixture: same corpus exposed via both adapters.
# ---------------------------------------------------------------------------
CORPUS_DOCS = [
Document(
uri="test://anarchism", source_type="test", title="Anarchism",
content=(
"Anarchism is a political philosophy that promotes a stateless "
"society. " * 6
+ 'The phrase "abolition of authority" describes the core principle '
'of anarchism. ' * 4
),
),
Document(
uri="test://capital", source_type="test", title="Capital",
content=(
"The eight forms of capital include living, social, intellectual, "
"material, financial, experiential, cultural, and spiritual. " * 6
),
),
Document(
uri="test://noise-1", source_type="test", title="Random Noise",
content=("Lorem ipsum dolor sit amet. " * 12),
),
]
class _FakeSource(Source):
source_type = "test"
def __init__(self, ds): self.ds = ds
def iter_documents(self) -> Iterator[Document]:
yield from self.ds
@pytest.fixture
def both_adapters(tmp_path: Path):
"""Yield (sqlite_corpus, sidecar_corpus) over the same content."""
db = tmp_path / "shard.db"
c0 = connect(db)
ingest_source(c0, _FakeSource(CORPUS_DOCS))
c0.execute("PRAGMA wal_checkpoint(FULL)")
c0.close()
# bucket layout + sidecar
bucket = tmp_path / "bucket"
clones = bucket / "clones" / "snap-1"
clones.mkdir(parents=True)
bucket_shard = clones / "000.db"
shutil.copy(db, bucket_shard)
sidecar_path = clones / "000.sidecar.bin"
build_sidecar(bucket_shard, sidecar_path)
# in-process Range-aware server
cwd_before = os.getcwd()
os.chdir(bucket)
httpd = ThreadingHTTPServer(("127.0.0.1", 0), _RangeHandler)
port = httpd.server_address[1]
threading.Thread(target=httpd.serve_forever, daemon=True).start()
base_url = f"http://127.0.0.1:{port}"
manifest = BucketManifest(
shards=[{
"url": f"{base_url}/clones/snap-1/000.db",
"shard_idx": 0,
"label": "test-shard",
"sidecar_url": f"{base_url}/clones/snap-1/000.sidecar.bin",
}],
blob_base=f"{base_url}/blobs",
snapshot_root=None,
snapshot_ts=None,
)
multi = MultiShardSidecarCorpus(
manifest, sidecar_cache_dir=str(tmp_path / "cache"),
)
conn = connect(db)
try:
yield SqliteShardCorpus(conn), SidecarBucketCorpus(multi)
finally:
multi.close()
conn.close()
httpd.shutdown()
os.chdir(cwd_before)
# ---------------------------------------------------------------------------
# The shared end-to-end harness — the prototype of what query() will do
# once it takes a Corpus.
# ---------------------------------------------------------------------------
def _retrieve_and_verify(
corpus, question: str, *, stub_answer: str, top_k: int = 3,
):
"""Mini retrieval pipeline through the Corpus protocol. Returns a
dict with audit_mode + cited document_roots + verdict shape."""
hits = corpus.fts_body(question, limit=top_k)
if not hits:
return {"audit_mode": "UNGROUNDED", "n_quotes": 0, "n_verified": 0,
"sources": [], "cited_roots": set()}
chunks_payload: list[dict] = []
for rank, h in enumerate(hits, 1):
rows = corpus.chunks_for_doc(h.document_root, limit=1)
if not rows:
continue
r = rows[0]
chunks_payload.append({
"source_root": h.document_root,
"document_uri": h.document_uri,
"title": h.title,
"chunk_idx": r.idx,
"chunk_root": r.leaf_hash or "0" * 64,
"span": r.content[:1200],
"source_role": (
"primary_answer_source" if rank == 1 else "background_source"
),
})
evidence_map = build_evidence_map(chunks_payload)
evidence_text = render_evidence_map(evidence_map)
messages = [
{"role": "system", "content": CLAIM_LATTICE_SYSTEM_PROMPT},
{"role": "user", "content": (
f"EVIDENCE:\n\n{evidence_text}\n\n"
f"QUESTION: {question}\n\n"
f"{CLAIM_LATTICE_GROUNDING_REMINDER}"
)},
]
answer = StubClient(answer=stub_answer).chat_completion(
messages, model="stub", temperature=0.0,
)
verdict = verify_claim_lattice(
answer, evidence_map, question=question, warrant_check_enabled=False,
)
cited_roots = {h.document_root for h in hits}
return {
"audit_mode": verdict["audit_mode"],
"n_quotes": verdict["n_quotes"],
"n_verified": verdict["n_verified"],
"cited_roots": cited_roots,
"primary_root": hits[0].document_root if hits else "",
"primary_title": hits[0].title if hits else "",
}
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_pipeline_grounds_anarchism_through_both_adapters(both_adapters):
"""Verbatim-quote answer should land at the same audit_mode on both
adapters and primary-cite the Anarchism doc on both."""
sq, sc = both_adapters
question = "what is the core principle of anarchism?"
# Verbatim from CORPUS_DOCS[0]: 'abolition of authority'
stub = (
'The core principle of anarchism is the "abolition of authority". [E1]'
)
sq_result = _retrieve_and_verify(sq, question, stub_answer=stub)
sc_result = _retrieve_and_verify(sc, question, stub_answer=stub)
assert sq_result["audit_mode"] == sc_result["audit_mode"], (
f"audit_mode differs: sqlite={sq_result['audit_mode']} "
f"sidecar={sc_result['audit_mode']}"
)
# Both must surface the Anarchism doc as primary.
assert "Anarchism" in sq_result["primary_title"]
assert "Anarchism" in sc_result["primary_title"]
def test_pipeline_handles_noise_question_consistently(both_adapters):
"""An off-topic question (no matching doc) must produce UNGROUNDED
/ empty-evidence on both adapters."""
sq, sc = both_adapters
question = "what is the population of mars?"
stub = "Mars has no population because it is uninhabited. [E1]"
sq_result = _retrieve_and_verify(sq, question, stub_answer=stub)
sc_result = _retrieve_and_verify(sc, question, stub_answer=stub)
# Off-topic should land at UNGROUNDED or HYBRID on BOTH paths —
# neither corpus has Mars content. STRICT would be a false positive.
for r in (sq_result, sc_result):
assert r["audit_mode"] in ("UNGROUNDED", "HYBRID"), (
f"unexpected STRICT on off-topic: {r}"
)
def test_pipeline_chunks_for_primary_are_same_bytes(both_adapters):
"""Both adapters must surface byte-identical chunk content for the
same document_root — wallet-side Merkle verification depends on it."""
sq, sc = both_adapters
question = "anarchism"
sq_hits = sq.fts_body(question, limit=1)
assert sq_hits
droot = sq_hits[0].document_root
sq_chunks = sq.chunks_for_doc(droot)
sc_chunks = sc.chunks_for_doc(droot)
assert sq_chunks and sc_chunks
for a, b in zip(sq_chunks, sc_chunks):
assert a.content == b.content, (
f"chunk[{a.idx}] content differs by adapter"
)
assert a.leaf_hash == b.leaf_hash

View file

@ -0,0 +1,194 @@
"""Integration tests for Corpus adapters — real corpus → both backends.
Builds a small SQLite shard, runs the same fixtures through:
- SqliteShardCorpus directly
- SidecarBucketCorpus over a sidecar built from the same shard,
served via an in-process HTTP-Range server, opened via apsw VFS
Asserts the two adapters agree on the WINNING document_root for each
fixture query. Score scales differ (FTS5 BM25 is negative, sidecar
BM25 + boost is positive), so we compare TOP-K SETS, not raw scores —
the contract is "both backends point at the same primary source for
the same query."
What this catches that unit tests don't:
* Token/stem/fold mismatches between FTS5's unicode61 tokenizer and
the sidecar's NFKD-fold + ASCII-word path
* Sidecar build-time term loss vs FTS5's full-text body coverage
* apsw HttpRangeVFS read-path correctness on real SQLite files
Uses MockBucketBackend-style in-process serving (same trick as
tests/test_wallet_bucket.py) so the suite stays offline.
"""
from __future__ import annotations
import os
import sqlite3
import threading
from http.server import ThreadingHTTPServer
from pathlib import Path
from typing import Iterator
import pytest
apsw = pytest.importorskip("apsw")
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.corpus import (
SidecarBucketCorpus,
SqliteShardCorpus,
)
from arborist.source import Source
from arborist.store import connect
from arborist.wallet.bucket import (
BucketManifest,
MultiShardSidecarCorpus,
)
from arborist.wallet.sidecar import build_sidecar
# ---------------------------------------------------------------------------
# Range-aware HTTP server (mirrors tests/test_wallet_bucket.py)
# ---------------------------------------------------------------------------
from arborist.wallet._range_http_server import RangeHandler as _RangeHandler
@pytest.fixture
def served_corpus(tmp_path: Path):
"""Build a 4-doc corpus, sidecar it, expose via HTTP. Yields:
(sqlite_conn, multi_shard_corpus, base_url)
"""
class _S(Source):
source_type = "test"
def __init__(self, ds): self.ds = ds
def iter_documents(self) -> Iterator[Document]:
yield from self.ds
db = tmp_path / "shard.db"
conn0 = connect(db)
ingest_source(conn0, _S([
Document(uri=f"test://doc-{i}", source_type="test",
title=f"Doc {i}",
content=(
f"Topic alpha-{i} is described in many sentences here. " * 6
+ f"The unique phrase for entry {i} is 'lemma-{i}'. " * 6
))
for i in range(4)
]))
# Checkpoint WAL + close so the on-disk file is self-contained
# before we copy it into the bucket layout.
conn0.execute("PRAGMA wal_checkpoint(FULL)")
conn0.close()
# Build sidecar from a copy of the shard placed in the bucket layout.
bucket = tmp_path / "bucket"
clones = bucket / "clones" / "snap-1"
clones.mkdir(parents=True)
import shutil
bucket_shard = clones / "000.db"
shutil.copy(db, bucket_shard)
sidecar_path = clones / "000.sidecar.bin"
build_sidecar(bucket_shard, sidecar_path)
# Re-open the original for SqliteShardCorpus tests.
conn = connect(db)
# Serve bucket dir.
cwd_before = os.getcwd()
os.chdir(bucket)
httpd = ThreadingHTTPServer(("127.0.0.1", 0), _RangeHandler)
port = httpd.server_address[1]
t = threading.Thread(target=httpd.serve_forever, daemon=True)
t.start()
base_url = f"http://127.0.0.1:{port}"
manifest = BucketManifest(
shards=[{
"url": f"{base_url}/clones/snap-1/000.db",
"shard_idx": 0,
"label": "test-shard",
"sidecar_url": f"{base_url}/clones/snap-1/000.sidecar.bin",
}],
blob_base=f"{base_url}/blobs",
snapshot_root=None,
snapshot_ts=None,
)
multi = MultiShardSidecarCorpus(manifest, sidecar_cache_dir=str(tmp_path / "cache"))
try:
yield conn, multi, base_url
finally:
multi.close()
conn.close()
httpd.shutdown()
os.chdir(cwd_before)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def _top_doc_root(hits) -> str:
return hits[0].document_root if hits else ""
def test_adapters_both_surface_target_doc_in_top_k(served_corpus):
"""A token unique to one doc must appear in BOTH adapters' top-K.
Note: which doc ranks #1 can differ — sidecar applies a title-boost
that SqliteShardCorpus.fts_body doesn't (its title-LIKE route lands
in the next protocol refactor). The contract here is recall, not
rank: both backends must SEE the right doc in the top results.
"""
conn, multi, _ = served_corpus
sqlite_corpus = SqliteShardCorpus(conn)
sidecar_corpus = SidecarBucketCorpus(multi)
# 'lemma 3' has 'lemma' in every doc but '3' is rarer (Doc 3 title +
# body 'lemma-3'); a sane retrieval surfaces Doc 3 in the top-K of
# both backends.
sq_hits = sqlite_corpus.fts_body("lemma 3", limit=4)
sc_hits = sidecar_corpus.fts_body("lemma 3", limit=4)
assert sq_hits, "sqlite adapter returned no hits"
assert sc_hits, "sidecar adapter returned no hits"
sq_titles = {h.title for h in sq_hits}
sc_titles = {h.title for h in sc_hits}
assert "Doc 3" in sq_titles, f"sqlite top-K missing Doc 3: {sq_titles}"
assert "Doc 3" in sc_titles, f"sidecar top-K missing Doc 3: {sc_titles}"
def test_adapters_chunks_for_doc_decode_equivalently(served_corpus):
"""Same document_root → both adapters return decoded chunk text that
matches byte-for-byte (modulo any unpack_chunk normalization)."""
conn, multi, _ = served_corpus
sq = SqliteShardCorpus(conn)
sc = SidecarBucketCorpus(multi)
sq_hits = sq.fts_body("lemma 3", limit=1)
assert sq_hits
droot = sq_hits[0].document_root
sq_chunks = sq.chunks_for_doc(droot)
sc_chunks = sc.chunks_for_doc(droot)
assert len(sq_chunks) == len(sc_chunks), (
f"chunk count mismatch: sqlite={len(sq_chunks)} sidecar={len(sc_chunks)}"
)
for a, b in zip(sq_chunks, sc_chunks):
assert a.idx == b.idx
assert a.leaf_hash == b.leaf_hash
assert a.content == b.content
def test_sqlite_snapshot_root_matches_compute_snapshot_root(served_corpus):
"""Adapter-reported snapshot_root must match compute_snapshot_root()
on the same connection — the wallet relies on this for binding."""
from arborist.snapshot import compute_snapshot_root
conn, _, _ = served_corpus
sq = SqliteShardCorpus(conn)
expected, _ = compute_snapshot_root(conn)
assert sq.snapshot_root() == expected