Skip to content

Commit ef5b22d

Browse files
Pigbibicodex
andcommitted
Validate canonical status count invariants
Co-Authored-By: Codex <noreply@openai.com>
1 parent c6542c5 commit ef5b22d

2 files changed

Lines changed: 52 additions & 3 deletions

File tree

src/political_event_tracking_research/feed_status_canonical.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
_OUTCOME_KEYS = frozenset({"feed_id", "feed_url", "kind", "state", "rows", "error_code"})
1616
_ERROR_RE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
1717
_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
18+
_EMPTY_DIGEST = hashlib.sha256(b"[]").hexdigest()
1819
_WIRE_KEYS = frozenset(
1920
{
2021
"status_version",
@@ -244,7 +245,7 @@ def _validate_wire(value: object) -> dict[str, object]:
244245
if not _DIGEST_RE.fullmatch(aggregate):
245246
_fail("status_digest_invalid")
246247
feeds = data["feeds"]
247-
if not isinstance(feeds, list):
248+
if not isinstance(feeds, list) or not feeds:
248249
_fail("feed_invalid")
249250
previous: tuple[str, str] | None = None
250251
feed_ids: set[str] = set()
@@ -268,14 +269,20 @@ def _validate_wire(value: object) -> dict[str, object]:
268269
if state in {"accepted", "quarantined"} and kind not in {"rss2", "atom"}:
269270
_fail("feed_kind_invalid")
270271
accepted_count = _integer(item["accepted_row_count"], "feed_counter_invalid")
271-
_integer(item["rejected_row_count"], "feed_counter_invalid")
272+
rejected_count = _integer(item["rejected_row_count"], "feed_counter_invalid")
273+
if accepted_count > MAX_ROWS_PER_FEED:
274+
_fail("feed_counter_invalid")
275+
if rejected_count != 0:
276+
_fail("rejected_count_invalid")
272277
if state == "accepted" and accepted_count == 0:
273278
_fail("feed_state_invalid")
274279
if state != "accepted" and accepted_count != 0:
275280
_fail("feed_state_invalid")
276281
digest = _string(item["row_digest"], "status_digest_invalid")
277282
if not _DIGEST_RE.fullmatch(digest):
278283
_fail("status_digest_invalid")
284+
if state != "accepted" and digest != _EMPTY_DIGEST:
285+
_fail("empty_digest_invalid")
279286
error = item["error_code"]
280287
if state == "accepted" and error is not None:
281288
_fail("feed_state_invalid")
@@ -287,10 +294,18 @@ def _validate_wire(value: object) -> dict[str, object]:
287294
failed = sum(item["state"] == "failed" for item in feeds)
288295
quarantined = sum(item["state"] == "quarantined" for item in feeds)
289296
accepted_rows = sum(item["accepted_row_count"] for item in feeds)
297+
rejected_rows = sum(item["rejected_row_count"] for item in feeds)
290298
if data["successful_feed_count"] != accepted or data["failed_feed_count"] != failed:
291299
_fail("status_counter_invalid")
292-
if data["quarantined_feed_count"] != quarantined or data["accepted_row_count"] != accepted_rows:
300+
if (
301+
data["quarantined_feed_count"] != quarantined
302+
or data["accepted_row_count"] != accepted_rows
303+
or rejected_rows != 0
304+
or data["rejected_row_count"] != 0
305+
):
293306
_fail("status_counter_invalid")
307+
if accepted_rows == 0 and data["aggregate_row_digest"] != _EMPTY_DIGEST:
308+
_fail("empty_digest_invalid")
294309
complete = failed == 0 and quarantined == 0
295310
if data["publication_complete"] != complete or data["eligible_for_live_publication"] != complete:
296311
_fail("status_flag_invalid")

tests/test_feed_status_canonical.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
from political_event_tracking_research.feed_status_canonical import (
9+
MAX_ROWS_PER_FEED,
910
DecisionContractError,
1011
DecisionKind,
1112
build_decision,
@@ -96,3 +97,36 @@ def test_tampered_duplicate_and_noncanonical_bytes_fail_closed() -> None:
9697
read_status(duplicate)
9798
with pytest.raises(DecisionContractError, match="noncanonical"):
9899
read_status(result.status_bytes + b"\n")
100+
101+
102+
def test_readback_rejects_empty_feed_list_and_wrong_empty_digests() -> None:
103+
result = build_decision([outcome("empty", "quarantined", error_code="zero_entries")])
104+
payload = json.loads(result.status_bytes)
105+
payload["feeds"] = []
106+
payload["feed_count"] = 0
107+
payload["configured_feed_count"] = 0
108+
with pytest.raises(DecisionContractError, match="feed_count|feed_invalid"):
109+
read_status(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())
110+
111+
payload = json.loads(result.status_bytes)
112+
payload["feeds"][0]["row_digest"] = "0" * 64
113+
with pytest.raises(DecisionContractError, match="empty_digest"):
114+
read_status(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())
115+
116+
117+
def test_readback_rechecks_row_count_bounds_sums_and_rejected_zero() -> None:
118+
result = build_decision([outcome("a", "accepted")])
119+
payload = json.loads(result.status_bytes)
120+
payload["feeds"][0]["accepted_row_count"] = MAX_ROWS_PER_FEED + 1
121+
with pytest.raises(DecisionContractError, match="feed_counter"):
122+
read_status(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())
123+
124+
payload = json.loads(result.status_bytes)
125+
payload["accepted_row_count"] = 0
126+
with pytest.raises(DecisionContractError, match="counter"):
127+
read_status(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())
128+
129+
payload = json.loads(result.status_bytes)
130+
payload["feeds"][0]["rejected_row_count"] = 1
131+
with pytest.raises(DecisionContractError, match="rejected"):
132+
read_status(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode())

0 commit comments

Comments
 (0)