Agent-Tier Tests (AI-client connectivity)
The AGENT tier verifies that real AI clients -- Claude Code, Codex, and a
deterministic MCP contract client -- can reach and correctly use Envoy's MCP
tools end to end. Like destructive-tests.md, this is a dev-only convention
for the Embody test harness -- it has NO shipped template counterpart.
The tier model
- Suites inherit
AgentTestCase (which carries AGENT = True). They are
EXCLUDED from RunTests / RunTestsSync / RunTestsDeferred* AND from
RunDestructiveTests by _discoverTestSuites' tier filter. Do not remove
that guard: a normal full run must never spawn AI clients or burn
subscription usage silently.
- They run ONLY via
op.unit_tests.RunAgentTests(suite_name=None, test_name=None, delay_frames=30) -- fire-and-forget; poll GetResults().
- Two layers:
- Tier 1 (
test_agent_contract): no LLM. An out-of-process stdlib MCP
client (agent_clients/mcp_contract_client.py) spawns the EXACT bridge
command from .mcp.json and checks handshake, tool inventory vs the
EXPECTED_ENVOY_TOOLS manifest, and a curated call sequence. This is the
cheap, deterministic half -- run it first when diagnosing.
- Tier 2 (
test_agent_smoke_claude / test_agent_smoke_codex): a real
agent on scripted micro-tasks, verified against LIVE TD state (probe op
exists with the exact run token), never the agent's prose alone.
Why the runner is async (do not "simplify" it)
Envoy drains MCP requests on TD's MAIN thread (max 5 per frame). A test that
blocks the main thread while an agent subprocess makes MCP calls deadlocks
the very tools under test until every call times out. So RunAgentTests
launches subprocesses non-blocking (stdout/stderr to temp FILES -- an unread
PIPE deadlocks the child at ~64KB; stdin always DEVNULL -- codex exec hangs
probing a silent stdin pipe on Windows, openai/codex#20919) and polls them
via a run(delayFrames=N) chain using the STRING-EXPRESSION form, so a
mid-run extension reinit cannot strand the state machine on a stale instance.
Timeouts kill the whole process TREE (the CLIs spawn the bridge as a child).
Auth and billing (subscription only)
AgentTestCase.launchEnv() strips ANTHROPIC_API_KEY,
ANTHROPIC_AUTH_TOKEN, OPENAI_API_KEY, CODEX_API_KEY from the child
env: a set API key silently OVERRIDES subscription auth and bills per
token. With them absent, claude -p uses the stored Pro/Max OAuth login
and codex exec the ChatGPT login.
- Missing CLI -> loud SKIP (
requireCli). Codex suites gate on
codex login status (exit 0 = logged in) before any billed task.
- Claude exit codes worth knowing: 100 = not logged in, 101 = MCP server
unreachable (with
--strict-mcp-config), 102 = tool permission denied in
-p mode (no TTY -> no prompt -> immediate deny). Codex exec exit codes
are NOT documented as task success -- judge by --output-last-message +
TD state.
Isolation choices (keep them)
- Smoke agents run from a NEUTRAL temp cwd: no CLAUDE.md / AGENTS.md /
project settings, so the agent does the micro-task, not session rituals.
- Claude:
--mcp-config <envoy-only json> + --strict-mcp-config +
per-task --allowedTools allowlists + a standing --disallowedTools
denylist (execute_python, run_tests, delete_op, import_network,
restart_td, launch_td, switch_instance).
- Codex: inline
-c mcp_servers.envoy.* overrides (Codex does not read
.mcp.json; the user's ~/.codex/config.toml is never touched), with
-c approval_policy="never" (the exec subcommand rejects -a on
installed builds) + default_tools_approval_mode="auto" so MCP calls are
not auto-cancelled, --sandbox read-only (MCP servers spawn OUTSIDE the
sandbox), and a real .exe when the npm .cmd shim is what PATH probing
finds.
delete_op is NOT part of any Tier-2 smoke task: a peer session's recent
writes can gate it mid-test (multi-session destructive gate). Tier 1
covers delete_op and treats a MULTI-SESSION GATE refusal as
operational-with-gate.
- Each spawned client registers as its own Envoy session via
EMBODY_SESSION_LABEL (agent-test-tier1, claude-smoke,
codex-smoke), so get_sessions / _peers traffic is attributable.
Maintenance
- Envoy tool surface changed -> update
EXPECTED_ENVOY_TOOLS in
test_agent_contract.py in the SAME commit. The inventory check fails on
drift in either direction. Remember tools register on the MCPServer instance
at Envoy START -- restart Envoy before believing a mismatch.
- Model pins live at the top of the smoke suites (
CLAUDE_SMOKE_MODEL,
CODEX_SMOKE_MODEL). The Codex model list rotates with releases; update
deliberately, never blindly.
- The runner machinery itself is unit-tested in
test_agent_runner.py
(normal tier): tier gating, job lifecycle, timeout kill, verdict
classification, Filecleanup storage roundtrip. Update it when touching the
agent runner.
For the AI agent
- Never run
RunAgentTests() casually: it spends the user's subscription
usage and takes minutes. Run it when asked, before releases, or when
MCP-facing code changed.
- Prefer Tier 1 alone (
RunAgentTests(suite_name='test_agent_contract'))
for transport/contract diagnosis -- it is free and deterministic.
- The spawned agents appear as peer sessions; expect
_peers advisories
during a run and do not treat the smoke agents' touches as a human peer.
1---2name: agent-tests3description: MUST READ before calling RunAgentTests or touching agent-tier test infrastructure -- these tests spawn real AI clients (Claude Code, Codex) and SPEND SUBSCRIPTION USAGE; never run casually. Tier model, async runner rationale, auth/isolation rules.4---56# Agent-Tier Tests (AI-client connectivity)78The AGENT tier verifies that real AI clients -- Claude Code, Codex, and a9deterministic MCP contract client -- can reach and correctly use Envoy's MCP10tools end to end. Like `destructive-tests.md`, this is a dev-only convention11for the Embody test harness -- it has NO shipped template counterpart.1213## The tier model1415- Suites inherit `AgentTestCase` (which carries `AGENT = True`). They are16 EXCLUDED from `RunTests` / `RunTestsSync` / `RunTestsDeferred*` AND from17 `RunDestructiveTests` by `_discoverTestSuites`' tier filter. Do not remove18 that guard: a normal full run must never spawn AI clients or burn19 subscription usage silently.20- They run ONLY via `op.unit_tests.RunAgentTests(suite_name=None,21 test_name=None, delay_frames=30)` -- fire-and-forget; poll `GetResults()`.22- Two layers:23 - **Tier 1 (`test_agent_contract`)**: no LLM. An out-of-process stdlib MCP24 client (`agent_clients/mcp_contract_client.py`) spawns the EXACT bridge25 command from `.mcp.json` and checks handshake, tool inventory vs the26 `EXPECTED_ENVOY_TOOLS` manifest, and a curated call sequence. This is the27 cheap, deterministic half -- run it first when diagnosing.28 - **Tier 2 (`test_agent_smoke_claude` / `test_agent_smoke_codex`)**: a real29 agent on scripted micro-tasks, verified against LIVE TD state (probe op30 exists with the exact run token), never the agent's prose alone.3132## Why the runner is async (do not "simplify" it)3334Envoy drains MCP requests on TD's MAIN thread (max 5 per frame). A test that35blocks the main thread while an agent subprocess makes MCP calls deadlocks36the very tools under test until every call times out. So `RunAgentTests`37launches subprocesses non-blocking (stdout/stderr to temp FILES -- an unread38PIPE deadlocks the child at ~64KB; stdin always DEVNULL -- codex exec hangs39probing a silent stdin pipe on Windows, openai/codex#20919) and polls them40via a `run(delayFrames=N)` chain using the STRING-EXPRESSION form, so a41mid-run extension reinit cannot strand the state machine on a stale instance.42Timeouts kill the whole process TREE (the CLIs spawn the bridge as a child).4344## Auth and billing (subscription only)4546- `AgentTestCase.launchEnv()` strips `ANTHROPIC_API_KEY`,47 `ANTHROPIC_AUTH_TOKEN`, `OPENAI_API_KEY`, `CODEX_API_KEY` from the child48 env: a set API key silently OVERRIDES subscription auth and bills per49 token. With them absent, `claude -p` uses the stored Pro/Max OAuth login50 and `codex exec` the ChatGPT login.51- Missing CLI -> loud SKIP (`requireCli`). Codex suites gate on52 `codex login status` (exit 0 = logged in) before any billed task.53- Claude exit codes worth knowing: 100 = not logged in, 101 = MCP server54 unreachable (with `--strict-mcp-config`), 102 = tool permission denied in55 `-p` mode (no TTY -> no prompt -> immediate deny). Codex exec exit codes56 are NOT documented as task success -- judge by `--output-last-message` +57 TD state.5859## Isolation choices (keep them)6061- Smoke agents run from a NEUTRAL temp cwd: no CLAUDE.md / AGENTS.md /62 project settings, so the agent does the micro-task, not session rituals.63- Claude: `--mcp-config <envoy-only json>` + `--strict-mcp-config` +64 per-task `--allowedTools` allowlists + a standing `--disallowedTools`65 denylist (execute_python, run_tests, delete_op, import_network,66 restart_td, launch_td, switch_instance).67- Codex: inline `-c mcp_servers.envoy.*` overrides (Codex does not read68 `.mcp.json`; the user's `~/.codex/config.toml` is never touched), with69 `-c approval_policy="never"` (the exec subcommand rejects `-a` on70 installed builds) + `default_tools_approval_mode="auto"` so MCP calls are71 not auto-cancelled, `--sandbox read-only` (MCP servers spawn OUTSIDE the72 sandbox), and a real `.exe` when the npm `.cmd` shim is what PATH probing73 finds.74- `delete_op` is NOT part of any Tier-2 smoke task: a peer session's recent75 writes can gate it mid-test (multi-session destructive gate). Tier 176 covers delete_op and treats a MULTI-SESSION GATE refusal as77 operational-with-gate.78- Each spawned client registers as its own Envoy session via79 `EMBODY_SESSION_LABEL` (`agent-test-tier1`, `claude-smoke`,80 `codex-smoke`), so `get_sessions` / `_peers` traffic is attributable.8182## Maintenance8384- Envoy tool surface changed -> update `EXPECTED_ENVOY_TOOLS` in85 `test_agent_contract.py` in the SAME commit. The inventory check fails on86 drift in either direction. Remember tools register on the MCPServer instance87 at Envoy START -- restart Envoy before believing a mismatch.88- Model pins live at the top of the smoke suites (`CLAUDE_SMOKE_MODEL`,89 `CODEX_SMOKE_MODEL`). The Codex model list rotates with releases; update90 deliberately, never blindly.91- The runner machinery itself is unit-tested in `test_agent_runner.py`92 (normal tier): tier gating, job lifecycle, timeout kill, verdict93 classification, Filecleanup storage roundtrip. Update it when touching the94 agent runner.9596## For the AI agent9798- Never run `RunAgentTests()` casually: it spends the user's subscription99 usage and takes minutes. Run it when asked, before releases, or when100 MCP-facing code changed.101- Prefer Tier 1 alone (`RunAgentTests(suite_name='test_agent_contract')`)102 for transport/contract diagnosis -- it is free and deterministic.103- The spawned agents appear as peer sessions; expect `_peers` advisories104 during a run and do not treat the smoke agents' touches as a human peer.