|
| 1 | +"""Regression test: gen_ui_agent must import/construct without raising. |
| 2 | +
|
| 3 | +Reproduces the crash-on-import that took down `showcase-ag2` staging |
| 4 | +deploys since 76874f06d. Importing `agents.gen_ui_agent` constructs the |
| 5 | +`ConversableAgent` at module scope, which registers `set_steps` as an LLM |
| 6 | +tool. AG2 runs every tool parameter through pydantic |
| 7 | +`TypeAdapter(param).json_schema()`. When `set_steps` carries |
| 8 | +`from __future__ import annotations`, its `context_variables: ContextVariables` |
| 9 | +parameter is seen as an unresolved `ForwardRef('ContextVariables')`, and |
| 10 | +schema generation raises `pydantic.errors.PydanticUserError` at import time |
| 11 | +-> importing this agent module aborts server startup -> the service never |
| 12 | +comes up -> the Railway healthcheck (`/health`, owned by the integration's |
| 13 | +top-level entrypoint, not this module) fails. |
| 14 | +
|
| 15 | +The success criterion mirrors the failed healthcheck: the module imports |
| 16 | +cleanly and the agent object (with its registered tools) is built. |
| 17 | +""" |
| 18 | + |
| 19 | +import importlib |
| 20 | +import os |
| 21 | +from pathlib import Path |
| 22 | + |
| 23 | +import pytest |
| 24 | + |
| 25 | + |
| 26 | +# ConversableAgent construction validates that an LLM config / key is |
| 27 | +# present. The crash we are guarding against happens during tool-schema |
| 28 | +# generation, which is reached only after that validation, so seed a dummy |
| 29 | +# key. No network call is made at import time. |
| 30 | +os.environ.setdefault("OPENAI_API_KEY", "test-key-not-used") |
| 31 | + |
| 32 | + |
| 33 | +def _gen_ui_agent_source_path() -> Path: |
| 34 | + """Resolve src/agents/gen_ui_agent.py relative to this test file. |
| 35 | +
|
| 36 | + This test lives at <integration>/tests/python/, so the integration root |
| 37 | + is two levels up; the source then sits under src/agents/. Resolving via |
| 38 | + __file__ keeps the guard working regardless of where the repo is checked |
| 39 | + out (no hardcoded absolute path). |
| 40 | + """ |
| 41 | + integration_root = Path(__file__).resolve().parents[2] |
| 42 | + return integration_root / "src" / "agents" / "gen_ui_agent.py" |
| 43 | + |
| 44 | + |
| 45 | +def test_gen_ui_agent_source_has_no_future_annotations(): |
| 46 | + """The source must NOT carry `from __future__ import annotations`. |
| 47 | +
|
| 48 | + This is the load-bearing regression pin. `from __future__ import |
| 49 | + annotations` makes Python stringify every annotation (PEP 563), so the |
| 50 | + `set_steps` tool's `context_variables: ContextVariables` parameter is seen |
| 51 | + by AG2 as an unresolved `ForwardRef('ContextVariables')`. AG2 runs each |
| 52 | + tool parameter through pydantic `TypeAdapter(param).json_schema()`, which |
| 53 | + then raises `PydanticUserError` at import time -> importing this agent |
| 54 | + module aborts server startup -> the service never comes up -> the |
| 55 | + Railway/staging healthcheck (`/health`, owned by the entrypoint, not this |
| 56 | + module) fails (regression 76874f06d). |
| 57 | +
|
| 58 | + Asserting on the source text (not just "import didn't crash") makes this a |
| 59 | + TRUE guard: it fails the instant someone re-adds the future-import, even if |
| 60 | + a future AG2/pydantic happens to resolve the forward-ref gracefully and the |
| 61 | + import would no longer crash on its own. |
| 62 | + """ |
| 63 | + source_path = _gen_ui_agent_source_path() |
| 64 | + assert source_path.is_file(), f"could not locate source at {source_path}" |
| 65 | + |
| 66 | + offending = "from __future__ import annotations" |
| 67 | + source_lines = source_path.read_text(encoding="utf-8").splitlines() |
| 68 | + matches = [ |
| 69 | + f" line {i}: {line!r}" |
| 70 | + for i, line in enumerate(source_lines, start=1) |
| 71 | + if line.strip() == offending |
| 72 | + ] |
| 73 | + assert not matches, ( |
| 74 | + f"{source_path} must NOT contain `{offending}`.\n" |
| 75 | + "PEP 563 stringifies annotations, turning the set_steps tool's " |
| 76 | + "`context_variables: ContextVariables` into an unresolved ForwardRef; " |
| 77 | + "AG2's pydantic tool-schema generation then raises PydanticUserError " |
| 78 | + "at import time, so importing this agent module aborts server startup " |
| 79 | + "and the service never comes up, failing the Railway healthcheck " |
| 80 | + "(`/health`, owned by the entrypoint, not this module) (regression " |
| 81 | + "76874f06d). Remove the future-import.\nFound at:\n" + "\n".join(matches) |
| 82 | + ) |
| 83 | + |
| 84 | + |
| 85 | +def test_gen_ui_agent_imports_without_pydantic_error(): |
| 86 | + """Importing the module must not raise PydanticUserError (or anything). |
| 87 | +
|
| 88 | + Proves the crash path is actually clear on the *installed* AG2/pydantic |
| 89 | + (complements the static source guard above, which is version-independent). |
| 90 | + """ |
| 91 | + try: |
| 92 | + module = importlib.import_module("agents.gen_ui_agent") |
| 93 | + except Exception: # noqa: BLE001 - re-raise so the verbatim traceback is kept |
| 94 | + # Let the original exception propagate with its full traceback intact: |
| 95 | + # for a deep pydantic schema-gen crash the verbatim stack IS the |
| 96 | + # load-bearing diagnostic, which a re-wrapped pytest.fail string loses. |
| 97 | + pytest.fail( |
| 98 | + "gen_ui_agent failed to import (see traceback below)", |
| 99 | + pytrace=True, |
| 100 | + ) |
| 101 | + |
| 102 | + # The agent and its ASGI app must be constructed (i.e. tool registration, |
| 103 | + # where the crash occurred, completed). |
| 104 | + assert module.agent is not None |
| 105 | + assert module.gen_ui_agent_app is not None |
| 106 | + # set_steps must be registered as a tool on the agent. |
| 107 | + tool_names = {t.name for t in module.agent.tools} |
| 108 | + assert "set_steps" in tool_names |
0 commit comments