Coverage for node / src / stigmem_node / routes / facts / cid.py: 87%
31 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
1"""CID verification route for facts."""
3from __future__ import annotations
5from typing import Annotated
7from fastapi import Depends, HTTPException, status
8from pydantic import BaseModel
10from ...auth import Identity, resolve_identity
11from ...cid import compute_cid_from_row, stored_cid_from_row
12from ...db import db
13from ...garden_acl import require_fact_garden_read
14from .common import logger, router
17class _CidVerifyResponse(BaseModel):
18 cid_valid: bool
19 computed_cid: str
20 stored_cid: str | None
21 mismatch_reason: str | None = None
24@router.post("/{fact_id}/verify-cid", response_model=_CidVerifyResponse, tags=["facts"])
25def verify_cid(
26 fact_id: str,
27 identity: Annotated[Identity, Depends(resolve_identity)],
28) -> _CidVerifyResponse:
29 """Verify a fact's stored CID against a freshly computed one.
31 Covered by Spec-21-Content-Addressed-IDs.
32 """
33 if not identity.can_read(): 33 ↛ 34line 33 didn't jump to line 34 because the condition on line 33 was never true
34 raise HTTPException(
35 status_code=status.HTTP_403_FORBIDDEN, detail="read permission required"
36 ) # noqa: E501
37 with db() as conn:
38 row = conn.execute(
39 "SELECT * FROM facts WHERE id = ? AND tenant_id = ?",
40 (fact_id, identity.tenant_id),
41 ).fetchone()
42 if row is None:
43 raise HTTPException(status_code=404, detail="fact not found")
44 # Garden ACL: a restricted-garden fact's CID is an existence/content
45 # oracle — hide it from non-members (spec §17.3; sibling of single-get).
46 require_fact_garden_read(conn, fact_id, identity.tenant_id, identity)
47 computed = compute_cid_from_row(row)
48 stored = stored_cid_from_row(row)
49 if stored is None:
50 return _CidVerifyResponse(
51 cid_valid=False,
52 computed_cid=computed,
53 stored_cid=None,
54 mismatch_reason="stored_cid is null (pre-Phase-13 record pending backfill)",
55 )
56 if computed == stored: 56 ↛ 58line 56 didn't jump to line 58 because the condition on line 56 was always true
57 return _CidVerifyResponse(cid_valid=True, computed_cid=computed, stored_cid=stored)
58 logger.warning("CID mismatch for fact %s: computed=%s stored=%s", fact_id, computed, stored)
59 return _CidVerifyResponse(
60 cid_valid=False,
61 computed_cid=computed,
62 stored_cid=stored,
63 mismatch_reason="stored_cid does not match computed_cid",
64 )