Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ All notable changes to vouch are documented here. Format follows
in config.yaml (#476).

### Fixed
- `receipts.verify_receipt()` no longer reports `VERIFIED` for an
empty-quote, zero-length byte span. the guard only checked `quote is
None`, not an empty string, so `quote=""` with `byte_start ==
byte_end` decoded to `""`, trivially equaled the empty quote, and
verified -- despite carrying no actual quoted text, contradicting the
function's own docstring. `locate_span` already refused to mint such
a receipt on the propose path; this closes the same gap on the
verify path, reachable via bundle import or sync.
- approve/reject/expire record the audit event *before* moving the
proposal to decided/. a crash between the two used to leave a durable
decision with no authoritative history; it now leaves a pending
Expand Down
4 changes: 2 additions & 2 deletions src/vouch/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def verify_receipt(evidence: Evidence, source_bytes: bytes) -> ReceiptResult:
when the decoded span equals the quote exactly.
"""
start, end, quote = evidence.byte_start, evidence.byte_end, evidence.quote
if start is None or end is None or quote is None:
if start is None or end is None or not quote:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return ReceiptResult(ReceiptStatus.NO_RECEIPT, "no byte-offset span")
if start > end or end > len(source_bytes):
return ReceiptResult(
Expand Down Expand Up @@ -184,7 +184,7 @@ def verify_evidence(store: KBStore, evidence: Evidence) -> ReceiptResult:
"""
from .storage import ArtifactNotFoundError

if evidence.byte_start is None or evidence.byte_end is None or evidence.quote is None:
if evidence.byte_start is None or evidence.byte_end is None or not evidence.quote:
return ReceiptResult(ReceiptStatus.NO_RECEIPT, "no byte-offset span")
try:
source_bytes = store.read_source_content(evidence.source_id)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ def test_no_receipt_when_quote_absent() -> None:
assert result.status is ReceiptStatus.NO_RECEIPT


def test_no_receipt_when_quote_is_empty_string() -> None:
# a zero-length span decodes to "" and trivially equals an empty quote --
# the docstring promises NO_RECEIPT for "no quote to compare", and
# locate_span already refuses to mint an empty-quote receipt for the same
# reason (see test_locate_span_returns_none_for_empty_quote); verify_receipt
# must reject one landing on disk some other way (bundle import, sync),
# not report it VERIFIED.
ev = _ev(quote="", byte_start=0, byte_end=0)
result = verify_receipt(ev, SOURCE)
assert result.status is ReceiptStatus.NO_RECEIPT
assert result.verified is False


def test_receipt_uses_byte_offsets_not_char_offsets() -> None:
# "café — au lait": 'é' is 2 bytes (0xc3 0xa9), '—' is 3 bytes (em dash).
# "au lait" starts at char index 7 but byte index 10. A char-offset
Expand Down Expand Up @@ -160,6 +173,23 @@ def test_verify_evidence_not_verified_when_source_missing(store: KBStore) -> Non
assert result.verified is False


def test_verify_evidence_no_receipt_for_empty_quote_and_missing_source(
store: KBStore,
) -> None:
# verify_evidence has its own pre-check ahead of the source-read (so a
# missing source doesn't mask a plain "nothing to compare" case) --
# it must apply the same falsy-quote rule verify_receipt does, or an
# empty quote paired with a missing source reports FORGED instead of
# NO_RECEIPT, same bug as verify_receipt's, one call site up.
ev = Evidence(
id="e4", source_id="does-not-exist",
locator="x", quote="", byte_start=0, byte_end=0,
)
result = verify_evidence(store, ev)
assert result.status is ReceiptStatus.NO_RECEIPT
assert result.verified is False


# ---- the quote step: locate a span, or drop what cannot be quoted ----


Expand Down
Loading