Skip to content

Commit 68f55f0

Browse files
author
Santosh Kumar Radha
committed
Refactor functional tests around reusable agents
1 parent db63730 commit 68f55f0

9 files changed

Lines changed: 365 additions & 323 deletions

File tree

tests/functional/README.md

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,29 @@ This test suite runs end-to-end functional tests in an isolated Docker environme
3535

3636
```
3737
tests/functional/
38+
├── agents/ # Reusable agent definitions
39+
│ ├── __init__.py
40+
│ └── quick_start_agent.py
3841
├── docker/
3942
│ ├── docker compose.local.yml # SQLite mode (fast)
4043
│ ├── docker compose.postgres.yml # PostgreSQL mode (production-like)
4144
│ ├── Dockerfile.test-runner # Test execution container
4245
│ ├── agentfield-test.yaml # Control plane configuration
4346
│ └── wait-for-services.sh # Health check script
44-
├── conftest.py # Pytest fixtures
45-
├── test_hello_world.py # Example functional test
47+
├── tests/
48+
│ ├── test_hello_world.py # Hello World functional test
49+
│ └── test_quick_start.py # README Quick Start validation
50+
├── utils/
51+
│ ├── __init__.py
52+
│ └── agent_server.py # Shared run-agent helper
53+
├── conftest.py # Pytest fixtures
4654
├── requirements.txt # Test dependencies
4755
├── .env.example # Environment template
4856
└── README.md # This file
4957
```
5058

59+
The `agents/` directory stores normal-looking AgentField nodes (complete with `if __name__ == "__main__"` hooks) that tests can import and run. Shared helpers such as the `run_agent_server` async context manager live in `utils/` so every test can start/stop agents the same way.
60+
5161
### Test Flow
5262

5363
1. Docker Compose starts the control plane (and PostgreSQL if needed)
@@ -176,46 +186,39 @@ make test-functional-local
176186

177187
## 🧪 Writing Tests
178188

189+
### Reusable Agent Nodes
190+
191+
- Put canonical agent implementations in `agents/<name>_agent.py`. Each module should expose a `create_agent(openrouter_config, **kwargs)` helper (see `agents/quick_start_agent.py`).
192+
- Tests import those helpers, instantiate the agent (exactly like production code), and then spin it up with `utils.agent_server.run_agent_server`.
193+
- Agent modules can also be executed directly (`python -m agents.quick_start_agent`) because they expose a `create_agent_from_env` helper.
194+
179195
### Basic Structure
180196

181197
```python
182198
import pytest
183-
from agentfield import Agent
199+
from agents.my_agent import create_agent
200+
from utils.agent_server import run_agent_server
184201

185202
@pytest.mark.functional
186203
@pytest.mark.openrouter
187204
@pytest.mark.asyncio
188205
async def test_my_feature(
189-
make_test_agent,
190-
openrouter_config, # ALWAYS use this fixture - NEVER hardcode models
206+
openrouter_config,
191207
async_http_client,
192208
):
193-
# Create agent with OpenRouter config (uses OPENROUTER_MODEL env var)
194-
agent = make_test_agent(
195-
node_id="my-test-agent",
196-
ai_config=openrouter_config, # This respects OPENROUTER_MODEL
197-
)
198-
199-
# Define reasoner
200-
@agent.reasoner()
201-
async def my_reasoner(input_data: str) -> dict:
202-
response = await agent.ai(prompt=input_data)
203-
return {"result": response}
204-
205-
# Start agent and register
206-
# ... (see test_hello_world.py for full example)
207-
208-
# Execute through control plane
209-
result = await async_http_client.post(
210-
f"/api/v1/reasoners/{agent.node_id}.my_reasoner",
211-
json={"input": {"input_data": "test"}}
212-
)
213-
214-
# Validate
215-
assert result.status_code == 200
216-
assert "result" in result.json()
209+
agent = create_agent(openrouter_config, node_id="my-test-agent")
210+
211+
async with run_agent_server(agent):
212+
response = await async_http_client.post(
213+
f"/api/v1/reasoners/{agent.node_id}.my_reasoner",
214+
json={"input": {"input_data": "test"}},
215+
)
216+
assert response.status_code == 200
217+
assert "result" in response.json()
217218
```
218219

