test(sdk): the pin was a permission, so give the premises a proof
The guard checked whether the installed SDK satisfied the pin. Nobody had ever checked whether anyone had READ it. Those are different questions, and the gap between them was a whole version range: pinned >=0.2.111,<0.3, premises source-verified through 0.2.110, installed 0.2.120. Every build in between was admissible and unexamined — `uv sync --upgrade` would have kept 806 tests green on an SDK no one had opened. Written red first: a guard handed 0.2.140 returned it without complaint. _VERIFIED_THROUGH is the ratchet. It records the newest build actually read at source, and a newer one fails naming the five premises to re-check. The pin is untouched and was never the defect — measurement dissolved the premise that it needed lifting. It was not too narrow but too wide, and a wider permission is not repaired by widening it further. The premises themselves were prose the failure message recited. Nothing tested them, so one that stopped being true would have surfaced on the one live paid run (S10, D6). They are now a table introspected against the installed package, with the printed prose derived from that same table so a checked attribute cannot go unreported or a reported one unchecked. The premise introspection structurally cannot see — that query() yields an AssistantMessage then a closing ResultMessage — is named apart, and is the honest reason the human reading still has to happen. Value-proved, not merely named: disabling the ratchet reds 1 test, stubbing the inventory to "no gaps" reds 3, re-hardcoding the prose reds 1, and lowering _VERIFIED_THROUGH below the installed build reds the real installed-version test rather than only a monkeypatched one. 0.2.139 read at source (0.2.120 -> 0.2.139, latest on PyPI today; STATE said 0.2.134, measured 08-09 and stale). The public query.py is byte-identical, every premise field keeps its type and default, and the parser changes are additive. One needed a look: 0.2.139 added a skills path defaulting setting_sources to ["user", "project"], which would have undone the S10 isolation fix — it fires only on None, so the explicit [] is out of reach. Prose carrying stale version claims moved with the reading, never ahead of it: each was re-verified at 0.2.139 before being restated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dKDjVG7qrBh9NkAAxutqN
This commit is contained in:
parent
0b69354c74
commit
90a41774fc
8 changed files with 271 additions and 38 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **The SDK pin now has a proof behind it, not only a permission.** `claude-agent-sdk`
|
||||
moved 0.2.120 → 0.2.139, and the version guard gained `_VERIFIED_THROUGH`: the newest
|
||||
build whose source was actually read. The pin (`>=0.2.111,<0.3`, unchanged) says what
|
||||
`uv` may resolve; it never said anyone had looked. Those are different questions, and
|
||||
conflating them left 0.2.111–0.2.999 admissible while the premises were source-verified
|
||||
only through 0.2.110 — `uv sync --upgrade` would have kept the suite green on an
|
||||
unexamined build. A build newer than the last one read now fails RED, naming the
|
||||
premises to re-check. A third question — "is something newer upstream?" — is
|
||||
deliberately NOT asked: it needs the network, and this suite stays offline.
|
||||
- **The SDK premises are checked, not merely printed.** They had existed only as a
|
||||
sentence the failure message recited; nothing verified them, so a premise that stopped
|
||||
being true would have surfaced on the one live paid run. They are now a table
|
||||
introspected against the installed package, with the operator-facing prose derived from
|
||||
that same table so the two cannot drift. The one premise introspection structurally
|
||||
cannot see — that `query()` yields an `AssistantMessage` then a closing `ResultMessage`
|
||||
— is named separately, and is the honest reason a human reading is still required.
|
||||
- Re-verified at 0.2.139 source level: the public `query.py` is byte-identical to 0.2.120,
|
||||
every premise field keeps its type and default, and the parser changes are additive
|
||||
(a new `origin` passthrough, a new `ConversationResetMessage` this client ignores).
|
||||
0.2.139 also added a skills path that can default `setting_sources` to
|
||||
`["user", "project"]`; it fires only on `None`, so the explicit `[]` that carries the
|
||||
S10 isolation fix is out of its reach.
|
||||
|
||||
## [0.1.0] - 2026-08-17
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ alene; det er datert i README og skal ikke påstås om arbeid etter den datoen.
|
|||
`docs/plan/`. Løpende state: `STATE.md` (local-only).
|
||||
|
||||
## Stack
|
||||
Python ≥3.10. **Claude Agent SDK** (`claude-agent-sdk` ≥0.2, verifisert 0.2.110 per
|
||||
Python ≥3.10. **Claude Agent SDK** (`claude-agent-sdk` ≥0.2, kildeverifisert 0.2.139 per
|
||||
2026-08-18 — pinnen `>=0.2.111,<0.3` er en TILLATELSE, `_VERIFIED_THROUGH` i
|
||||
`tests/test_sdk_version_guard.py` er beviset og går rødt på en ulest nyere build. Per
|
||||
2026-07-03: MIT-lisens, bundler Claude Code CLI — ingen separat installasjon; import og
|
||||
konstruksjon fungerer uten API-nøkkel, nøkkel trengs kun ved faktisk `query()`).
|
||||
Pakkehåndtering: `uv`. Pydantic for kontrakts-validering.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ Four offline checks (for a given backend profile, ``anthropic`` today):
|
|||
refusal naming ``uv sync``, never an ImportError), the run-path symbols exist,
|
||||
and the bundled Claude Code CLI is present on disk — located offline via the
|
||||
SDK package's own files, mirroring the SDK's ``_find_cli`` order (bundled
|
||||
first, then a ``claude`` on PATH). Verified against installed 0.2.120.
|
||||
first, then a ``claude`` on PATH). Verified against installed 0.2.139 by
|
||||
reading its ``_find_cli`` (bundled at line 250, PATH at 256) — the order is
|
||||
unchanged from 0.2.120.
|
||||
* **termination** — the §8 stop contract constructs with positive round/token
|
||||
caps and the per-call USD cap is positive (the budget belt is SET).
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,18 @@ turn, a first-class USD cap (``ClaudeAgentOptions.max_budget_usd``) ON TOP of
|
|||
the §8 token/round meter that the loop already charges, and NO filesystem
|
||||
settings (``setting_sources=[]``).
|
||||
|
||||
Verified against claude-agent-sdk 0.2.110 at source level and release notes
|
||||
through 0.2.120 — pinned ``>=0.2.111,<0.3`` with a version guard
|
||||
(``tests/test_sdk_version_guard.py``) that forces re-verification of these
|
||||
premises before any widening: ``query()`` yields ``AssistantMessage`` (text
|
||||
blocks + real model id) and a closing ``ResultMessage`` (provider-reported
|
||||
``usage`` + ``total_cost_usd``). A reply without usage is passed through as
|
||||
``None`` so the meter fails CLOSED (§8) — this client never invents a count.
|
||||
Verified against claude-agent-sdk 0.2.139 AT SOURCE LEVEL (2026-08-18; the
|
||||
public ``query.py`` is byte-identical to 0.2.120 and every field below is
|
||||
present with an unchanged type and default). Pinned ``>=0.2.111,<0.3``, but the
|
||||
pin is only a permission — ``tests/test_sdk_version_guard.py`` carries the
|
||||
proof: it checks each attribute below against the INSTALLED package and goes
|
||||
red on any build newer than the last one actually read. The premises:
|
||||
``query()`` yields ``AssistantMessage`` (text blocks + real model id) and a
|
||||
closing ``ResultMessage`` (provider-reported ``usage`` + ``total_cost_usd``).
|
||||
Unknown message types are ignored by the loop below, so an SDK that adds one
|
||||
(0.2.139 added ``ConversationResetMessage``) does not disturb this client.
|
||||
A reply without usage is passed through as ``None`` so the meter fails CLOSED
|
||||
(§8) — this client never invents a count.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -47,14 +52,16 @@ _USAGE_TOKEN_FIELDS = (
|
|||
def build_call_options(model_id: str, *, max_budget_usd: float) -> ClaudeAgentOptions:
|
||||
"""One bounded, ISOLATED completion call (§8 + S10 post-mortem).
|
||||
|
||||
``setting_sources=[]`` is the SDK's documented isolation mode (verified
|
||||
against 0.2.110): the spawned CLI loads NO filesystem settings — no
|
||||
``setting_sources=[]`` is the SDK's documented isolation mode (re-read at
|
||||
0.2.139): the spawned CLI loads NO filesystem settings — no
|
||||
session hooks, no CLAUDE.md, no operator instructions. The default
|
||||
(``None``) loads ALL sources: in the S10 live run that injected the
|
||||
operator's config into every call (~10-15k uncached tokens each) and
|
||||
mandated a confirmation preamble that made pure-JSON replies impossible.
|
||||
``system_prompt=None`` serializes to an EMPTY system prompt, not the
|
||||
Claude Code preset.
|
||||
Claude Code preset. 0.2.139 added a skills path that can DEFAULT
|
||||
``setting_sources`` to ``["user", "project"]`` — it fires only on ``None``,
|
||||
so the explicit ``[]`` here is not reachable by it (read at source).
|
||||
"""
|
||||
return ClaudeAgentOptions(
|
||||
model=model_id,
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class TestPlaceholderDetector:
|
|||
|
||||
|
||||
class TestSdkAndBundledCli:
|
||||
"""Nøkkelantakelse (verified against installed claude-agent-sdk 0.2.120): the
|
||||
"""Nøkkelantakelse (verified against installed claude-agent-sdk 0.2.139): the
|
||||
SDK imports and its bundled CLI is present on disk — checked OFFLINE via the
|
||||
package's own files, mirroring the SDK's ``_find_cli`` order. Never a net call."""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The S10 live run leaked the operator's Claude Code configuration into every
|
||||
spawned CLI session: ``ClaudeAgentOptions.setting_sources`` defaults to
|
||||
``None``, which loads ALL filesystem settings (verified against SDK 0.2.110)
|
||||
``None``, which loads ALL filesystem settings (verified against SDK 0.2.139)
|
||||
— session-start hooks injected STATE.md into the model's context, every reply
|
||||
opened with a mandated confirmation line (so a reply was NEVER pure JSON),
|
||||
and each call paid ~10-15k uncached context tokens. ``[]`` is the SDK's
|
||||
|
|
@ -49,11 +49,13 @@ class TestBuildCallOptions:
|
|||
def test_the_system_prompt_is_not_the_claude_code_preset(self) -> None:
|
||||
# Pins the OPTION value against the Claude Code preset. That None
|
||||
# reaches the spawned CLI as --system-prompt "" was verified by
|
||||
# READING subprocess_cli.py (0.2.110–0.2.120) — this test does NOT
|
||||
# READING subprocess_cli.py (0.2.110–0.2.139, re-read at 0.2.139:
|
||||
# `system_prompt is None` still serializes to `--system-prompt ""`)
|
||||
# — this test does NOT
|
||||
# bind that transport serialization; doing so would couple the suite
|
||||
# to SDK-private API (the F11 fragility this repo retired).
|
||||
#
|
||||
# HONEST LIMIT (measured, 0.2.120): `system_prompt=None` is NOT
|
||||
# HONEST LIMIT (measured, re-measured at 0.2.139): `system_prompt=None` is NOT
|
||||
# distinguishable from leaving the field untouched — the SDK default
|
||||
# is None too, so deleting `system_prompt=None` from build_call_options
|
||||
# left the old `is None` assertion GREEN. It pinned the SDK's default,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,38 @@
|
|||
"""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.
|
||||
``sdk_client.py`` rests on a version whose source was actually READ.
|
||||
|
||||
Offline-safe: reads installed package metadata only — no key, no network.
|
||||
Three questions hide inside "is the SDK fine?", and conflating them is what
|
||||
let a whole version range go unexamined (measured 2026-08-09: installed
|
||||
0.2.120, pin ``>=0.2.111,<0.3``, premises source-verified only through
|
||||
0.2.110 — so 0.2.111–0.2.999 satisfied the pin while nobody had read them):
|
||||
|
||||
(a) is the build inside the PIN? — what ``uv`` is allowed to resolve
|
||||
(b) is the build inside the VERIFIED? — what a human has actually read
|
||||
(c) is something newer upstream? — REQUIRES NETWORK, never in pytest
|
||||
|
||||
A pin is a permission, not a proof, and it can reach further than the reading
|
||||
that justified it. ``_VERIFIED_THROUGH`` is the ratchet for (b): a build newer
|
||||
than the last one read at source goes RED naming the premises to re-verify,
|
||||
even though the pin admits it. Question (c) is deliberately absent — an
|
||||
offline suite cannot answer it, and a test that reached the network to try
|
||||
would trade a silent gap for a flaky one.
|
||||
|
||||
Two instruments, because they fail differently: ``TestTheSdkSurfaceInventory``
|
||||
introspects the INSTALLED package, so a premise naming an attribute that no
|
||||
longer exists goes red on ANY build — but introspection only sees the names it
|
||||
was told to look for, and cannot see a field whose MEANING changed underneath a
|
||||
stable name. ``_VERIFIED_THROUGH`` covers exactly that blind spot by demanding
|
||||
a human read. Neither subsumes the other.
|
||||
|
||||
Offline-safe: reads installed package metadata and already-imported classes —
|
||||
no key, no network.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import importlib.metadata
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -23,6 +44,14 @@ _PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|||
_VERIFIED_FLOOR = (0, 2, 111)
|
||||
_VERIFIED_CEILING = (0, 3)
|
||||
|
||||
# The newest build whose SOURCE was read for the premises below — question (b).
|
||||
# Raising this is a claim that someone opened the package and checked, so it
|
||||
# moves ONLY together with that reading. 0.2.139 read 2026-08-18: the public
|
||||
# ``query.py`` is byte-identical to 0.2.120, every premise field is present with
|
||||
# an unchanged type and default, and the parser changes are purely additive
|
||||
# (a new ``origin`` passthrough, a new ``ConversationResetMessage``).
|
||||
_VERIFIED_THROUGH = (0, 2, 139)
|
||||
|
||||
|
||||
def _pin_for(floor: tuple[int, ...], ceiling: tuple[int, ...]) -> str:
|
||||
"""The pyproject requirement string the verified range implies.
|
||||
|
|
@ -38,16 +67,39 @@ def _pin_for(floor: tuple[int, ...], ceiling: tuple[int, ...]) -> str:
|
|||
|
||||
_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",
|
||||
# The sdk_client.py attribute premises, as a table the suite can CHECK rather
|
||||
# than a sentence it can only print. Every entry is read by sdk_client.py; the
|
||||
# inventory below asserts each one against the installed package, so a premise
|
||||
# that quietly stopped being true fails HERE instead of on the one live run.
|
||||
_SDK_SURFACE: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("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",)),
|
||||
)
|
||||
|
||||
# The premise introspection is STRUCTURALLY unable to see: an ordering fact
|
||||
# about a stream, not an attribute on a class. It is named separately rather
|
||||
# than dropped, because the honest reason the version ratchet still exists is
|
||||
# that this line can only be checked by a human reading the source.
|
||||
_SEMANTIC_PREMISES = ("query() yields AssistantMessage then a closing ResultMessage",)
|
||||
|
||||
|
||||
def _premise_text(surface: tuple[tuple[str, tuple[str, ...]], ...]) -> tuple[str, ...]:
|
||||
"""The prose the failure message prints — DERIVED from the checked table.
|
||||
|
||||
Not a second hand-maintained copy: an attribute added to the table appears in
|
||||
the operator-facing message for free, and one removed cannot linger there
|
||||
claiming a premise nobody verifies any more (value-proof below).
|
||||
"""
|
||||
return tuple(f"{cls}." + "/.".join(attrs) for cls, attrs in surface) + _SEMANTIC_PREMISES
|
||||
|
||||
|
||||
_SDK_PREMISES = _premise_text(_SDK_SURFACE)
|
||||
|
||||
|
||||
def _parse(raw: str) -> tuple[int, ...]:
|
||||
return tuple(int(part) for part in raw.split(".")[:3])
|
||||
|
|
@ -83,6 +135,33 @@ def _pin_is_declared(text: str, pin: str) -> bool:
|
|||
return f'"{pin}"' in body
|
||||
|
||||
|
||||
def missing_sdk_attributes(
|
||||
surface: tuple[tuple[str, tuple[str, ...]], ...] = _SDK_SURFACE,
|
||||
) -> tuple[str, ...]:
|
||||
"""Premises the INSTALLED build fails to satisfy — introspected, never assumed.
|
||||
|
||||
Fail-closed on three distinct absences, because each would otherwise read as
|
||||
"no gaps found": the class is gone from the package, the class is no longer a
|
||||
dataclass (so its fields are not what this check knows how to read), or the
|
||||
attribute is missing. Returning () must mean "checked and clean", never
|
||||
"could not look" — that conflation is the whole defect class.
|
||||
"""
|
||||
import claude_agent_sdk
|
||||
|
||||
gaps: list[str] = []
|
||||
for cls_name, attrs in surface:
|
||||
cls = getattr(claude_agent_sdk, cls_name, None)
|
||||
if cls is None:
|
||||
gaps.append(f"{cls_name}: class absent from claude_agent_sdk")
|
||||
continue
|
||||
if not dataclasses.is_dataclass(cls):
|
||||
gaps.append(f"{cls_name}: no longer a dataclass — fields unreadable")
|
||||
continue
|
||||
declared = {field.name for field in dataclasses.fields(cls)}
|
||||
gaps.extend(f"{cls_name}.{attr}" for attr in attrs if attr not in declared)
|
||||
return tuple(gaps)
|
||||
|
||||
|
||||
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")
|
||||
|
|
@ -94,6 +173,19 @@ def check_sdk_version() -> str:
|
|||
"BEFORE widening the pin (pyproject.toml + this guard together): "
|
||||
+ "; ".join(_SDK_PREMISES)
|
||||
)
|
||||
if _parse(raw) > _VERIFIED_THROUGH:
|
||||
# Question (b), the one the pin cannot answer: this build is PERMITTED
|
||||
# and unread. Green here would mean `uv sync --upgrade` silently retires
|
||||
# the verification that justified the pin in the first place.
|
||||
through = ".".join(str(part) for part in _VERIFIED_THROUGH)
|
||||
raise AssertionError(
|
||||
f"claude-agent-sdk {raw} satisfies the pin "
|
||||
f"{_PIN.removeprefix('claude-agent-sdk')} but is NEWER than the last build "
|
||||
f"read at source ({through}). The pin permits it; nobody has verified it. "
|
||||
"Read the new version's source, then raise _VERIFIED_THROUGH in the SAME "
|
||||
"commit that installs it — these sdk_client.py premises are what to re-check: "
|
||||
+ "; ".join(_SDK_PREMISES)
|
||||
)
|
||||
return raw
|
||||
|
||||
|
||||
|
|
@ -122,11 +214,114 @@ class TestSdkVersionGuard:
|
|||
with pytest.raises(AssertionError):
|
||||
check_sdk_version()
|
||||
|
||||
def test_a_version_inside_the_pin_but_beyond_the_verified_trips_the_guard(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# THE HOLE THIS RATCHET CLOSES (measured 2026-08-09): 0.2.140 satisfies
|
||||
# the pin, so `uv sync --upgrade` installs it without a word, and before
|
||||
# _VERIFIED_THROUGH existed the whole suite stayed GREEN on a build whose
|
||||
# premises nobody had read. A permission is not a proof.
|
||||
monkeypatch.setattr(importlib.metadata, "version", lambda name: "0.2.140")
|
||||
with pytest.raises(AssertionError) as err:
|
||||
check_sdk_version()
|
||||
message = str(err.value)
|
||||
assert "sdk_client.py" in message
|
||||
# The message must separate the two questions, or the operator re-reads
|
||||
# the pin — the thing that was never wrong — instead of the source.
|
||||
assert "satisfies the pin" in message
|
||||
assert "NEWER than the last build read at source" in message
|
||||
for premise in _SDK_PREMISES:
|
||||
assert premise in message
|
||||
|
||||
def test_the_build_read_at_source_itself_passes(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# BOUNDARY CONTROL: a ratchet that rejected everything would pass the test
|
||||
# above for the wrong reason. The verified build itself must be accepted.
|
||||
through = ".".join(str(part) for part in _VERIFIED_THROUGH)
|
||||
monkeypatch.setattr(importlib.metadata, "version", lambda name: through)
|
||||
assert check_sdk_version() == through
|
||||
|
||||
def test_the_verified_reading_lies_inside_the_pin_it_justifies(self) -> None:
|
||||
# A reading outside the installable range would vouch for a build uv can
|
||||
# never resolve — the ratchet would be green and inert.
|
||||
assert _VERIFIED_FLOOR <= _VERIFIED_THROUGH < _VERIFIED_CEILING
|
||||
|
||||
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 TestTheSdkSurfaceInventory:
|
||||
"""The premise table, checked against the INSTALLED package (§11).
|
||||
|
||||
Until now the premises existed only as a sentence printed on failure — prose
|
||||
the suite could recite but never test. Any of them could have stopped being
|
||||
true and nothing would have gone red until the one live run (S10, D6) spent
|
||||
real money to find out.
|
||||
"""
|
||||
|
||||
def test_every_premise_holds_on_the_installed_build(self) -> None:
|
||||
assert missing_sdk_attributes() == ()
|
||||
|
||||
def test_the_table_is_not_empty_of_the_things_it_claims_to_check(self) -> None:
|
||||
# POSITIVE CONTROL, ahead of the negative: "no gaps" is also what an
|
||||
# EMPTY table returns. Without this, the assertion above would hold just
|
||||
# as well for a premise list somebody had quietly deleted.
|
||||
assert len(_SDK_SURFACE) == 4
|
||||
assert sum(len(attrs) for _, attrs in _SDK_SURFACE) == 15
|
||||
|
||||
def test_every_checked_attribute_is_named_in_the_operator_facing_message(self) -> None:
|
||||
printed = "; ".join(_SDK_PREMISES)
|
||||
for cls_name, attrs in _SDK_SURFACE:
|
||||
for attr in attrs:
|
||||
assert attr in printed, f"{cls_name}.{attr} is checked but never reported"
|
||||
|
||||
|
||||
class TestTheInventoryGoesRedWhenTheSurfaceMoves:
|
||||
"""LOAD-BEARING (§11): prove the inventory can SEE absence, on a mutated COPY.
|
||||
|
||||
A checker that reports no gaps is making a claim about the package; it is
|
||||
indistinguishable from a checker that cannot look. Each proof below breaks the
|
||||
surface deliberately and requires the specific gap to be named — mirroring the
|
||||
known-positive discipline the version ratchet above already follows.
|
||||
"""
|
||||
|
||||
def test_red_when_a_premise_names_an_attribute_the_build_lacks(self) -> None:
|
||||
gaps = missing_sdk_attributes((("ResultMessage", ("total_cost_usd", "no_such_field")),))
|
||||
# Not merely "non-empty": the REAL attribute must survive as satisfied and
|
||||
# only the fabricated one be reported, or a checker that flagged everything
|
||||
# would pass this too.
|
||||
assert gaps == ("ResultMessage.no_such_field",)
|
||||
|
||||
def test_red_when_the_class_disappears_from_the_package(self) -> None:
|
||||
gaps = missing_sdk_attributes((("NoSuchMessage", ("text",)),))
|
||||
assert gaps == ("NoSuchMessage: class absent from claude_agent_sdk",)
|
||||
|
||||
def test_red_when_the_checked_name_is_not_a_dataclass(self) -> None:
|
||||
# `query` is a REAL export of the package — a genuine known-positive, not a
|
||||
# fabricated stand-in. It is a function, so its fields are not readable the
|
||||
# way this check reads fields, and the honest answer is a reported gap
|
||||
# rather than a silent () from a `dataclasses.fields` TypeError.
|
||||
import claude_agent_sdk
|
||||
|
||||
assert hasattr(claude_agent_sdk, "query"), "control is broken: the export is gone"
|
||||
gaps = missing_sdk_attributes((("query", ("text",)),))
|
||||
assert gaps == ("query: no longer a dataclass — fields unreadable",)
|
||||
|
||||
def test_red_when_the_prose_stops_covering_the_table(self) -> None:
|
||||
# VALUE-proof on the derivation: an attribute added to the table appears in
|
||||
# the operator-facing message WITHOUT a second edit. Were the prose a hand-
|
||||
# maintained copy, this new attribute would be checked and never reported —
|
||||
# the exact drift that made the old sentence outlive its verification.
|
||||
widened = _premise_text((("ResultMessage", ("usage", "a_newly_relied_on_field")),))
|
||||
assert "a_newly_relied_on_field" in "; ".join(widened)
|
||||
assert "a_newly_relied_on_field" not in "; ".join(_SDK_PREMISES)
|
||||
|
||||
def test_the_semantic_premise_survives_derivation(self) -> None:
|
||||
# The stream-ordering premise has no attribute to introspect, so nothing
|
||||
# would go red if it silently dropped out of the derived prose.
|
||||
assert _SEMANTIC_PREMISES[0] in "; ".join(_SDK_PREMISES)
|
||||
|
||||
|
||||
class TestTheSeamGoesRedWhenDetached:
|
||||
"""LOAD-BEARING (§11): the assertions above must FAIL on a detached pyproject.
|
||||
|
||||
|
|
|
|||
14
uv.lock
generated
14
uv.lock
generated
|
|
@ -177,7 +177,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "claude-agent-sdk"
|
||||
version = "0.2.120"
|
||||
version = "0.2.139"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -185,13 +185,13 @@ dependencies = [
|
|||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/7f/7b69aed292a4edecae132e4dbe6b6decb4e88ec142fc91d117b19058c9e0/claude_agent_sdk-0.2.120.tar.gz", hash = "sha256:e428552f79a76e0d85789369eeb58249b33f350200124e5fc86b24168bd00805", size = 268639, upload-time = "2026-07-15T23:18:50.997Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/b6/cfcdefed1f866a8ba372ef3884c8020dd54338d15d8b45d5a1ff7432cea1/claude_agent_sdk-0.2.139.tar.gz", hash = "sha256:4395ed541cdd4c13aeb1213b3b414b7e8a94cc060a773137e961882e81c174a7", size = 319519, upload-time = "2026-08-14T22:34:48.038Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/85/5e8958704db0f8195e63f8ec4a80c5fb14756edc785bb3535e0dc5d91104/claude_agent_sdk-0.2.120-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ead9fb4bdaf70069978703ec6d74b30bd269e9632a4aea4dc8c4e999ac3a1b", size = 71110966, upload-time = "2026-07-15T23:18:54.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/53/c6cdad82ac100c8a45887999614e9fe206b77b43790dacc6de42b156ad4c/claude_agent_sdk-0.2.120-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:1248591c7bffeb6e10e8cd169e0766854951fba8816e3e7d81a003f8bfca6f08", size = 76067995, upload-time = "2026-07-15T23:18:58.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f8/248e3f58d0f0aa7d76bd34b11b18135cc124f5b9a9b55219cc1ca03d662a/claude_agent_sdk-0.2.120-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:abc73ccdf3decca566cd18084e74bc2f2d10b8b77cc1fd5ed4299d5c15e5078b", size = 81027434, upload-time = "2026-07-15T23:19:03.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/59/6adb0c53534646f1d5ddc41226ff37b2adc413a472e2f87a9011548a137f/claude_agent_sdk-0.2.120-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:888070c246c92e102c52001d26532cd3646a700656d7c368f2f91a1d3c16b534", size = 82084704, upload-time = "2026-07-15T23:19:08.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/06/036b8dce1e86ecd5e2e1ddc281736cdb33bad6d24e748b9553e235b028fe/claude_agent_sdk-0.2.120-py3-none-win_amd64.whl", hash = "sha256:bc1441c94f60c9e7b4b8c641742fedf68451f573062ef69dd43d86a61b1fb219", size = 81958518, upload-time = "2026-07-15T23:19:13.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/7f/f04c33553cbc69bb96d045dc38a6266726fad72130f22f405dfe9eb54bf1/claude_agent_sdk-0.2.139-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbc50cc475ec633cabfa36347646097e9b1466d53130e4a04a87308ff830c87b", size = 88043656, upload-time = "2026-08-14T22:34:53.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d0/a17f5318ca0220479f20fdf83fa54a838a0a13ee203495ff67c72c3f43a7/claude_agent_sdk-0.2.139-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:1c08206b1603444582cd365effaf95d2a8248661f1492281fb2d529b0887c047", size = 93000433, upload-time = "2026-08-14T22:34:58.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/2e/5bcec31700d76ad2d5b9fc28521a75a66f464063dac11373cf8d61446a4f/claude_agent_sdk-0.2.139-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:e69ae1a0b2af684c64839cc16e10b70800d9d2f57622b8c0d1739dd878cd7346", size = 97396659, upload-time = "2026-08-14T22:35:03.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/7f/582b3c1936c9f4ebc1bdc55a3923f1b680ef3c01928ffff1ea38eb84f637/claude_agent_sdk-0.2.139-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:34b289b3436fe24013f7b9cfe9f0a4e0806917a9ef8bbe829cda9a7b12d41a77", size = 98391889, upload-time = "2026-08-14T22:35:09.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/54/d94af31d19b4e8d63d1b15002fd333ea77a040b7ab7a388044e511c8f9f6/claude_agent_sdk-0.2.139-py3-none-win_amd64.whl", hash = "sha256:9b76f0ffe216d6ca290d5f4f295ecb030dc496f101986ac99480a89d4abc6426", size = 100746507, upload-time = "2026-08-14T22:35:15.144Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue