test(sdk-guard): anchor the pyproject pin, make the detach-proof a test
The sweep the §12 work called for, run over every test reading a static repo document. Enumerated population: four such guards (method-spec, ingest-spec, README, pyproject). Three were already sound — the two spec guards were anchored in sessions 14/15, and the README guard extracts flags by regex and cross-checks them against real --help output with explicit vacuity guards. The fourth was green-but-dead, and it was MEASURED, not inferred: `assert _PIN in _PYPROJECT.read_text()` stayed GREEN (4 passed) while the real dependency drifted to >=0.2.110 below the guard's own verified floor, because the literal survived in a trailing comment. The comment above it claimed "Detach-proof: the pin and this guard cannot drift apart silently" — the exact drift it named is what it let through. Three narrowings, each one a measured degeneration rather than a precaution: - ANCHOR: match inside the `dependencies = [...]` array, fail-closed with ValueError when the array is renamed (a silently empty slice would make every assertion vacuous). - QUOTED FORM: the slice alone still did not detach — a comment sits inside the array too. Requiring `"<pin>"` with comments stripped does. - VALUE BINDING: _PIN is now DERIVED from _VERIFIED_FLOOR/_CEILING via _pin_for(), so the range this guard enforces and the pin it demands cannot part company. The error message derives from it too, instead of carrying a third hand-maintained copy that could lie. Five permanent red-proofs replace the manual spot-check, all run against a mutated COPY of the text, never pyproject.toml itself. Measured degeneracy: substring-anywhere restored -> 1 red; anchor widened to the whole file -> 2 red; the derived-pin binding severed -> 2 red. Suite 683 -> 688; ruff, format and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FcKMxznPVR9zfdsdu5Ztdn
This commit is contained in:
parent
e70de95afb
commit
769687159f
1 changed files with 94 additions and 4 deletions
|
|
@ -22,7 +22,21 @@ _PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
||||||
# The verified range — MUST match the pyproject pin (bound below).
|
# The verified range — MUST match the pyproject pin (bound below).
|
||||||
_VERIFIED_FLOOR = (0, 2, 111)
|
_VERIFIED_FLOOR = (0, 2, 111)
|
||||||
_VERIFIED_CEILING = (0, 3)
|
_VERIFIED_CEILING = (0, 3)
|
||||||
_PIN = "claude-agent-sdk>=0.2.111,<0.3"
|
|
||||||
|
|
||||||
|
def _pin_for(floor: tuple[int, ...], ceiling: tuple[int, ...]) -> str:
|
||||||
|
"""The pyproject requirement string the verified range implies.
|
||||||
|
|
||||||
|
DERIVED, never a third hand-maintained copy: the range this guard enforces and
|
||||||
|
the pin it asserts on cannot drift apart, because moving the range moves the
|
||||||
|
expected pin with it (value-proof below).
|
||||||
|
"""
|
||||||
|
low = ".".join(str(part) for part in floor)
|
||||||
|
high = ".".join(str(part) for part in ceiling)
|
||||||
|
return f"claude-agent-sdk>={low},<{high}"
|
||||||
|
|
||||||
|
|
||||||
|
_PIN = _pin_for(_VERIFIED_FLOOR, _VERIFIED_CEILING)
|
||||||
|
|
||||||
# The sdk_client.py attribute premises the verified range vouches for
|
# 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-review 2026-07-16, verified against package source through 0.2.120).
|
||||||
|
|
@ -39,12 +53,43 @@ def _parse(raw: str) -> tuple[int, ...]:
|
||||||
return tuple(int(part) for part in raw.split(".")[:3])
|
return tuple(int(part) for part in raw.split(".")[:3])
|
||||||
|
|
||||||
|
|
||||||
|
def _dependencies_block(text: str) -> str:
|
||||||
|
"""The ``dependencies = [...]`` array of a pyproject, as text (ANCHOR).
|
||||||
|
|
||||||
|
Fail-closed: a pyproject that no longer declares the array raises rather than
|
||||||
|
returning "" — a silently empty anchor would make every assertion below vacuous
|
||||||
|
(the degeneration mode measured on the §12 guard).
|
||||||
|
"""
|
||||||
|
start = text.find("dependencies = [")
|
||||||
|
if start == -1:
|
||||||
|
raise ValueError("pyproject declares no 'dependencies = [' array — anchor lost")
|
||||||
|
end = text.find("]", start)
|
||||||
|
if end == -1:
|
||||||
|
raise ValueError("pyproject 'dependencies' array is unterminated — anchor lost")
|
||||||
|
return text[start : end + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def _pin_is_declared(text: str, pin: str) -> bool:
|
||||||
|
"""True when ``pin`` is a real runtime dependency — not merely present somewhere.
|
||||||
|
|
||||||
|
Two narrowings, each one a measured degeneration rather than a precaution:
|
||||||
|
``pin in text`` was GREEN while the real dependency had drifted below the
|
||||||
|
verified floor and the literal survived in a trailing comment (measured
|
||||||
|
2026-07-31), so the match is scoped to the anchor AND requires the QUOTED
|
||||||
|
form with comments stripped — a comment sits inside the array too, so the
|
||||||
|
slice alone still does not detach.
|
||||||
|
"""
|
||||||
|
body = "\n".join(line.split("#", 1)[0] for line in _dependencies_block(text).splitlines())
|
||||||
|
return f'"{pin}"' in body
|
||||||
|
|
||||||
|
|
||||||
def check_sdk_version() -> str:
|
def check_sdk_version() -> str:
|
||||||
"""Fail if the installed SDK is outside the verified range — naming the premises."""
|
"""Fail if the installed SDK is outside the verified range — naming the premises."""
|
||||||
raw = importlib.metadata.version("claude-agent-sdk")
|
raw = importlib.metadata.version("claude-agent-sdk")
|
||||||
if not (_VERIFIED_FLOOR <= _parse(raw) < _VERIFIED_CEILING):
|
if not (_VERIFIED_FLOOR <= _parse(raw) < _VERIFIED_CEILING):
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
f"claude-agent-sdk {raw} is OUTSIDE the verified range >=0.2.111,<0.3. "
|
f"claude-agent-sdk {raw} is OUTSIDE the verified range "
|
||||||
|
f"{_PIN.removeprefix('claude-agent-sdk')}. "
|
||||||
"Re-verify the sdk_client.py attribute premises against the new version "
|
"Re-verify the sdk_client.py attribute premises against the new version "
|
||||||
"BEFORE widening the pin (pyproject.toml + this guard together): "
|
"BEFORE widening the pin (pyproject.toml + this guard together): "
|
||||||
+ "; ".join(_SDK_PREMISES)
|
+ "; ".join(_SDK_PREMISES)
|
||||||
|
|
@ -78,5 +123,50 @@ class TestSdkVersionGuard:
|
||||||
check_sdk_version()
|
check_sdk_version()
|
||||||
|
|
||||||
def test_the_pyproject_pin_matches_the_verified_range(self) -> None:
|
def test_the_pyproject_pin_matches_the_verified_range(self) -> None:
|
||||||
# Detach-proof: the pin and this guard cannot drift apart silently.
|
# The seam: what uv installs and what this guard vouches for are ONE range.
|
||||||
assert _PIN in _PYPROJECT.read_text(encoding="utf-8")
|
assert _pin_is_declared(_PYPROJECT.read_text(encoding="utf-8"), _PIN)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheSeamGoesRedWhenDetached:
|
||||||
|
"""LOAD-BEARING (§11): the assertions above must FAIL on a detached pyproject.
|
||||||
|
|
||||||
|
Red-proofs run against a mutated COPY of the pyproject text — never against
|
||||||
|
``pyproject.toml`` itself. Each one is a former manual spot-check made permanent:
|
||||||
|
a detach-proof that dies with the session is not a proof.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_red_when_the_pin_survives_only_outside_the_dependencies_block(self) -> None:
|
||||||
|
# The MEASURED degeneration: the real dependency drops below the verified
|
||||||
|
# floor while the exact pin string lives on in a trailing comment. The
|
||||||
|
# unanchored `_PIN in text` assertion stayed GREEN through exactly this.
|
||||||
|
text = _PYPROJECT.read_text(encoding="utf-8")
|
||||||
|
mutated = text.replace(f'"{_PIN}",', f'"claude-agent-sdk>=0.2.110,<0.3", # was: {_PIN}')
|
||||||
|
assert mutated != text, "mutation did not apply — the red-proof would be vacuous"
|
||||||
|
assert _PIN in mutated, "the literal must survive, or this proves the wrong thing"
|
||||||
|
assert not _pin_is_declared(mutated, _PIN)
|
||||||
|
|
||||||
|
def test_red_when_the_verified_floor_drifts_from_the_pin(self) -> None:
|
||||||
|
# VALUE-proof, not merely a detach-proof: moving the range this guard
|
||||||
|
# enforces moves the pin it demands, so the two cannot part company.
|
||||||
|
drifted = _pin_for((0, 2, 110), _VERIFIED_CEILING)
|
||||||
|
assert drifted != _PIN
|
||||||
|
assert not _pin_is_declared(_PYPROJECT.read_text(encoding="utf-8"), drifted)
|
||||||
|
|
||||||
|
def test_red_when_the_verified_ceiling_drifts_from_the_pin(self) -> None:
|
||||||
|
drifted = _pin_for(_VERIFIED_FLOOR, (0, 4))
|
||||||
|
assert drifted != _PIN
|
||||||
|
assert not _pin_is_declared(_PYPROJECT.read_text(encoding="utf-8"), drifted)
|
||||||
|
|
||||||
|
def test_the_anchor_is_fail_closed_when_the_array_is_renamed(self) -> None:
|
||||||
|
# An anchor that degenerates to "" would make every assertion above vacuous.
|
||||||
|
text = _PYPROJECT.read_text(encoding="utf-8").replace("dependencies = [", "deps = [")
|
||||||
|
with pytest.raises(ValueError, match="anchor lost"):
|
||||||
|
_dependencies_block(text)
|
||||||
|
|
||||||
|
def test_the_anchor_does_not_degenerate_to_the_whole_file(self) -> None:
|
||||||
|
# Mutate the ANCHOR itself: a slice that silently widened to the entire
|
||||||
|
# pyproject would re-admit the substring-anywhere defect unnoticed.
|
||||||
|
text = _PYPROJECT.read_text(encoding="utf-8")
|
||||||
|
block = _dependencies_block(text)
|
||||||
|
assert len(block) < len(text)
|
||||||
|
assert "[project]" not in block
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue