feat(inbox): C2.5 — inbox hardening + SDK version guard (closes C-F7, C-N3, R-6)
- File-layer decision vocabulary (§4.2 set) with SKIP semantics — an unknown decision never reaches the store (C-F7, the review's run proof is the fixture) - Fail-fast caps (max_files / max_rationale_chars) via InboxLimitError raised OUTSIDE the tolerant try — a cap breach is never swallowed as a skip - R-6 id grammar (mirrors ingest _ID_RE) as a pydantic pattern on VerdictDocument.id AND re-checked in write_verdict, since model_copy(update=) bypasses model validation — traversal ids can no longer write outside the inbox - promotion._filename_token: any sanitised id maps to a content hash — 'e/vil' can no longer clobber the distinct id 'evil' (restarbeid-funn 2) - SDK pinned >=0.2.111,<0.3 + version guard test naming the sdk_client.py attribute premises; resolved 0.2.120, all premises re-verified against it - sdk_client read loop bound offline with REAL SDK message types (R-4/R-5): text aggregation, error fail-paths, usage/cost extraction, _total_tokens fail-closed, non-positive budget guard - test_sdk_isolation comment no longer claims the --system-prompt "" serialization the test body does not bind (honesty rule §1) Guard-G2 assessment (guard-plan §4): the allowlist + caps + id grammar landed here are G2's necessary part; an optional scan_output depth pass over rationale (still a verbatim prose channel into the fold prompt, R-9) remains relevant as a later additive session — the trigger picture is unchanged. 4 detach proofs red → restored green. Full gate: 389 passed (365→389), ruff+format+mypy clean; golden + shared/ + runs/s10/ byte-untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e7ce6b0a31
commit
80a2fa1a77
9 changed files with 441 additions and 35 deletions
82
tests/test_sdk_version_guard.py
Normal file
82
tests/test_sdk_version_guard.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""SDK version guard (C2.5, closes C-N3) — LOAD-BEARING (§11).
|
||||
|
||||
The seam this file keeps alive: every SDK attribute premise in
|
||||
``sdk_client.py`` was verified against a CONCRETE version range
|
||||
(0.2.110 read at source level, release notes through 0.2.120; sdk-review
|
||||
2026-07-16). The pin ``claude-agent-sdk>=0.2.111,<0.3`` freezes that range —
|
||||
this guard makes an upgrade outside it a RED test naming exactly which
|
||||
premises must be re-verified, instead of a silent behaviour drift.
|
||||
|
||||
Offline-safe: reads installed package metadata only — no key, no network.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
||||
|
||||
# The verified range — MUST match the pyproject pin (bound below).
|
||||
_VERIFIED_FLOOR = (0, 2, 111)
|
||||
_VERIFIED_CEILING = (0, 3)
|
||||
_PIN = "claude-agent-sdk>=0.2.111,<0.3"
|
||||
|
||||
# The sdk_client.py attribute premises the verified range vouches for
|
||||
# (sdk-review 2026-07-16, verified against package source through 0.2.120).
|
||||
_SDK_PREMISES = (
|
||||
"AssistantMessage.error/.model/.content",
|
||||
"ResultMessage.usage/.total_cost_usd/.is_error/.subtype/.errors",
|
||||
"ClaudeAgentOptions.max_budget_usd/.setting_sources/.system_prompt/.max_turns/.model/.tools",
|
||||
"TextBlock.text",
|
||||
"query() yields AssistantMessage then a closing ResultMessage",
|
||||
)
|
||||
|
||||
|
||||
def _parse(raw: str) -> tuple[int, ...]:
|
||||
return tuple(int(part) for part in raw.split(".")[:3])
|
||||
|
||||
|
||||
def check_sdk_version() -> str:
|
||||
"""Fail if the installed SDK is outside the verified range — naming the premises."""
|
||||
raw = importlib.metadata.version("claude-agent-sdk")
|
||||
if not (_VERIFIED_FLOOR <= _parse(raw) < _VERIFIED_CEILING):
|
||||
raise AssertionError(
|
||||
f"claude-agent-sdk {raw} is OUTSIDE the verified range >=0.2.111,<0.3. "
|
||||
"Re-verify the sdk_client.py attribute premises against the new version "
|
||||
"BEFORE widening the pin (pyproject.toml + this guard together): "
|
||||
+ "; ".join(_SDK_PREMISES)
|
||||
)
|
||||
return raw
|
||||
|
||||
|
||||
class TestSdkVersionGuard:
|
||||
def test_the_installed_sdk_is_within_the_verified_range(self) -> None:
|
||||
assert check_sdk_version() == importlib.metadata.version("claude-agent-sdk")
|
||||
|
||||
def test_a_version_above_the_ceiling_trips_the_guard(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(importlib.metadata, "version", lambda name: "0.3.0")
|
||||
with pytest.raises(AssertionError) as err:
|
||||
check_sdk_version()
|
||||
message = str(err.value)
|
||||
assert "sdk_client.py" in message
|
||||
for premise in _SDK_PREMISES:
|
||||
assert premise in message
|
||||
|
||||
def test_a_version_below_the_floor_trips_the_guard(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# 0.2.110 works but misses the 0.2.111 read-loop fixes (NDJSON
|
||||
# whitespace on >64 KiB lines, plain-string content TypeError,
|
||||
# zombie subprocess on cancellation) — below the floor is unverified.
|
||||
monkeypatch.setattr(importlib.metadata, "version", lambda name: "0.2.110")
|
||||
with pytest.raises(AssertionError):
|
||||
check_sdk_version()
|
||||
|
||||
def test_the_pyproject_pin_matches_the_verified_range(self) -> None:
|
||||
# Detach-proof: the pin and this guard cannot drift apart silently.
|
||||
assert _PIN in _PYPROJECT.read_text(encoding="utf-8")
|
||||
Loading…
Add table
Add a link
Reference in a new issue