Skip to content

Commit 65fe7c9

Browse files
committed
feat(testing): Add model matrix test harness and RFC
This adds the initial implementation and RFC for the model matrix test harness. The harness enables running ADK conformance scenarios across multiple backend/model combinations via a YAML-driven configuration to produce structured pass/fail reports. Included files: - RFC-model-matrix-harness.md - Unit tests and validators for the model matrix - Temporary exploration scripts and test agents used during development
1 parent 4aa0fd8 commit 65fe7c9

21 files changed

Lines changed: 2818 additions & 0 deletions

RFC-model-matrix-harness.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# RFC: Model Matrix Test Harness
2+
3+
**Author:** George (gweale) **Date:** 2026-02-25 **Status:** Draft
4+
5+
## Summary
6+
7+
This RFC proposes a YAML-driven model matrix test harness that runs ADK conformance scenarios across multiple backend/model combinations (Gemini API, Vertex AI) and produces structured pass/fail reports. No existing ADK source code is modified.
8+
9+
## Motivation
10+
11+
ADK supports multiple backends and models, but there is no automated way to validate that the full feature surface — tool calling, auth flows, streaming, session rewind, MCP integration, third-party adapters, and the public API contract — works consistently across each backend/model pair. Manual testing is slow and coverage gaps are invisible. A matrix harness would catch backend-specific regressions before they reach users and provide a repeatable conformance baseline for new model releases.
12+
13+
## Proposal
14+
15+
This RFC proposes a standalone test harness that lives entirely in the `tests/` tree. A YAML configuration file declares targets (backend + environment), models, and a list of scenario IDs to execute. An orchestrator loads this config into a validated Pydantic schema, expands it into row plans (one per target/model pair), and runs each row's scenarios against a subprocess ADK API server, collecting results into a structured JSON report with a Markdown summary.
16+
17+
Proposed entry point:
18+
19+
```python
20+
def run_matrix(
21+
*,
22+
config_path: Path,
23+
output_dir: Optional[Path] = None,
24+
environment: Optional[Mapping[str, str]] = None,
25+
dry_run_override: Optional[bool] = None,
26+
strict_mode_override: Optional[bool] = None,
27+
) -> MatrixReport
28+
```
29+
30+
Key design elements:
31+
32+
- **Config schema** — Pydantic models for targets, models, run options, and unit-test groups, with validation for required env vars and cross-references
33+
- **Extensible scenario registry** — ships with 25 scenarios across 9 domains (tool pipeline, MCP, auth, live/streaming, deployment parity, memory/rewind, third-party adapters, API contract, CLI flags). Adding a new scenario requires only writing a validator function, registering it as a `ScenarioDefinition`, and listing its ID in the YAML config — no orchestrator or schema changes needed
34+
- **Server runtime** — spins up a subprocess `adk api_server` per row, polls for readiness, and tears down after execution
35+
- **Live scenario runtime**async client supporting `/run`, `/run_sse`, `/run_live` (WebSocket), concurrent prompt races, session rewind, and artifact operations
36+
- **Strict mode gate** — optionally enforces zero fail/error rows and flags unexpected skips
37+
- **Unit group integration** — runs named pytest selector groups as part of each row, reported alongside scenarios
38+
39+
The architecture is designed so that the orchestrator, config schema, and reporting layer never need to change when new scenarios are added. Targets, models, and unit groups are also open for extension via the YAML config alone. Fully backward compatible — all code lives under `tests/`, no existing source files are modified.
40+
41+
## Timeline
42+
43+
N/A
44+
45+
## Cross-workstream Impacts
46+
47+
None — the harness consumes existing ADK public APIs and CLI commands without modifying them.
48+
49+
## Outcome
50+
51+
Success means any backend/model combination can be validated with a single `python run_matrix.py --config matrix.yaml` invocation, producing a structured report that CI or developers can use to catch regressions. Immediate next steps are enabling non-dry-run execution against live Gemini API and Vertex AI targets and integrating the harness into CI.

my_test_agent/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import agent

my_test_agent/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from google.adk.agents import Agent
2+
3+
root_agent = Agent(
4+
name="test_agent",
5+
model="gemini-3.5-flash",
6+
instruction="You are a helpful assistant. Keep your response very short.",
7+
)

parse_html.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import sys
2+
from bs4 import BeautifulSoup
3+
4+
class TextExtract(BeautifulSoup):
5+
def get_text(self):
6+
return super().get_text(separator='\n', strip=True)
7+
8+
html = sys.stdin.read()
9+
soup = BeautifulSoup(html, 'html.parser')
10+
main_content = soup.find('article') or soup.find('main') or soup.body
11+
if main_content:
12+
print(main_content.get_text(separator='\n', strip=True))
13+
else:
14+
print("Could not find main content")

