portfolio-optimiser-claude/tests/test_sdk_version_guard.py
Kjell Tore Guttormsen 80a2fa1a77 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>
2026-07-16 20:26:41 +02:00

82 lines
3.3 KiB
Python

"""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")