core/tests/test_proposal_review_scan.py
Shay 3c6f820b54 feat(proposal-review): read-only scanner over the comprehension-failure sink (RPT-a)
core/proposal_review/{model,scan}.py: scan reads teaching/proposals/comprehension_failures/*.json into typed PendingProposal records (path, content_hash, failure_family, status, mounted, requires_review, problem_text_sha256, observed_attempts); any unparseable file becomes a MalformedArtifact (never silently dropped). Pure read — never writes/moves/deletes; deterministic (sorted by path); missing sink -> empty. The sink path is computed independently of the emitter (the reporter verifies the contract, doesn't import the writer).

Read-only reporter, NOT an idle_tick (ChatRuntime.idle_tick stays the only one), NOT L10. 4 tests incl. read-only invariant + malformed flagging.
2026-06-07 21:39:03 -07:00

56 lines
2.3 KiB
Python

"""Tests for the read-only proposal scanner (RPT-a).
Emits real proposals via the contemplation pass into a tmp sink, scans them into typed
PendingProposal records, flags malformed artifacts, and pins that scanning never mutates the sink.
"""
from __future__ import annotations
import json
from pathlib import Path
from core.proposal_review.model import PendingProposal
from core.proposal_review.scan import scan
from evals.constraint_oracle.runner import _load_r2_gold
from generate.contemplation import contemplate
def _emit_gold_proposals(root: Path) -> None:
for fx in _load_r2_gold():
if fx["expect"] == "reader_refuses" and fx["reader_reason"] in (
"missing_total_count",
"missing_weighted_total",
):
contemplate(fx["text"], proposal_root=root, case_id=fx["id"])
def test_scan_reads_emitted_proposals(tmp_path: Path) -> None:
_emit_gold_proposals(tmp_path)
proposals, malformed = scan(tmp_path)
assert len(proposals) == 2 and malformed == []
assert all(isinstance(p, PendingProposal) for p in proposals)
assert {p.failure_family for p in proposals} == {"missing_total_count", "missing_weighted_total"}
assert all(p.status == "proposal_only" and not p.mounted and p.requires_review for p in proposals)
assert all(p.content_hash == Path(p.path).stem for p in proposals)
def test_scan_flags_malformed_artifacts(tmp_path: Path) -> None:
(tmp_path / "bad.json").write_text("{ not valid json", encoding="utf-8")
(tmp_path / "incomplete.json").write_text(json.dumps({"status": "proposal_only"}), encoding="utf-8")
proposals, malformed = scan(tmp_path)
assert proposals == [] and len(malformed) == 2
reasons = " ".join(m.reason for m in malformed)
assert "invalid_json" in reasons and "missing_field" in reasons
def test_scan_missing_sink_is_empty(tmp_path: Path) -> None:
assert scan(tmp_path / "does_not_exist") == ([], [])
def test_scan_is_read_only(tmp_path: Path) -> None:
_emit_gold_proposals(tmp_path)
before = {p.name: p.read_bytes() for p in tmp_path.glob("*.json")}
scan(tmp_path)
scan(tmp_path)
after = {p.name: p.read_bytes() for p in tmp_path.glob("*.json")}
assert before == after and before # unchanged, and non-empty (the proposals exist)