qa/providence_query: cache persist on miss + audit chain append
Phase 2 step 2 of #000072. The skeleton landed in 20faae0 looked up
providence_cache + returned on hit, but the miss branch just
returned the fresh run_query result without writing anything back —
so every call paid the LLM cost. Now misses persist:
1. Build run_dag via arborist.qa.dag.build_run_dag (claim-lattice
mode, 9-stage variant — same shape legacy query() emits)
2. Append a providence_write event via arborist.store.append_audit
INSIDE a BEGIN IMMEDIATE transaction
3. INSERT INTO providence_cache with all 25 columns the schema
requires (cache_key, source_root, document_uri, question_*,
answer_text, merkle_proof placeholder, 4-dim policy hashes,
3-dim schema versions, audit_event_hash linking to the just-
appended event, run_dag_root + run_dag_blob, audit_mode, etc.)
4. COMMIT — atomic; audit chain + providence_cache stay
consistent on crash mid-way
Result dict on miss now carries audit_event_hash + run_dag_root so
the caller can reference the audit chain or replay the DAG.
status="fresh_persisted" (was "fresh" in the skeleton) names the
new behavior.
NOT done yet (deferred to subsequent steps):
- merkle_proof is "[]" placeholder. Schema requires NOT NULL.
Real per-chunk proofs are a follow-up; the cache row is
consistent without them but downstream wallet verification has
nothing to walk.
- burn_existing doesn't emit a providence_burn audit event
(legacy query() does — query.py:3084 region). Audit chain
still grows monotonically on the persist side, just doesn't
record what was burned.
- equivalence_class fallback lookup (legacy tries both dedup-mode
keys when fidelity allows; primary only here)
Tests (tests/test_providence_query.py, 4):
- first call persists row + audit event
- second call returns cache_hit ignoring different stub
- burn_existing forces re-run + re-persist; audit chain grows
- chain links correctly across two distinct cache_keys
283 tests pass in the broader query/corpus/sidecar/wallet/bucket/
claim_lattice/byte_identity/providence gate.
This commit is contained in:
parent
b1c8fb7eba
commit
17b9622a08
2 changed files with 319 additions and 20 deletions
159
tests/test_providence_query.py
Normal file
159
tests/test_providence_query.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""End-to-end tests for arborist.qa.providence_query.
|
||||
|
||||
Covers Phase 2 step 1 + step 2 of #000072:
|
||||
- Step 1: cache hit / miss / burn shape (skeleton landed 20faae0)
|
||||
- Step 2: persist on miss — providence_cache row + audit chain
|
||||
event written atomically inside one transaction
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
import pytest
|
||||
|
||||
from arborist.document import Document
|
||||
from arborist.ingest import ingest_source
|
||||
from arborist.qa.client import StubClient
|
||||
from arborist.qa.corpus import SqliteShardCorpus
|
||||
from arborist.qa.providence_query import providence_query
|
||||
from arborist.source import Source
|
||||
from arborist.store import connect
|
||||
|
||||
|
||||
class _S(Source):
|
||||
source_type = "test"
|
||||
def __init__(self, ds): self.ds = ds
|
||||
def iter_documents(self) -> Iterator[Document]:
|
||||
yield from self.ds
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup(tmp_path):
|
||||
"""Build a one-doc corpus + an empty qa.db; return (corpus, qa_path)."""
|
||||
shard = tmp_path / "shard.db"
|
||||
qa = tmp_path / "qa.db"
|
||||
c = connect(shard)
|
||||
ingest_source(c, _S([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. ' * 4
|
||||
),
|
||||
)]))
|
||||
c.execute("PRAGMA wal_checkpoint(FULL)")
|
||||
c.close()
|
||||
connect(qa).close() # init schema
|
||||
corpus = SqliteShardCorpus(connect(shard))
|
||||
return corpus, qa
|
||||
|
||||
|
||||
def test_first_call_persists_with_audit_event(setup):
|
||||
corpus, qa = setup
|
||||
stub = 'The core principle of anarchism is the "abolition of authority". [E1]'
|
||||
r = providence_query(
|
||||
corpus, "what is the core principle of anarchism?",
|
||||
StubClient(answer=stub),
|
||||
qa_db=qa, model_id="stub", top_k=2,
|
||||
)
|
||||
assert r["status"] == "fresh_persisted"
|
||||
assert r["audit_mode"] == "STRICT"
|
||||
assert r["cache_key"]
|
||||
assert r["audit_event_hash"]
|
||||
assert r["run_dag_root"]
|
||||
qaro = sqlite3.connect(f"file:{qa}?mode=ro", uri=True)
|
||||
n_cache = qaro.execute(
|
||||
"SELECT COUNT(*) FROM providence_cache WHERE cache_key = ?",
|
||||
(r["cache_key"],),
|
||||
).fetchone()[0]
|
||||
assert n_cache == 1
|
||||
n_audit = qaro.execute(
|
||||
"SELECT COUNT(*) FROM audit_events WHERE event_type='providence_write'"
|
||||
).fetchone()[0]
|
||||
assert n_audit == 1
|
||||
qaro.close()
|
||||
|
||||
|
||||
def test_second_call_returns_cache_hit(setup):
|
||||
"""Second call with the SAME question + model + corpus must hit
|
||||
the cache and ignore the (different) stub answer."""
|
||||
corpus, qa = setup
|
||||
q = "what is the core principle of anarchism?"
|
||||
stub1 = 'The core principle of anarchism is the "abolition of authority". [E1]'
|
||||
r1 = providence_query(
|
||||
corpus, q, StubClient(answer=stub1),
|
||||
qa_db=qa, model_id="stub", top_k=2,
|
||||
)
|
||||
r2 = providence_query(
|
||||
corpus, q, StubClient(answer="WRONG ANSWER [E1]"),
|
||||
qa_db=qa, model_id="stub", top_k=2,
|
||||
)
|
||||
assert r2["status"] == "cache_hit"
|
||||
assert r2["audit_mode"] == r1["audit_mode"]
|
||||
assert r2["answer_text"] == r1["answer_text"] # NOT the wrong stub
|
||||
assert r2["cache_key"] == r1["cache_key"]
|
||||
|
||||
|
||||
def test_burn_existing_forces_re_run_and_re_persists(setup):
|
||||
"""burn_existing deletes the cached row, re-runs the LLM, and
|
||||
persists a NEW row (which by chance may have the same cache_key
|
||||
if inputs are identical — that's expected). audit chain grows by
|
||||
one event per write."""
|
||||
corpus, qa = setup
|
||||
q = "what is the core principle of anarchism?"
|
||||
stub = 'The core principle of anarchism is the "abolition of authority". [E1]'
|
||||
providence_query(
|
||||
corpus, q, StubClient(answer=stub),
|
||||
qa_db=qa, model_id="stub", top_k=2,
|
||||
)
|
||||
r2 = providence_query(
|
||||
corpus, q, StubClient(answer=stub),
|
||||
qa_db=qa, model_id="stub", top_k=2, burn_existing=True,
|
||||
)
|
||||
assert r2["status"] == "burned"
|
||||
assert r2["burned_existing"] == 1
|
||||
# After burn + re-persist, exactly one live row + 2 audit events
|
||||
# (write + write — the burn itself doesn't currently emit a
|
||||
# 'providence_burn' event in providence_query; legacy query()
|
||||
# does, but that's deferred to a later sub-step).
|
||||
qaro = sqlite3.connect(f"file:{qa}?mode=ro", uri=True)
|
||||
n_live = qaro.execute(
|
||||
"SELECT COUNT(*) FROM providence_cache "
|
||||
"WHERE falsification_state = 'live'"
|
||||
).fetchone()[0]
|
||||
assert n_live == 1
|
||||
n_audit = qaro.execute(
|
||||
"SELECT COUNT(*) FROM audit_events WHERE event_type='providence_write'"
|
||||
).fetchone()[0]
|
||||
assert n_audit == 2
|
||||
qaro.close()
|
||||
|
||||
|
||||
def test_audit_chain_links_correctly(setup):
|
||||
"""Each providence_write event must chain off the previous
|
||||
head — append_audit's own discipline. Verified by walking the
|
||||
chain backwards from the latest event."""
|
||||
corpus, qa = setup
|
||||
q = "what is the core principle of anarchism?"
|
||||
stub = 'The core principle of anarchism is the "abolition of authority". [E1]'
|
||||
# Write two rows via two distinct cache_keys (different model_ids).
|
||||
r1 = providence_query(
|
||||
corpus, q, StubClient(answer=stub),
|
||||
qa_db=qa, model_id="stub", top_k=2,
|
||||
)
|
||||
r2 = providence_query(
|
||||
corpus, q, StubClient(answer=stub),
|
||||
qa_db=qa, model_id="stub2", top_k=2,
|
||||
)
|
||||
assert r1["cache_key"] != r2["cache_key"]
|
||||
qaro = sqlite3.connect(f"file:{qa}?mode=ro", uri=True)
|
||||
rows = qaro.execute(
|
||||
"SELECT event_hash, prev_event_hash, event_type FROM audit_events "
|
||||
"WHERE event_type='providence_write' ORDER BY ts ASC"
|
||||
).fetchall()
|
||||
qaro.close()
|
||||
assert len(rows) == 2
|
||||
# Second event's prev_event_hash must be the first event's hash.
|
||||
assert rows[1][1] == rows[0][0]
|
||||
Loading…
Add table
Add a link
Reference in a new issue