220+
> `make_test_agent` is still available for quick experiments, but we recommend capturing production-like agents under `agents/` so they can be reused across multiple tests or even run manually.
221+
219222
### Available Fixtures
220223

221224
#### Configuration Fixtures
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
Reusable agent definitions for functional tests.
3+
4+
Each module in this package should expose functions that return
5+
configured `Agent` instances that mirror real deployments.
6+
"""
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Agent definition that mirrors the README Quick Start example.
3+
4+
Tests can import `create_agent` to obtain a fully configured Agent
5+
without needing to replicate the agent definition inline.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import os
11+
from typing import Dict, Optional
12+
13+
import requests
14+
from agentfield import AIConfig, Agent
15+
16+
DEFAULT_NODE_ID = "quick-start-agent"
17+
18+
19+
def create_agent(
20+
ai_config: AIConfig,
21+
*,
22+
node_id: str = DEFAULT_NODE_ID,
23+
callback_url: Optional[str] = None,
24+
**agent_kwargs,
25+
) -> Agent:
26+
"""
27+
Build the Quick Start agent with the canonical fetch_url + summarize flow.
28+
"""
29+
agent_kwargs.setdefault("dev_mode", True)
30+
agent_kwargs.setdefault("callback_url", callback_url or "http://test-agent")
31+
agent_kwargs.setdefault(
32+
"agentfield_server", os.environ.get("AGENTFIELD_SERVER", "http://localhost:8080")
33+
)
34+
35+
agent = Agent(
36+
node_id=node_id,
37+
ai_config=ai_config,
38+
**agent_kwargs,
39+
)
40+
41+
@agent.skill()
42+
def fetch_url(url: str) -> str:
43+
response = requests.get(url, timeout=10)
44+
response.raise_for_status()
45+
return response.text
46+
47+
@agent.reasoner()
48+
async def summarize(url: str) -> Dict[str, str]:
49+
"""
50+
Fetch a URL, summarize it via OpenRouter, and return metadata.
51+
"""
52+
content = fetch_url(url)
53+
truncated = content[:2000]
54+
55+
ai_response = await agent.ai(
56+
system=(
57+
"You summarize documentation for internal verification. "
58+
"Be concise and focus on the site's purpose."
59+
),
60+
user=(
61+
"Summarize the following web page in no more than two sentences. "
62+
"Focus on what the site is intended for.\n"
63+
f"Content:\n{truncated}"
64+
),
65+
)
66+
summary_text = getattr(ai_response, "text", str(ai_response)).strip()
67+
68+
return {
69+
"url": url,
70+
"summary": summary_text,
71+
"content_snippet": truncated[:200],
72+
}
73+
74+
return agent
75+
76+
77+
def create_agent_from_env() -> Agent:
78+
"""
79+
Convenience helper to instantiate the agent from environment variables.
80+
81+
Useful if you want to run this module as a standalone script.
82+
"""
83+
api_key = os.environ["OPENROUTER_API_KEY"]
84+
model = os.environ.get("OPENROUTER_MODEL", "openrouter/google/gemini-2.5-flash-lite")
85+
86+
ai_config = AIConfig(
87+
model=model,
88+
api_key=api_key,
89+
temperature=float(os.environ.get("OPENROUTER_TEMPERATURE", "0.7")),
90+
max_tokens=int(os.environ.get("OPENROUTER_MAX_TOKENS", "500")),
91+
timeout=float(os.environ.get("OPENROUTER_TIMEOUT", "60.0")),
92+
retry_attempts=int(os.environ.get("OPENROUTER_RETRIES", "2")),
93+
)
94+
return create_agent(ai_config)
95+
96+
97+
if __name__ == "__main__":
98+
# Allow developers to run: `python -m agents.quick_start_agent`
99+
agent = create_agent_from_env()
100+
agent.run()

0 commit comments

Comments
 (0)