Skip to content

Commit 80f0277

Browse files
GWealecopybara-github
authored andcommitted
test: add a load smoke test for contributing samples
Add test_sample_loads, which loads and constructs the root agent of every sample under contributing/samples the way `adk run` does. Samples needing external infra, credentials, or optional dependencies are skipped with a reason; three samples already broken against the current API are marked xfail. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 944102624
1 parent 50ff37f commit 80f0277

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

tests/unittests/test_samples.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,35 @@
1515
from __future__ import annotations
1616

1717
import json
18+
import os
1819
from pathlib import Path
20+
import sys
1921

22+
from google.adk.agents import config_agent_utils
2023
from google.adk.apps.app import App
2124
from google.adk.cli.agent_test_runner import test_agent_replay as _test_agent_replay
25+
from google.adk.cli.utils.agent_loader import AgentLoader
2226
from google.genai import types
2327
import pytest
2428

2529
CONTRIBUTING_DIR = Path(__file__).parent.parent.parent / "contributing"
30+
SAMPLES_DIR = CONTRIBUTING_DIR / "samples"
31+
32+
33+
@pytest.fixture(autouse=True)
34+
def _load_samples_like_adk_run():
35+
"""Loads samples with the YAML key denylist off, matching `adk run`.
36+
37+
The denylist is a hosted-web-server guard that fast_api enables globally and
38+
never resets, so a fast_api test earlier in the process would otherwise leave
39+
it on and block valid config samples here.
40+
"""
41+
saved = config_agent_utils._ENFORCE_YAML_KEY_DENYLIST
42+
config_agent_utils._set_enforce_yaml_key_denylist(False)
43+
try:
44+
yield
45+
finally:
46+
config_agent_utils._set_enforce_yaml_key_denylist(saved)
2647

2748

