Skip to content

Commit c5c581d

Browse files
bokelleyclaude
andauthored
fix(testing): make a2a_compat_shim resilient to wrong a2a-sdk in /tmp worktrees (#433)
* fix(testing): make a2a_compat_shim resilient to wrong a2a-sdk in /tmp worktrees Attribute assignments like `pb.Role.user = pb.Role.ROLE_USER` at module import time would raise AttributeError if a2a-sdk isn't at the pinned version (>=1.0.1,<1.0.2), propagating through conftest.py's top-level import and breaking pytest collection entirely. Agents running in fresh /tmp worktrees with uninitialized environments hit this on PRs #391, #406, #407. Two changes: - `a2a_compat_shim.py`: introduce `_proto_alias()` helper that guards each attribute alias independently with hasattr + a per-alias RuntimeWarning (includes install command) rather than letting AttributeError propagate. - `conftest.py`: wrap the shim import in try/except (ImportError|AttributeError) with a fallback to None; update the autouse fixture to no-op when the shim is unavailable, so collection always succeeds and only A2A tests fail. https://claude.ai/code/session_01AnL37fUet4e3yXt9YBxd7a * fix(testing): stacklevel=2 in _proto_alias + document _STATE_STRING_MAP asymmetry stacklevel=2 makes the per-alias warning point at the _proto_alias() call site in the module body (the useful diagnostic location) rather than at the warnings.warn() line inside the helper. Add a comment at _STATE_STRING_MAP explaining that any AttributeError from the dict literal is caught by conftest.py's import guard, so the different guard pattern is intentional and collection still succeeds. https://claude.ai/code/session_01AnL37fUet4e3yXt9YBxd7a --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d897b62 commit c5c581d

2 files changed

Lines changed: 51 additions & 13 deletions

File tree

tests/a2a_compat_shim.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from __future__ import annotations
2626

2727
import sys
28+
import warnings
2829
from typing import Any
2930

3031
from a2a import types as pb
@@ -62,26 +63,45 @@
6263
]
6364

6465

66+
def _proto_alias(cls: Any, src: str, dst: str) -> None:
67+
"""Set cls.dst = cls.src, warning independently per alias if src is absent.
68+
69+
Guarding each alias independently avoids partial-patch state: if one
70+
source attribute is missing the rest still land, and each missing
71+
attribute gets its own actionable warning rather than a group abort.
72+
"""
73+
if not hasattr(cls, src):
74+
warnings.warn(
75+
f"a2a_compat_shim: {cls.__name__}.{src} not found — "
76+
"verify a2a-sdk>=1.0.1,<1.0.2 is installed. "
77+
"Run: pip install 'a2a-sdk>=1.0.1,<1.0.2'. A2A tests may fail.",
78+
RuntimeWarning,
79+
stacklevel=2,
80+
)
81+
return
82+
setattr(cls, dst, getattr(cls, src))
83+
84+
6585
# --- Role enum backwards-compat aliases (attribute-level monkey-patch) ---
6686
# ``Role.user`` / ``Role.agent`` didn't exist on the proto enum; tests
6787
# referenced them verbatim. Adding them once here means every call site
6888
# (``role=Role.user``) keeps compiling without per-file edits.
69-
pb.Role.user = pb.Role.ROLE_USER # type: ignore[attr-defined]
70-
pb.Role.agent = pb.Role.ROLE_AGENT # type: ignore[attr-defined]
89+
_proto_alias(pb.Role, "ROLE_USER", "user")
90+
_proto_alias(pb.Role, "ROLE_AGENT", "agent")
7191

7292

