Speed up Rust E2E tests with shared clients - #2250
Conversation
Reuse file-scoped Copilot clients for eligible Rust E2E tests while retaining dedicated clients for startup and lifecycle-sensitive cases. Keep shared router tasks on a persistent runtime and bound harness cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Introduces reusable Rust E2E clients and proxies to reduce runtime startup overhead while retaining dedicated clients for lifecycle-sensitive tests.
Changes:
- Adds shared E2E group lifecycle, isolation cleanup, persistent Tokio runtime, and bounded I/O/process handling.
- Migrates eligible E2E suites to shared clients with custom group options where needed.
- Adds test-support client hooks for router startup and session cleanup.
Show a summary per file
| File | Description |
|---|---|
rust/src/lib.rs |
Adds test-only router and session-cleanup APIs. |
rust/tests/e2e/support.rs |
Implements shared E2E groups, cleanup, and bounded proxy operations. |
rust/tests/e2e/abort.rs |
Explicitly retains dedicated clients. |
rust/tests/e2e/ask_user.rs |
Shares the client across ask-user tests. |
rust/tests/e2e/builtin_tools.rs |
Shares built-in tool test infrastructure. |
rust/tests/e2e/canvas.rs |
Reuses one client for canvas tests. |
rust/tests/e2e/client_api.rs |
Shares eligible client API tests. |
rust/tests/e2e/commands.rs |
Reuses a client for command tests. |
rust/tests/e2e/compaction.rs |
Shares compaction test infrastructure. |
rust/tests/e2e/elicitation.rs |
Reuses one client for elicitation tests. |
rust/tests/e2e/event_fidelity.rs |
Shares event-fidelity tests. |
rust/tests/e2e/hooks.rs |
Shares basic hook tests. |
rust/tests/e2e/hooks_extended.rs |
Shares extended hook tests. |
rust/tests/e2e/mode_empty.rs |
Adds a shared empty-mode client configuration. |
rust/tests/e2e/mode_handlers.rs |
Shares mode-handler tests. |
rust/tests/e2e/multi_provider_registry.rs |
Shares provider-routing tests. |
rust/tests/e2e/multi_turn.rs |
Shares multi-turn tests. |
rust/tests/e2e/permissions.rs |
Shares eligible permission tests. |
rust/tests/e2e/pre_mcp_tool_call_hook.rs |
Shares pre-MCP hook tests. |
rust/tests/e2e/rpc_additional_edge_cases.rs |
Shares additional RPC edge cases. |
rust/tests/e2e/rpc_agent.rs |
Shares agent RPC tests. |
rust/tests/e2e/rpc_event_log.rs |
Shares event-log tests. |
rust/tests/e2e/rpc_event_side_effects.rs |
Shares event side-effect tests. |
rust/tests/e2e/rpc_mcp_and_skills.rs |
Shares MCP and skills RPC tests. |
rust/tests/e2e/rpc_mcp_config.rs |
Shares MCP configuration tests. |
rust/tests/e2e/rpc_mcp_lifecycle.rs |
Shares MCP lifecycle tests. |
rust/tests/e2e/rpc_queue.rs |
Shares queue tests. |
rust/tests/e2e/rpc_remote.rs |
Shares remote-session tests. |
rust/tests/e2e/rpc_schedule.rs |
Shares scheduling tests. |
rust/tests/e2e/rpc_server.rs |
Shares eligible server RPC tests. |
rust/tests/e2e/rpc_server_misc.rs |
Shares eligible miscellaneous server tests. |
rust/tests/e2e/rpc_server_plugins.rs |
Shares plugin and marketplace tests. |
rust/tests/e2e/rpc_server_remote_control.rs |
Shares remote-control tests. |
rust/tests/e2e/rpc_session_state.rs |
Shares session-state RPC tests. |
rust/tests/e2e/rpc_session_state_extras.rs |
Shares supplementary session-state tests. |
rust/tests/e2e/rpc_shell_and_fleet.rs |
Shares shell and fleet tests. |
rust/tests/e2e/rpc_shell_edge_cases.rs |
Shares shell edge-case tests. |
rust/tests/e2e/rpc_shell_user_requested.rs |
Shares user-requested shell tests. |
rust/tests/e2e/rpc_tasks_and_handlers.rs |
Shares task and handler tests. |
rust/tests/e2e/rpc_ui_ephemeral_query.rs |
Shares the ephemeral-query test. |
rust/tests/e2e/rpc_workspace_checkpoints.rs |
Shares workspace-checkpoint tests. |
rust/tests/e2e/session.rs |
Migrates eligible session tests to reuse. |
rust/tests/e2e/session_config.rs |
Shares eligible session configuration tests. |
rust/tests/e2e/session_fs_sqlite.rs |
Adds shared SQLite client options. |
rust/tests/e2e/session_lifecycle.rs |
Shares session lifecycle tests. |
rust/tests/e2e/session_todos_changed.rs |
Shares the todos-changed test. |
rust/tests/e2e/skills.rs |
Shares skills tests. |
rust/tests/e2e/streaming_fidelity.rs |
Shares eligible streaming tests. |
rust/tests/e2e/system_message_sections.rs |
Shares system-message section tests. |
rust/tests/e2e/tool_results.rs |
Shares tool-result tests. |
rust/tests/e2e/tools.rs |
Shares tool invocation tests. |
Review details
- Files reviewed: 51/51 changed files
- Comments generated: 4
- Review effort level: Balanced
Keep runtime-global plugin state, model-catalog changes, and Windows-locked config directories on dedicated clients. Match nightly formatting and resolve the new cleanup helper's clippy warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (4)
rust/src/lib.rs:2203
- This doc comment describes
cleanup_sessions_for_test, but it is attached tostart_router_for_test, which only starts router tasks. Update it so callers of the test-support API are not told that invoking this method deletes sessions.
/// Disconnect and delete every session owned by this test client's isolated
/// runtime. This is test-harness plumbing, not part of the supported SDK API.
#[cfg(feature = "test-support")]
#[doc(hidden)]
pub fn start_router_for_test(&self) {
rust/tests/e2e/support.rs:230
- A timed-out or panicking test only tears down the shared client when cleanup also fails or this happens to be the final invocation. Successful session/work-dir cleanup cannot reset arbitrary connection-level state left by the failed test, so later tests can inherit corrupted state; an early-aborted run can also leave the proxy/client alive. Treat every non-success
resultas a teardown condition before propagating the failure.
rust/tests/e2e/rpc_session_state.rs:58 - This model-switch test must remain isolated. The runtime caches
/modelsfor 30 minutes per auth/base URL; almost every snapshot in this shared group exposes onlyclaude-sonnet-4.5, while this snapshot also requiresgpt-5.4. Whichever test primes the shared cache determines whether this switch succeeds. Use the dedicated context and adjust the group's expected shared invocation count; the Node, .NET, and Python versions already isolate this case for the same reason.
rust/tests/e2e/session.rs:1006 - This reasoning-effort test needs a dedicated runtime because its snapshot uniquely adds
gpt-5.4; the other sharedsessionsnapshots can prime the runtime's 30-minute/modelscache with onlyclaude-sonnet-4.5. That makes the outcome order-dependent. Keep this test isolated, as the Node, .NET, and Python equivalents do, and decrement the shared invocation count.
- Files reviewed: 51/51 changed files
- Comments generated: 1
- Review effort level: Balanced
Preserve skipped shared-group accounting without configuring snapshots, reap failed proxy startups, correct test-support docs, and avoid CodeQL binding ambiguity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep model-catalog-dependent tests on dedicated clients, correct provider group teardown accounting, and discard shared connection state after any failed test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (2)
rust/tests/e2e/multi_provider_registry.rs:243
assert_routingis invoked by three separate#[tokio::test]functions, in addition to the registration test, so this group runs four times. Withexpected_invocationsset to 2, the group tears down after the second test, then creates a new client/proxy for the remaining tests and never reaches the teardown condition again (completedbecomes 3 and 4). This defeats reuse and leaks the final runtime/proxy in a full unfiltered run. Set the expected count to 4.
static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard(CATEGORY, 4);
rust/tests/e2e/support.rs:728
- This cleanup path is not actually bounded: it ignores a failed
kill()and then calls blockingwait(). If the startup-timeout/error path cannot terminate the proxy, the test process can still hang indefinitely, contradicting the PR's bounded-shutdown goal. Polltry_wait()only until the cleanup deadline (and report/return after it) instead of using an unconditional wait.
- Files reviewed: 51/51 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Replace blocking waits after proxy termination with deadline-bounded try_wait polling so startup and shutdown failures cannot hang the test process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (2)
rust/tests/e2e/support.rs:1283
- The new startup cleanup covers timeout, channel, and regex-shape failures, but a line matching the regex can still exit through
serde_json::from_str(...)?without killing or waiting forchild; missing metadata fields also panic viaexpectwith the child still alive. Parse and validate the metadata before transferring the child, and on every parse/validation error callkill_and_wait_childbefore returning.
rust/tests/e2e/support.rs:344 - This skipped path bypasses
E2E_CONCURRENCY, even though that semaphore is what guarantees all in-process operations are serialized. If this skipped invocation is the group's last one,shutdown_bounded()can run concurrently with another in-process test while that test has process-wide environment/CWD overrides active. Acquire the same permit before updating the count and possibly tearing down the shared in-process client.
- Files reviewed: 51/51 changed files
- Comments generated: 0 new
- Review effort level: Balanced
The in-process client helper is now used by the E2E target, so its dead-code expectation is unfulfilled under the CI clippy feature set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (1)
rust/tests/e2e/support.rs:170
- Client startup is outside every timeout in this helper.
Client::startawaits unbounded JSON-RPC handshakes (andsessionFs.setProviderfor the SQLite group), so a runtime that starts but stops responding can hang the test binary indefinitely; the dedicated helper previously bounded this call because it ran inside the test closure. Bound this startup too, and ensure the spawned startup task/partially started client is aborted or force-stopped on expiry rather than detaching it.
- Files reviewed: 51/51 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Time out and abort shared client handshakes, serialize skipped in-process teardown, and terminate the proxy on every startup metadata validation failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (1)
rust/tests/e2e/support.rs:174
- If shared client startup fails or times out here, the function panics before
completed_invocationsis incremented. The remaining tests can then only reachexpected_invocations - 1, so their recreated shared client/proxy is never shut down and the proxy can be orphaned when the test binary exits. Advance invocation accounting on every exit path (for example with a guard established before initialization), or explicitly account for startup/setup failures before panicking.
- Files reviewed: 51/51 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Advance shared-group accounting before context and client initialization so failed startup paths cannot prevent a recreated group from reaching final teardown. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (1)
rust/tests/e2e/support.rs:711
- This always allows the replay proxy to write its current capture, including when
with_shared_e2e_contextis tearing the group down after a timeout, panic, setup failure, or cleanup failure. In local recording mode (GITHUB_ACTIONS != true), that replaces the test snapshot with a partial failing capture; the dedicated path andE2eContext::dropavoid this by passingtrue. Thread askip_writing_cacheflag intoshutdown_boundedand set it whenever the test or cleanup did not succeed.
- Files reviewed: 51/51 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed in 8ea6fb1: shared teardown now skips replay-cache writes whenever the test or cleanup fails, while preserving cache writes after successful teardown. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reuse file-scoped Copilot clients for eligible Rust E2E tests while retaining dedicated clients for startup and lifecycle-sensitive cases. Keep shared router tasks on a persistent runtime and bound harness cleanup.
Summary
Why
Rust was starting an expensive Copilot runtime and replay proxy for almost every E2E test. Equivalent Node tests reuse the client within a file and complete dramatically faster.
Focused validation
session::: 37 passed in 16.12scanvas::: 4 passed with reuse in 3.84scommands::: 4 passed in 1.84selicitation::plus related tests: 21 passed in 7.30srpc_event_log::: 4 passed in 2.15srpc_queue::: 2 passed in 2.15smulti_turn::: 2 passed in 2.27smode_empty::: 6 passed in 2.66ssession_fs_sqlite::: 2 passed in 12.74sabort::: 2 passed in 4.04scargo fmt --checkpassesA full local E2E rerun was intentionally not performed after the focused validation because it previously exceeded the useful local feedback window; this draft is intended to collect complete CI timing and correctness data.