diff --git a/workspace/main.py b/workspace/main.py index 5c09b134..85e891e2 100644 --- a/workspace/main.py +++ b/workspace/main.py @@ -437,13 +437,23 @@ async def main(): # pragma: no cover ) async def _send_initial_prompt(): """Wait for server to be ready, then send initial_prompt as self-message.""" - # Wait for the A2A server to accept connections + # Wait for the A2A server to accept connections. + # Use the SDK's own constant for the well-known path so this + # probe and the route mounted by create_agent_card_routes() + # never drift apart. Pre-fix this hardcoded the pre-1.x + # well-known path string; a2a-sdk 1.x renamed it (the + # canonical value lives in a2a.utils.constants now), so + # the probe got 404 every attempt and fell through to + # "server not ready after 30s, skipping" even though the + # server was actually serving fine. Net effect: every + # workspace silently dropped its `initial_prompt`. + from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH ready = False for attempt in range(30): await asyncio.sleep(1) try: async with httpx.AsyncClient(timeout=5.0) as client: - resp = await client.get(f"http://127.0.0.1:{port}/.well-known/agent.json") + resp = await client.get(f"http://127.0.0.1:{port}{AGENT_CARD_WELL_KNOWN_PATH}") if resp.status_code == 200: ready = True break diff --git a/workspace/tests/test_agent_card_well_known_path.py b/workspace/tests/test_agent_card_well_known_path.py new file mode 100644 index 00000000..fe06c9fd --- /dev/null +++ b/workspace/tests/test_agent_card_well_known_path.py @@ -0,0 +1,84 @@ +"""Pin the agent-card readiness probe to the SDK's canonical path. + +main.py's _send_initial_prompt() polls the local A2A server's +well-known agent-card URL to know when it's safe to send the initial +prompt as a self-message. Pre-fix the URL was hardcoded to the pre-1.x +literal; a2a-sdk 1.x renamed the well-known path (the canonical value +lives in `a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH`), so the +probe got 404 every attempt and silently fell through to "server not +ready after 30s, skipping" — dropping every workspace's +`initial_prompt` from config.yaml. + +The fix is to import the SDK's `AGENT_CARD_WELL_KNOWN_PATH` constant +and use it directly in the probe URL. These tests pin the static +invariants of that fix: + + 1. No hardcoded `/.well-known/agent.json` literal anywhere in + main.py (catches a future contributor reverting to a literal). + 2. The probe URL fstring interpolates `AGENT_CARD_WELL_KNOWN_PATH` + (catches a "fix" that imports the constant for show but still + uses a literal in the actual GET). + +Note: we deliberately do not assert the constant's value or compare +it against `create_agent_card_routes()` here. The runtime SDK is +mocked in this directory's conftest for the executor-test path, so +any test that imports the real `a2a.utils.constants` would either +collide with the mock or require running in a separate pytest session. +The two static invariants are sufficient: by always following whatever +the SDK constant says, we travel through any rename automatically. The +SDK's own contract that `create_agent_card_routes` mounts at the +constant's value is the SDK's responsibility, not ours. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +WORKSPACE_ROOT = Path(__file__).resolve().parents[1] + + +def test_main_uses_sdk_constant_for_agent_card_probe(): + """No hardcoded `/.well-known/agent.json` literal anywhere in main.py. + + The SDK constant (AGENT_CARD_WELL_KNOWN_PATH) is the single source + of truth — string-literal probes drift the moment the SDK renames. + """ + main = (WORKSPACE_ROOT / "main.py").read_text() + + bad_literal = "/.well-known/agent.json" + offenders = [ + (lineno, line) + for lineno, line in enumerate(main.splitlines(), 1) + if bad_literal in line + ] + assert not offenders, ( + f"Found pre-1.x literal {bad_literal!r} in main.py — must use " + f"the SDK's AGENT_CARD_WELL_KNOWN_PATH constant instead. " + f"Offending lines: {offenders}" + ) + + assert ( + "AGENT_CARD_WELL_KNOWN_PATH" in main + ), "main.py must import a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH" + + +def test_probe_loop_uses_constant_in_url_format(): + """Spot-check that the URL fstring in main.py interpolates the + constant, not a literal. Catches a future "fix" that imports the + constant for show but still uses a literal in the actual GET.""" + main = (WORKSPACE_ROOT / "main.py").read_text() + + # The probe pattern: `client.get(f"http://127.0.0.1:{port}{...}")` + # where `{...}` must be `{AGENT_CARD_WELL_KNOWN_PATH}`, not a + # hardcoded path. + pattern = re.compile( + r'client\.get\(f"http://127\.0\.0\.1:\{port\}\{(?P[^}]+)\}"\)' + ) + matches = pattern.findall(main) + assert matches, "no readiness probe pattern found in main.py" + for expr in matches: + assert "AGENT_CARD_WELL_KNOWN_PATH" in expr, ( + f"readiness probe URL uses {expr!r} instead of " + f"AGENT_CARD_WELL_KNOWN_PATH" + )