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