7393
# --- TaskState backwards-compat aliases ---
7494
# Tests reference ``TaskState.working`` / ``TaskState.completed`` / etc.
7595
# Proto enums don't have these symbol-shaped attributes; shim them in.
76-
pb.TaskState.completed = pb.TaskState.TASK_STATE_COMPLETED # type: ignore[attr-defined]
77-
pb.TaskState.failed = pb.TaskState.TASK_STATE_FAILED # type: ignore[attr-defined]
78-
pb.TaskState.working = pb.TaskState.TASK_STATE_WORKING # type: ignore[attr-defined]
79-
pb.TaskState.submitted = pb.TaskState.TASK_STATE_SUBMITTED # type: ignore[attr-defined]
80-
pb.TaskState.input_required = pb.TaskState.TASK_STATE_INPUT_REQUIRED # type: ignore[attr-defined]
81-
pb.TaskState.auth_required = pb.TaskState.TASK_STATE_AUTH_REQUIRED # type: ignore[attr-defined]
82-
pb.TaskState.canceled = pb.TaskState.TASK_STATE_CANCELED # type: ignore[attr-defined]
83-
pb.TaskState.rejected = pb.TaskState.TASK_STATE_REJECTED # type: ignore[attr-defined]
84-
pb.TaskState.unknown = pb.TaskState.TASK_STATE_UNSPECIFIED # type: ignore[attr-defined]
96+
_proto_alias(pb.TaskState, "TASK_STATE_COMPLETED", "completed")
97+
_proto_alias(pb.TaskState, "TASK_STATE_FAILED", "failed")
98+
_proto_alias(pb.TaskState, "TASK_STATE_WORKING", "working")
99+
_proto_alias(pb.TaskState, "TASK_STATE_SUBMITTED", "submitted")
100+
_proto_alias(pb.TaskState, "TASK_STATE_INPUT_REQUIRED", "input_required")
101+
_proto_alias(pb.TaskState, "TASK_STATE_AUTH_REQUIRED", "auth_required")
102+
_proto_alias(pb.TaskState, "TASK_STATE_CANCELED", "canceled")
103+
_proto_alias(pb.TaskState, "TASK_STATE_REJECTED", "rejected")
104+
_proto_alias(pb.TaskState, "TASK_STATE_UNSPECIFIED", "unknown")
85105

86106

87107
Role = pb.Role
@@ -125,6 +145,12 @@ def Message( # noqa: N802 (0.3 fixture shim)
125145
return pb.Message(**kwargs)
126146

127147

148+
# Note: this dict accesses pb.TaskState.TASK_STATE_* directly (no _proto_alias guard).
149+
# Any AttributeError here propagates out of the module, but conftest.py's
150+
# `except (ImportError, AttributeError)` catches it and sets _a2a_compat_shim=None,
151+
# so collection still succeeds. _proto_alias guards only the setattr side-effects;
152+
# this dict is purely read-only at construction and is only reached when pb.TaskState
153+
# has the correct 1.0 shape.
128154
_STATE_STRING_MAP: dict[str, pb.TaskState.ValueType] = {
129155
"completed": pb.TaskState.TASK_STATE_COMPLETED,
130156
"failed": pb.TaskState.TASK_STATE_FAILED,

tests/conftest.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import warnings as _warnings
34
from pathlib import Path
45
from types import UnionType
56
from typing import Any
@@ -10,7 +11,18 @@
1011
# Import the a2a-sdk 1.0 compat shim early so monkey-patches like
1112
# ``Role.user = ROLE_USER`` and ``TaskStatus.__init__`` string coercion
1213
# land before any test module constructs those proto types.
13-
from tests import a2a_compat_shim as _a2a_compat_shim # noqa: F401
14+
# Guard with try/except so a missing or wrong-version a2a-sdk doesn't
15+
# break collection — only A2A tests that actually use the shim will fail.
16+
try:
17+
from tests import a2a_compat_shim as _a2a_compat_shim
18+
except (ImportError, AttributeError) as _shim_exc:
19+
_warnings.warn(
20+
f"a2a_compat_shim unavailable ({_shim_exc}); "
21+
"run: pip install 'a2a-sdk>=1.0.1,<1.0.2'. A2A tests may fail.",
22+
RuntimeWarning,
23+
stacklevel=1,
24+
)
25+
_a2a_compat_shim = None # type: ignore[assignment]
1426

1527
_INTEGRATION_DIR = (Path(__file__).parent / "integration").resolve()
1628

@@ -48,7 +60,7 @@ def _a2a_compat_send_and_aggregate(
4860
a real a2a-sdk server and must NOT be shimmed — they rely on the
4961
genuine async-generator contract.
5062
"""
51-
if _is_integration_test(request):
63+
if _a2a_compat_shim is None or _is_integration_test(request):
5264
return
5365
_a2a_compat_shim.patch_send_and_aggregate(monkeypatch)
5466

0 commit comments

Comments
 (0)