Skip to content
Merged
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
12 changes: 11 additions & 1 deletion loop/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ def _read_json(path: Path, issues: list[dict]) -> dict[str, Any] | None:
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except UnicodeDecodeError as exc:
# Same rule as _validate_jsonl: undecodable bytes fail the file closed
# with a typed finding, never a traceback out of doctor_report (#107).
issues.append(ContractIssue("invalid_encoding", f"{path.name}: not valid UTF-8: {exc}", path))
Comment on lines +59 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Catch encoding failures during event-store reconciliation

When events.db is present, this catch only protects the first contract-validation pass: doctor_report subsequently calls event_consistency_issues, whose _terminal_desync and _state_divergence reread these files but do not catch UnicodeDecodeError. I reproduced a scaffolded workspace with a valid contract_opened event and an invalid-UTF-8 terminal_state.json; doctor_report still raises a raw traceback from runtime.py:212 instead of returning the new invalid_encoding finding. The reconciliation reads need the same encoding-error handling for event-backed workspaces.

Useful? React with 👍 / 👎.

return None
except json.JSONDecodeError as exc:
issues.append(ContractIssue("invalid_json", f"{path.name}: {exc}", path))
return None
Expand Down Expand Up @@ -139,7 +144,12 @@ def _fallback_yaml(text: str) -> dict[str, Any]:
def read_manifest(path: Path) -> dict[str, Any] | None:
if not path.exists():
return None
text = path.read_text(encoding="utf-8")
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
# Undecodable bytes are malformed content: fail safe to {} exactly like
# the malformed-YAML branch below, never propagate a traceback (#107).
return {}
try:
import yaml # type: ignore
except Exception:
Expand Down
4 changes: 3 additions & 1 deletion loop/verdict.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def _terminal_record(paths: LoopPaths) -> dict[str, Any]:
)
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
# UnicodeDecodeError became reachable here once doctor_report stopped
# raising it (#107); the site-agnostic typed-contract test depends on it.
raise VerdictError(f"terminal record is unreadable: {exc}") from exc
if not isinstance(data, dict):
raise VerdictError("terminal record is not an object")
Expand Down
25 changes: 25 additions & 0 deletions scripts/test_contract_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,28 @@ def test_repo_own_contract_still_validates_clean():
pytest.skip("no live .loop contract in this checkout (gitignored run-state)")
report = validate_contract(ROOT / ".loop")
assert report["ok"] is True, report["issues"]


def test_doctor_reports_invalid_encoding_for_an_undecodable_contract_object(tmp_path):
# #107: a terminal_state.json holding invalid UTF-8 bytes is a typed doctor
# finding, never a raw UnicodeDecodeError traceback out of doctor_report.
from loop.contract import doctor_report
from loop.scaffold import scaffold

workspace = tmp_path / "ws"
scaffold(workspace)
(workspace / ".loop" / "terminal_state.json").write_bytes(b"\xff\xfe{}")

report = doctor_report(workspace)
assert report["ok"] is False
assert any(i["code"] == "invalid_encoding" for i in report["issues"]), report["issues"]


def test_read_manifest_fails_safe_on_undecodable_bytes(tmp_path):
# Mirrors the malformed-YAML rule stated inline in read_manifest: a manifest
# that cannot be decoded fails safe to {} rather than propagating a traceback.
from loop.contract import read_manifest

path = tmp_path / "manifest.yaml"
path.write_bytes(b"\xff\xfename: x")
assert read_manifest(path) == {}
Loading