Fix SDK test completion races and timeout cleanup - #2642
Conversation
Subscribe before sending the permission E2E prompt so an ephemeral session.idle cannot be lost. Exercise the shared scenario against fake RPC with idle before and after the send reply, reusing the abort regression event fence and preserving the 120-second E2E timeout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The test-only changes correctly eliminate the subscription race and cover both event orderings.
Review tier: Balanced
Findings: None
What changed in this PR
Fixes a race in the .NET approve-all permission E2E test by subscribing before sending.
Changes:
- Uses
SendAndWaitAsyncfor race-free completion. - Adds deterministic early/late idle regression coverage.
- Extracts a reusable fake-server event-delivery fence.
| File | Description |
|---|---|
dotnet/test/E2E/PermissionE2ETests.cs |
Updates and extracts the send/wait scenario. |
dotnet/test/Unit/ClientSessionLifetimeTests.cs |
Adds regression cases and centralizes event fencing. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This comment has been minimized.
This comment has been minimized.
Replace post-send history-backfill waits with a send-and-wait helper that preserves the 120-second budget and requires a current-turn assistant message. Keep SendAsync under test with pre-armed completion observation. Exercise the shared helper with early events and reject missing or previous-turn-only answers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace unsafe post-send waits with sendAndWait or a trigger-based helper that subscribes before work starts. Remove ephemeral-idle history fallback, retain send behavior assertions and test budgets, and add deterministic early-event and cleanup regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Separate live event waiters from reads of already-completed history. Install listeners synchronously before sends, handler release, and abort/recovery operations; preserve caller contexts and required assistant output. Add deterministic fake-RPC regressions with ephemeral idle omitted from history. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use send_and_wait for ordinary turns and synchronously subscribe before sends, aborts, and pending-work operations. Remove ephemeral-idle backfill, retain per-caller timeouts and error policies, and cover early RPC completion plus cancellation cleanup without initializing E2E runtime from unit tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
CI exposed five regression fixtures completing a fake send before trace-context setup reached the RPC handler. Await the existing sendStarted fence so the RPC resolver is installed before delivering its response, without sleeps or timeout changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Capture suspended await chains, pending RPC metadata, session and transport state, and Python thread stacks at the original pytest-timeout signal. Sample native threads for macOS in-process failures and preserve evidence in xdist reports and CI artifacts without changing timeout or failure semantics. Add deterministic diagnostic regressions and subprocess coverage for xdist reporting, including real POSIX signal timeouts during test calls and fixture teardown. Generated by Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Keep ordinary PR and reusable checks unchanged. The manual reproduce_timeout input selects a single macOS/inprocess job, runs up to five complete pytest/xdist suites, and exits on the first failure with its original status and existing diagnostic artifacts. Preserve the 20-minute job budget. Validated extracted shell syntax and injected failures on invocations 1, 3, and 5, plus the five-success path. Generated by Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
pytest-timeout's signal interrupts run_until_complete without cancelling the test coroutine. Its held session disconnect lock can then block module cleanup. Identify exactly that task from the interrupted runner's traceback and schedule cancellation after preserving diagnostics; do not cancel unrelated tasks, force-stop the runtime, or hide the first failure. Add actual-plugin/module-fixture regressions for lock-only cleanup recovery and a still-unresponsive runtime. Both retain the original failed test; the latter still reports teardown failure. POSIX uses the real signal timer and Windows invokes the same plugin handler at the event-loop boundary. Generated by Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Limit the full-suite diagnostic scope to two invocations and add an allowlisted session-config scope with ten invocations. Preserve pytest/xdist options and stop at the first nonzero status. Record started/completed invocations, only mark complete after all pass, and attempt diagnostic artifact upload even after cancellation. Ordinary PR/reusable tests and the 20-minute budgets are unchanged. Validated extracted shell syntax, both count limits, first/middle/last failures, exit 130, process interruption, and rejection of unsupported targets. Generated by Copilot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Keep the existing dotnet test command and its selection unchanged. Record allowlisted build/runtime/test/shutdown progress and owned process metadata, then collect bounded native stack samples and terminate only the owned process group before the job deadline. Retain artifacts on failure, cancellation, and successful diagnostic controls. Add focused watchdog regressions including macOS sampling and POSIX pipe-retention cleanup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
SDK Consistency Review — PR #2642Scope confirmed via API: All 49 changed files are test infrastructure only — E2E test suites, shared test harnesses/helpers ( Cross-SDK consistency assessment:
Conclusion: No cross-SDK API/feature-parity issues found. This is a coordinated, consistent test-infrastructure fix across languages with no public SDK surface changes.
|
Summary
Fix completion-subscription races across SDK test suites, repair a reproducible Python timeout-cleanup defect, and retain diagnostics for intermittent Python and .NET timeouts. SDK product code, dependency manifests, and snapshots are unchanged.
session.idleis ephemeral: subscribing aftersendreturns can permanently miss completion, and durable history cannot recover idle. Starting a goroutine or scheduling an asyncio task is not itself a subscription fence.SendAsyncusing pre-armed observation.sendAndWaitfor ordinary turns and a subscribe-before-trigger helper when testingsendor releasing permission handlers. Clean up listeners on every terminal path.send_and_waitfor ordinary turns; synchronously subscribe before scheduling waiter tasks. Preserve timeouts/error policies across abort, pending-work, mode, and todo tests.Remove unsound idle-history fallback while preserving prompts, session setup, output assertions, and tests of nonblocking send. No forced already-idle flags, sleeps, timeout increases, or assertion weakening.
Confirmed Python cleanup defect
pytest-timeoutcan interruptRunner.runoutside the coroutine, leaving the timed-out test task alive and holding a session-disconnect lock. Module teardown then waits on that abandoned task, causing a second timeout.The harness now preserves diagnostics and cancels exactly the interrupted runner's test task. It does not cancel unrelated tasks, force-stop the runtime, or turn the initial timeout into success. Three actual-plugin/module-fixture controls verify the mechanism: the old behavior reproduces the lock-only teardown stall; the fix removes it; a runtime that continues withholding replies still causes teardown failure.
The original session-config test's initial 300-second timeout is not conclusively attributed to this cleanup defect. Its exact stalled await was not captured in the original log, and no speculative SDK/native repair is claimed.
Diagnostics and reproduction
Python timeout reports capture coroutine/async-generator await chains, pending RPC identifiers/methods, lock/transport state, Python thread stacks, and a bounded macOS native sample before cleanup destroys evidence. RPC payloads and arbitrary frame locals are excluded.
An opt-in manual
reproduce_timeoutdispatch supports two full-suite invocations or tensession-configinvocations with the usual pytest/xdist options. It stops at the first nonzero exit and retains the existing 20-minute job budget. Progress artifacts record each started/completed invocation and mark completion only when all planned invocations pass. Ordinary Python PR/reusable test invocations are unchanged.The .NET macOS/default CAPI shard-1 job later exceeded its 20-minute limit without retrievable logs or artifacts. A watchdog now wraps that cell's unchanged test command and selection, reserving time to retain evidence before the outer job deadline. It records allowlisted phase progress, owned-process metadata, and bounded native call graphs, including when a descendant retains output pipes after
dotnetexits. It preserves the first nonzero exit, reports watchdog expiry as failure, and terminates only its owned process group. The existing per-test blame timeout and outer job budget remain unchanged. Arbitrary console output and RPC payloads are not added to diagnostic artifacts.The earlier .NET hang remains unattributed. The diagnostic run passed without watchdog intervention; this is verified instrumentation and non-reproduction evidence, not a claimed root-cause fix.
Validation
Published revision:
733232b44bcd6f7957399fce7955401df9dfed92.Node.js required, passed.Go requiredand.NET requiredremain failing because of the original Go macOS and .NET Windows timeouts described below. Full-matrix validation is not green.reproduction-summary.txtconfirms all ten completed andreproduction_complete=true.Four full baseline passes and ten targeted passes are non-reproduction evidence, not proof that the initial Python timeout is repaired. Six SDK RPC probes and six proxy-control probes exercised withheld replies, early/late replies, cancellation, large/truncated responses, EOF/recovery, and concurrent shutdown. They confirmed the cleanup defect but did not establish the original timed-out await.
get_exchanges()uses independent HTTPX requests with inactivity timeouts, not a shared stdout response-future map.The expanded race repair used CI as requested. Local validation was limited to later diagnostic/cleanup additions where focused checks were needed. No test-failure reruns-to-green, manual CI cancellations, newly skipped existing SDK tests, or weakened checks were used. Only the two confirmed external download failures received targeted retries. Genuine pushes use the repository's existing PR concurrency behavior.
Outstanding CI timeouts
Both remaining failures are unattributed, not established as PR-caused, pre-existing, or transient infrastructure:
BlobNotFound; the original-attempt log archive contains no entries for this shard, and no Windows diagnostic artifact exists. The combined build/test step began, but its stalled phase, framework, and test remain unknown. Healthy comparator 103746971275 used unchanged .NET source/build inputs and passed 978 tests with six existing skips, including the four new race-regression cases. The macOS-only watchdog does not run in this Windows job, whose command and filter are unchanged.No speculative source or infrastructure changes were made for these two incidents. Further pre-timeout evidence capture is a follow-up; neither timeout is claimed repaired, and neither was retried to obtain a green result.
Preserved failure history and origin
Run 34761514877 passed 49/50 test jobs. Python macOS/inprocess reported 913 passed, one failed, nine existing skips, and one teardown error: unchanged
test_should_apply_systemmessage_on_session_resumetimed out at 300 seconds; module teardown timed out after the available-tools test body passed. The old log lacked actionable async/native stacks and artifacts. Identical Python code passed the preceding run.First expanded run 34760106004 exposed five new Node fixture deadlocks: fake RPC completion preceded installation of its resolver. Commit
df3457bdadded the existingsendStartedfence; all Node configurations then passed. An earlier Go Windows/default 20-minute timeout lacked retained diagnostics; unchanged Go code passed subsequently, without claiming attribution.This follows github/copilot-agent-runtime#20254 and its original optional CAPI C# SDK failure, using #2635's analogous abort-recovery fix as prior art. The original CI incident cannot conclusively be attributed to the reproduced subscription race because runtime logs were not retained.
Historical targeted validation at
fa8fa0d4: both new ordering cases failed with old code, then all 22 focused cases and all 16 permission E2Es passed; net472 build, formatting, and its .NET CI matrix passed. Local E2E used an isolated verified copy of the supplied runtime build, not the exact failing CI binary.Generated by Copilot