Skip to content

Commit f2bc745

Browse files
authored
test: restructure test suite into four distinct execution tiers (#128)
* fix(tests): add postgres dependencies to dev group for testing Added psycopg[binary]>=3.1 and pgvector>=0.3 to dev dependency group to ensure all unit tests can run during development and CI. This fixes the test collection error for test_postgres_backend.py while keeping postgres support optional for end users (via the pgvector optional dependency group). * test: restructure test suite into four distinct execution tiers * fix(tests): resolve linting errors and force isolated filesystem config in unit test fixture * docs: standardize cli commands to use evolve entrypoint across all readmes * test: dynamically inject E2E phoenix fixture URL
1 parent 3837a28 commit f2bc745

13 files changed

Lines changed: 173 additions & 85 deletions

README.md

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -125,28 +125,35 @@ If you’re experimenting with Evolve or exploring on‑the‑job learning for a
125125

126126
### Running Tests
127127

128-
```bash
129-
uv run pytest
130-
```
131-
132-
#### Phoenix Sync Tests
133-
134-
Tests for the Phoenix trajectory sync functionality are **skipped by default** since they require familiarity with the Phoenix integration. To include them:
135-
136-
```bash
137-
# Run all tests including Phoenix tests
138-
uv run pytest --run-phoenix
139-
140-
# Run only Phoenix tests
141-
uv run pytest -m phoenix
142-
```
143-
144-
#### End-to-End (E2E) Low-Code Verification
145-
146-
To run the full end-to-end verification pipeline (Agent -> Trace -> Tip):
147-
148-
```bash
149-
EVOLVE_E2E=true uv run pytest tests/e2e/test_e2e_pipeline.py -s
150-
```
151-
152-
See [docs/LOW_CODE_TRACING.md](docs/LOW_CODE_TRACING.md#end-to-end-verification) for more details.
128+
The test suite is organized into 4 cleanly isolated tiers depending on infrastructure requirements:
129+
130+
1. **Default Local Suite**
131+
Runs both fast logic tests (`unit`) and filesystem script verifications (`platform_integrations`).
132+
```bash
133+
uv run pytest
134+
```
135+
136+
2. **Unit Tests (Only)**
137+
Fast, fully-mocked tests verifying core logic and offline pipeline schemas.
138+
```bash
139+
uv run pytest -m unit
140+
```
141+
142+
3. **Platform Integration Tests**
143+
Fast filesystem-level integration tests verifying local tool installation and idempotency.
144+
```bash
145+
uv run pytest -m platform_integrations
146+
```
147+
148+
4. **End-to-End Infrastructure Tests**
149+
Heavy tests that autonomously spin up a background Phoenix server and simulate full agent workflows.
150+
```bash
151+
uv run pytest -m e2e --run-e2e
152+
```
153+
*(See [docs/LOW_CODE_TRACING.md](docs/LOW_CODE_TRACING.md#end-to-end-verification) for more details).*
154+
155+
5. **LLM Evaluation Tests**
156+
Tests needing active LLM inference to test resolution pipelines (requires LLM API keys).
157+
```bash
158+
uv run pytest -m llm
159+
```

README_phoenix_sync.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,20 @@ No additional dependencies required - uses only stdlib for Phoenix API calls.
3232

3333
```bash
3434
# Basic sync with defaults
35-
uv run python -m evolve.cli.cli sync phoenix
35+
uv run evolve sync phoenix
3636

3737
# Custom Phoenix URL and namespace
38-
uv run python -m evolve.cli.cli sync phoenix \
38+
uv run evolve sync phoenix \
3939
--url http://phoenix.example.com:6006 \
4040
--namespace my_namespace
4141

4242
# Fetch more spans and include errors
43-
uv run python -m evolve.cli.cli sync phoenix \
43+
uv run evolve sync phoenix \
4444
--limit 500 \
4545
--include-errors
4646

4747
# Full options
48-
uv run python -m evolve.cli.cli sync phoenix \
48+
uv run evolve sync phoenix \
4949
--url http://localhost:6006 \
5050
--namespace production \
5151
--project my_project \
@@ -145,7 +145,7 @@ Two entity types are stored:
145145

146146
```bash
147147
# Sync every hour
148-
0 * * * * cd /path/to/evolve && uv run python -m evolve.cli.cli sync phoenix --limit 100
148+
0 * * * * cd /path/to/evolve && uv run evolve sync phoenix --limit 100
149149
```
150150

151151
### Systemd Timer
@@ -158,7 +158,7 @@ Description=Evolve Phoenix Sync
158158
[Service]
159159
Type=oneshot
160160
WorkingDirectory=/path/to/evolve
161-
ExecStart=/path/to/uv run python -m evolve.cli.cli sync phoenix
161+
ExecStart=/path/to/uv run evolve sync phoenix
162162
Environment=PHOENIX_URL=http://localhost:6006
163163
Environment=EVOLVE_NAMESPACE_ID=production
164164
```

docs/LOW_CODE_TRACING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ curl "http://localhost:6006/v1/projects/test-agent/spans?limit=5"
200200
cd evolve_repo
201201
EVOLVE_BACKEND=filesystem \
202202
EVOLVE_TIPS_MODEL="gpt-4" \
203-
uv run python -m evolve.frontend.cli.cli sync phoenix \
203+
uv run evolve sync phoenix \
204204
--project test-agent \
205205
--include-errors
206206
```
@@ -209,7 +209,7 @@ uv run python -m evolve.frontend.cli.cli sync phoenix \
209209

210210
```bash
211211
EVOLVE_BACKEND=filesystem \
212-
uv run python -m evolve.frontend.cli.cli entities list evolve --type guideline
212+
uv run evolve entities list evolve --type guideline
213213
```
214214

215215
### 6. Understanding Tip Provenance (Metadata)
@@ -246,7 +246,7 @@ Evolve includes a comprehensive E2E verification suite to ensure that tracing an
246246
You can run the full regression suite using `pytest`:
247247

248248
```bash
249-
EVOLVE_E2E=true uv run pytest tests/e2e/test_e2e_pipeline.py -s
249+
uv run pytest -m e2e --run-e2e -s
250250
```
251251

252252
### Running Specific Tests
@@ -255,10 +255,10 @@ To test a specific agent framework:
255255

256256
```bash
257257
# Test smolagents
258-
EVOLVE_E2E=true uv run pytest tests/e2e/test_e2e_pipeline.py -k smolagents -s
258+
uv run pytest tests/e2e/test_e2e_pipeline.py -k smolagents -m e2e --run-e2e -s
259259

260260
# Test OpenAI Agents
261-
EVOLVE_E2E=true uv run pytest tests/e2e/test_e2e_pipeline.py -k openai_agents -s
261+
uv run pytest tests/e2e/test_e2e_pipeline.py -k openai_agents -m e2e --run-e2e -s
262262
```
263263

264264
### What It Tests

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ dev = [
4848
"anyio",
4949
"detect-secrets",
5050
"mypy",
51+
"pgvector>=0.3",
5152
"pre-commit",
53+
"psycopg[binary]>=3.1",
5254
"pytest",
5355
"pytest-cov",
5456
"pytest-retry",
@@ -85,12 +87,12 @@ evolve = ["**/*.jinja2"]
8587
package = true
8688

8789
[tool.pytest.ini_options]
88-
addopts = "--ignore=explorations -m 'not phoenix and not llm'"
90+
addopts = "--ignore=explorations -m 'not llm and not e2e'"
8991
markers = [
9092
"e2e",
9193
"unit",
92-
"phoenix",
93-
"llm"
94+
"llm",
95+
"platform_integrations"
9496
]
9597
anyio_mode = "auto"
9698

tests/conftest.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,19 @@ def mock_sentence_transformer(request):
3131
def pytest_addoption(parser):
3232
"""Add custom command line options."""
3333
parser.addoption(
34-
"--run-phoenix",
34+
"--run-e2e",
3535
action="store_true",
3636
default=False,
37-
help="Run Phoenix sync tests (skipped by default)",
37+
help="Run End-to-End infrastructure tests (skipped by default)",
3838
)
3939

4040

4141
def pytest_configure(config):
42-
"""Override marker filter when --run-phoenix is passed."""
43-
if config.getoption("--run-phoenix"):
44-
# Remove the default marker filter to include phoenix tests
45-
# Get current markexpr and modify it
46-
markexpr = config.getoption("markexpr", default="")
47-
if markexpr == "not phoenix":
48-
config.option.markexpr = ""
49-
elif "not phoenix" in markexpr:
50-
# Remove "not phoenix" from the expression
51-
new_expr = markexpr.replace("not phoenix and ", "").replace(" and not phoenix", "").replace("not phoenix", "")
52-
config.option.markexpr = new_expr.strip()
42+
"""Override marker filter when relevant flags are passed."""
43+
new_expr = config.getoption("markexpr", default="")
44+
45+
if config.getoption("--run-e2e"):
46+
# Remove "not e2e" from the expression
47+
new_expr = new_expr.replace("not e2e and ", "").replace(" and not e2e", "").replace("not e2e", "")
48+
49+
config.option.markexpr = new_expr.strip()

tests/e2e/test_e2e_pipeline.py

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import os
55
import datetime
66
import pytest
7-
from evolve.config.phoenix import phoenix_settings
7+
import urllib.request
8+
import urllib.error
89

910
# Configuration
10-
PHOENIX_URL = phoenix_settings.url
1111
# Use a session-scope timestamp or generate per test?
1212
# Per-test ensures no collisions even if run in parallel (though these should satisfy sequential)
1313
TIMESTAMP = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -20,10 +20,65 @@
2020
]
2121

2222

23+
@pytest.fixture(scope="session", autouse=True)
24+
def phoenix_server():
25+
"""Ensure a Phoenix server is running before executing E2E tests, and shut it down afterward."""
26+
# 1. Check if it's already running locally
27+
try:
28+
urllib.request.urlopen("http://localhost:6006/status", timeout=2)
29+
print("\nPhoenix is already running on port 6006.")
30+
yield "http://localhost:6006"
31+
return
32+
except (urllib.error.URLError, ConnectionError):
33+
pass
34+
35+
import sys
36+
37+
print("\nStarting local Phoenix server for E2E tests...")
38+
39+
env = os.environ.copy()
40+
env["PHOENIX_PORT"] = "6006"
41+
42+
# Start it using the current python executable to avoid 'uv run' overhead
43+
# We use run_in_thread=True and a sleepy while loop because run_in_thread=False
44+
# can crash the fastAPI uvicorn startup in some MacOS environments.
45+
script = "import phoenix as px; import time; px.launch_app(run_in_thread=True); import sys; sys.stdout.flush(); time.sleep(86400)"
46+
47+
proc = subprocess.Popen([sys.executable, "-c", script], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
48+
49+
# Poll until the server is responsive
50+
max_retries = 30
51+
for _ in range(max_retries):
52+
try:
53+
# specifically hit the status endpoint
54+
urllib.request.urlopen("http://localhost:6006/status", timeout=2)
55+
print("Phoenix server is up and running.")
56+
break
57+
except Exception:
58+
# Also check if process crashed early
59+
if proc.poll() is not None:
60+
stderr_output = proc.stderr.read() if proc.stderr else "Unknown error"
61+
pytest.fail(f"Phoenix server process crashed unexpectedly: {stderr_output}")
62+
time.sleep(1)
63+
else:
64+
proc.terminate()
65+
stderr_output = proc.stderr.read() if proc.stderr else "Unknown error"
66+
pytest.fail(f"Failed to start local Phoenix server within 30 seconds. Stderr: {stderr_output}")
67+
68+
yield "http://localhost:6006"
69+
70+
# Cleanup: shut down Phoenix when tests are done
71+
print("\nShutting down local Phoenix server...")
72+
proc.terminate()
73+
try:
74+
proc.wait(timeout=5)
75+
except subprocess.TimeoutExpired:
76+
proc.kill()
77+
78+
2379
@pytest.mark.e2e
24-
@pytest.mark.phoenix
2580
@pytest.mark.parametrize("agent_config", AGENTS_TO_TEST, ids=[a["name"] for a in AGENTS_TO_TEST])
26-
def test_e2e_pipeline_agent(agent_config):
81+
def test_e2e_pipeline_agent(agent_config, phoenix_server):
2782
"""
2883
Runs the full E2E pipeline for a specific agent configuration:
2984
1. Executing the agent script
@@ -58,7 +113,16 @@ def test_e2e_pipeline_agent(agent_config):
58113
if not os.path.exists(script_path):
59114
pytest.fail(f"Script not found: {script_path}")
60115

61-
result = subprocess.run(["uv", "run", "python", script_path], env=env, capture_output=True, text=True)
116+
try:
117+
result = subprocess.run(["uv", "run", "python", script_path], env=env, capture_output=True, text=True, timeout=90)
118+
except subprocess.TimeoutExpired as e:
119+
print("❌ Agent execution timed out after 90s")
120+
# Still try to capture what we can from stdout/stderr if possible
121+
stdout = e.stdout if e.stdout else ""
122+
stderr = e.stderr if e.stderr else ""
123+
print("STDOUT:", stdout)
124+
print("STDERR:", stderr)
125+
pytest.fail(f"Agent execution timed out for {agent_name}")
62126

63127
if result.returncode != 0:
64128
print(f"❌ Agent failed with exit code {result.returncode}")
@@ -78,7 +142,7 @@ def test_e2e_pipeline_agent(agent_config):
78142
import phoenix as px
79143
import sys
80144
try:
81-
c = px.Client(endpoint='{PHOENIX_URL}')
145+
c = px.Client(endpoint='{phoenix_server}')
82146
df = c.get_spans_dataframe(project_name='{project_name}')
83147
if df is not None and not df.empty:
84148
print(f"FOUND_TRACES:{{len(df)}}")
@@ -87,7 +151,11 @@ def test_e2e_pipeline_agent(agent_config):
87151
except Exception as e:
88152
print(f"ERROR:{{e}}")
89153
"""
90-
result = subprocess.run(["uv", "run", "python", "-c", check_script], capture_output=True, text=True)
154+
try:
155+
result = subprocess.run(["uv", "run", "python", "-c", check_script], capture_output=True, text=True, timeout=30)
156+
except subprocess.TimeoutExpired:
157+
print("❌ Phoenix trace verification script timed out")
158+
pytest.fail(f"Trace verification timed out for {project_name}")
91159

92160
output = result.stdout + result.stderr
93161
if "FOUND_TRACES" in output:
@@ -103,9 +171,7 @@ def test_e2e_pipeline_agent(agent_config):
103171
sync_command = [
104172
"uv",
105173
"run",
106-
"python",
107-
"-m",
108-
"evolve.frontend.cli.cli",
174+
"evolve",
109175
"sync",
110176
"phoenix",
111177
"--project",
@@ -128,36 +194,42 @@ def test_e2e_pipeline_agent(agent_config):
128194
tips_found = False
129195
sync_start = time.time()
130196
timeout = 120 # 2 minute timeout for sync
197+
output_lines = []
131198

132199
try:
133200
while True:
134201
if time.time() - sync_start > timeout:
135-
print("❌ Timeout waiting for tips generation")
202+
print(f"❌ Timeout waiting for tips generation ({timeout}s)")
136203
break
137204

138205
line = process.stdout.readline()
139-
if not line and process.poll() is not None:
140-
break
141-
142-
if line:
143-
line_stripped = line.strip()
144-
# print(f"[Sync] {line_stripped}") # Optional: verbose logging
145-
146-
# Check target log pattern
147-
match = re.search(r"generated (\d+) tips", line_stripped)
148-
if match:
149-
count = match.group(1)
150-
print(f"\n✅ SUCCESS: Generated {count} tips!")
151-
tips_found = True
206+
if not line:
207+
if process.poll() is not None:
152208
break
209+
time.sleep(0.1) # Avoid tight loop if no output but process alive
210+
continue
211+
212+
output_lines.append(line)
213+
line_stripped = line.strip()
214+
# print(f"[Sync] {line_stripped}") # Optional: verbose logging
215+
216+
# Check target log pattern
217+
match = re.search(r"generated (\d+) tips", line_stripped)
218+
if match:
219+
count = match.group(1)
220+
print(f"\n✅ SUCCESS: Generated {count} tips!")
221+
tips_found = True
222+
break
153223
finally:
154224
if process.poll() is None:
155225
print("Stopping sync process...")
156226
process.terminate()
157227
try:
158-
process.wait(timeout=5)
228+
process.wait(timeout=10)
159229
except subprocess.TimeoutExpired:
160230
process.kill()
161231

162232
if not tips_found:
233+
full_output = "".join(output_lines)
234+
print(f"Final Sync Output:\n{full_output}")
163235
pytest.fail(f"Failed to detect tip generation for {agent_name} within {timeout}s.")

0 commit comments

Comments
 (0)