portfolio-optimiser-claude/tests/test_sdk_version_guard.py
Kjell Tore Guttormsen 30ba68a703 test(loadbearing): close the vacuous-negative class across the whole suite
Oekt 17 found the class on four named files. This sweep ENUMERATES it: 42 negative
substring assertions across 21 test files (STATE's "~34 across 23" was a premise --
measured, it is 42/21). Sixteen of them measured an absence without ever having
shown presence; all sixteen now carry a positive control asserting the searched-for
string PRESENT in the source artifact, in EXACTLY the form the negative looks for.

Files touched: test_costsim, test_loop, test_okf (3 sites), test_preflight,
test_run_entrance, test_s10_run_layer, test_sdk_version_guard, test_simulation
(2 sites), test_step1_expel, test_step5_refine, test_step7_async_loop,
test_step8_promotion, test_valuereport.

VALUE-PROOF (green-without / red-with, per the oekt-17 rule that a detach proof is
not a value proof). Seven source/fixture mutations, each making the negative vacuous:

  M1 verdict fixture loses the realization signal        VALUE-PROVEN
  M2 decoy fixture loses its text                        VALUE-PROVEN
  M3 renderer stops emitting typed section headings      VALUE-PROVEN
  M4 promotion stops writing the marker                  VALUE-PROVEN (pass 2)
  M5 fold stops rendering the realization surface        VALUE-PROVEN
  M6 report stops labelling the cost section             VALUE-PROVEN
  M7 preflight stops importing the SDK                   VALUE-PROVEN

M4 needed pass 2: a PRECEDING assertion caught the same mutation, hiding the new
control behind it -- the oekt-17 lesson reproduced. The remaining nine controls are
vacuity guards (non-emptiness / form-presence) whose mutation would have to break
the source artificially; they are stated as guards, not claimed as value-proven.

MEASURED FINDING (test_loop): the FIRST-RUN-MARKER negative cannot be given a
positive control at all. Within a run only the CHECKER's critique is fed back --
the proposer's own prior reasoning crosses no prompt boundary, not even within a
run. So that negative holds trivially. Left in place with the limitation stated in
the test rather than dressed up as a controlled seam; the CRITIQUE negative beside
it IS controlled and is the real seam.

Mutations were in-place on src/ and shared/ with original bytes restored and
sha-verified; git status clean before and after. Suite 688 -> 688 (assertions added
inside existing tests, no new test cases). ruff + mypy --strict green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Vc5PmZGjwuJypdhzKnJa5
2026-07-31 21:39:28 +02:00

176 lines
8 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)
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
# (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 _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:
"""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 "
f"{_PIN.removeprefix('claude-agent-sdk')}. "
"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:
# The seam: what uv installs and what this guard vouches for are ONE range.
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)
# Positive control: the marker EXISTS in the whole file, in exactly the form the
# negative searches for — otherwise "absent from the slice" would hold for a
# pyproject that never had a [project] table to be excluded.
assert "[project]" in text
assert len(block) < len(text)
assert "[project]" not in block