2849
def get_test_files():
@@ -57,3 +78,154 @@ def get_test_files():
5778
def test_sample(sample_dir: Path, test_file: Path, monkeypatch):
5879
"""Tests a sample by replaying exported session events."""
5980
_test_agent_replay(sample_dir, test_file, monkeypatch)
81+
82+
83+
# Samples that cannot be loaded offline: they reach an external service, need an
84+
# optional dependency outside [all], or are not an independently loadable root.
85+
SKIP_LOAD = {
86+
"integrations/agent_registry_agent": "calls Agent Registry API at import",
87+
"integrations/api_registry_agent": "calls Cloud API Registry at import",
88+
"integrations/application_integration_agent": (
89+
"calls Integration Connectors API at import"
90+
),
91+
"integrations/integration_connector_euc_agent": (
92+
"calls Integration Connectors API at import"
93+
),
94+
"multimodal/static_non_text_content": (
95+
"uploads a file via the genai API at import"
96+
),
97+
"integrations/authn-adk-all-in-one/adk_agents/agent_openapi_tools": (
98+
"needs a local identity provider server on :5000"
99+
),
100+
"mcp/mcp_postgres_agent": (
101+
"needs POSTGRES_CONNECTION_STRING and a postgres server"
102+
),
103+
"code_execution/custom_code_execution": (
104+
"provisions a Vertex code-interpreter extension at import"
105+
),
106+
"code_execution/vertex_code_execution": (
107+
"provisions a Vertex code-interpreter extension at import"
108+
),
109+
"integrations/crewai_tool_kwargs": (
110+
"needs the crewai package (not installed on every Python version)"
111+
),
112+
"integrations/files_retrieval_agent": (
113+
"needs the llama-index-embeddings-google-genai package"
114+
),
115+
"integrations/toolbox_agent": (
116+
"needs the toolbox-adk package and a toolbox server"
117+
),
118+
"multimodal/computer_use": "needs the playwright package",
119+
"integrations/gepa": "experiment package, exposes no root_agent",
120+
"integrations/slack_agent": (
121+
"builds its agent inside main(), no module-level root_agent"
122+
),
123+
"adk_team/adk_documentation": (
124+
"package dir; its child agents are the samples"
125+
),
126+
"adk_team/adk_answering_agent/gemini_assistant": (
127+
"sub-agent of adk_answering_agent, not independently loadable"
128+
),
129+
"integrations/oauth_calendar_agent": (
130+
"fetches the Calendar API (calendar v3) discovery doc at import"
131+
),
132+
}
133+
134+
# Samples whose own code is currently broken against the ADK API. Loading them
135+
# fails today; remove the entry once the sample is fixed.
136+
XFAIL_LOAD = {
137+
"integrations/jira_agent": (
138+
"ApplicationIntegrationToolset no longer accepts tool_name"
139+
),
140+
"workflows/loop_config": (
141+
"root_agent.yaml references the nonexistent agent_class Workflow"
142+
),
143+
"models/hello_world_litellm_add_function_to_prompt": (
144+
"langchain_core requires an explicit import of langchain_core.tools"
145+
),
146+
"adk_team/adk_triaging_agent": (
147+
"agent.py imports adk_triaging_agent.settings, which is not present"
148+
),
149+
}
150+
151+
_DUMMY_ENV = {
152+
"GOOGLE_API_KEY": "dummy-key",
153+
"GEMINI_API_KEY": "dummy-key",
154+
"GOOGLE_CLOUD_PROJECT": "dummy-project",
155+
"GOOGLE_CLOUD_LOCATION": "us-central1",
156+
"OPENAI_API_KEY": "dummy-key",
157+
"ANTHROPIC_API_KEY": "dummy-key",
158+
"GITHUB_TOKEN": "dummy-token",
159+
"VERTEXAI_DATASTORE_ID": "dummy-datastore",
160+
}
161+
162+
163+
def get_sample_dirs():
164+
"""Yields a pytest param per loadable sample directory."""
165+
if not SAMPLES_DIR.exists():
166+
return
167+
sample_dirs = []
168+
for dirpath, dirnames, filenames in os.walk(SAMPLES_DIR):
169+
path = Path(dirpath)
170+
if path.name == "tests":
171+
dirnames[:] = []
172+
continue
173+
if any(
174+
f in filenames for f in ("agent.py", "__init__.py", "root_agent.yaml")
175+
):
176+
sample_dirs.append(path)
177+
for sample_dir in sorted(sample_dirs):
178+
rel = sample_dir.relative_to(SAMPLES_DIR).as_posix()
179+
if rel in SKIP_LOAD:
180+
marks = pytest.mark.skip(reason=SKIP_LOAD[rel])
181+
elif rel in XFAIL_LOAD:
182+
marks = pytest.mark.xfail(reason=XFAIL_LOAD[rel], strict=False)
183+
else:
184+
marks = ()
185+
yield pytest.param(sample_dir, id=rel, marks=marks)
186+
187+
188+
def _load_root_agent(sample_dir: Path):
189+
"""Loads a sample the way `adk run` does, isolating module side effects."""
190+
saved_modules = set(sys.modules)
191+
saved_path = list(sys.path)
192+
sys.path.insert(0, str(sample_dir.parent))
193+
try:
194+
loader = AgentLoader(str(sample_dir.parent))
195+
loader.remove_agent_from_cache(sample_dir.name)
196+
agent_or_app = loader.load_agent(sample_dir.name)
197+
return (
198+
agent_or_app.root_agent
199+
if isinstance(agent_or_app, App)
200+
else agent_or_app
201+
)
202+
finally:
203+
sys.path[:] = saved_path
204+
# Evict only the sample's own modules so the next sample reloads cleanly,
205+
# while leaving third-party libraries cached (re-importing libraries with
206+
# global registries, e.g. opentelemetry, breaks on reload).
207+
prefix = sample_dir.name
208+
for name in set(sys.modules) - saved_modules:
209+
module = sys.modules.get(name)
210+
file = getattr(module, "__file__", None)
211+
if (
212+
name == prefix
213+
or name.startswith(prefix + ".")
214+
or (
215+
file
216+
and Path(file).resolve().is_relative_to(SAMPLES_DIR.resolve())
217+
)
218+
):
219+
del sys.modules[name]
220+
221+
222+
@pytest.mark.parametrize("sample_dir", list(get_sample_dirs()))
223+
def test_sample_loads(sample_dir: Path, monkeypatch):
224+
"""Smoke test: every sample's agent imports and constructs a root agent."""
225+
for key, value in _DUMMY_ENV.items():
226+
monkeypatch.setenv(key, value)
227+
root_agent = _load_root_agent(sample_dir)
228+
assert root_agent is not None, f"{sample_dir} loaded no root agent"
229+
assert getattr(
230+
root_agent, "name", None
231+
), f"{sample_dir} root agent has no name"

0 commit comments

Comments
 (0)