search_docs.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import urllib.request
2+
import json
3+
4+
def search(query):
5+
url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
6+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
7+
try:
8+
html = urllib.request.urlopen(req).read().decode('utf-8')
9+
from html.parser import HTMLParser
10+
class MyParser(HTMLParser):
11+
def __init__(self):
12+
super().__init__()
13+
self.in_a = False
14+
self.results = []
15+
self.current_link = ""
16+
def handle_starttag(self, tag, attrs):
17+
if tag == 'a':
18+
for attr in attrs:
19+
if attr[0] == 'href' and 'ad_provider' not in attr[1]:
20+
if attr[1].startswith('//duckduckgo.com/l/?uddg='):
21+
import urllib.parse
22+
self.current_link = urllib.parse.unquote(attr[1].split('uddg=')[1].split('&')[0])
23+
self.in_a = True
24+
def handle_data(self, data):
25+
if self.in_a and data.strip():
26+
self.results.append((data.strip(), self.current_link))
27+
def handle_endtag(self, tag):
28+
if tag == 'a':
29+
self.in_a = False
30+
31+
parser = MyParser()
32+
parser.feed(html)
33+
for text, link in parser.results[:10]:
34+
print(f"{text}\n{link}\n")
35+
except Exception as e:
36+
print(f"Error: {e}")
37+
38+
search("site:cloud.google.com/vertex-ai 'Interactions API'")
39+
search("site:ai.google.dev 'Interactions API'")

test_agent/agent.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from . import agent
2+
from google.adk.agents import Agent
3+
4+
root_agent = Agent(
5+
name="test_agent",
6+
model="gemini-3.5-flash",
7+
instruction="You are a helpful assistant. Please say 'Hello, I am gemini-3.5-flash'.",
8+
)

test_interactions_raw.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import asyncio
2+
import os
3+
import logging
4+
from google.genai import Client
5+
from google.genai import types
6+
7+
async def check_models():
8+
# Vertex AI Client
9+
client_vertex = Client(
10+
vertexai=True,
11+
project="cloud-llm-preview1",
12+
location="us-central1",
13+
http_options={"api_version": "v1beta1"}
14+
)
15+
16+
models_to_test = [
17+
"gemini-3-flash-preview",
18+
"gemini-3.5-flash-preview"
19+
]
20+
21+
for client, client_name in [(client_vertex, "Vertex AI")]:
22+
print(f"\n=============================")
23+
print(f"Testing Client: {client_name}")
24+
print(f"=============================")
25+
for model_id in models_to_test:
26+
print(f"\n--- Testing model: {model_id} ---")
27+
try:
28+
interaction = await client.aio.interactions.create(
29+
model=model_id,
30+
input="Hello!"
31+
)
32+
print(f"Success!")
33+
except Exception as e:
34+
print(f"Error: {e}")
35+
36+
if __name__ == "__main__":
37+
asyncio.run(check_models())

test_model_info.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import asyncio
2+
from google.genai import Client
3+
4+
async def check_models():
5+
client = Client(
6+
vertexai=True,
7+
project="cloud-llm-preview1",
8+
location="us-central1"
9+
)
10+
11+
print("Checking models...")
12+
models = client.models.list()
13+
for m in models:
14+
if "gemini-3.5" in m.name or "gemini-2.5" in m.name:
15+
print(f"Model: {m.name}")
16+
print(f" Supported Actions: {m.supported_actions if hasattr(m, 'supported_actions') else 'Unknown'}")
17+
print(f" Version: {m.version if hasattr(m, 'version') else 'Unknown'}")
18+
19+
if __name__ == "__main__":
20+
asyncio.run(check_models())

test_run.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import asyncio
2+
import os
3+
from my_test_agent.agent import root_agent
4+
from google.adk.runners import Runner
5+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
6+
7+
async def main():
8+
runner = Runner(
9+
app_name="test_app",
10+
agent=root_agent,
11+
session_service=InMemorySessionService(),
12+
auto_create_session=True
13+
)
14+
print("Testing gemini-3.5-flash...")
15+
events = await runner.run_debug("Hi, please confirm your model version.")
16+
for e in events:
17+
if e.author == 'test_agent' and e.content:
18+
print("Response:", e.content.parts[0].text if e.content.parts else e.content)
19+
20+
if __name__ == "__main__":
21+
asyncio.run(main())
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations

0 commit comments

Comments
 (0)