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>
This comment has been minimized.
This comment has been minimized.
Retain goroutine and bounded managed/native stack evidence before the outer job timeout. Reuse the watchdog with owned Windows Job Object cleanup and preserve test selection, assertions, timeouts, and original failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Use the existing synthetic inference response helpers and observe the completed turn before saving and listing session metadata. Preserve nonempty discriminator assertions and always stop the per-test client when detach fails. Add shared-scenario ordering and cleanup controls. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Restore all three SDK workflows to the PR base and remove the now-unused .NET and Go watchdogs, stack collectors, diagnostic TestMain, supporting tests and documentation. Remove the Python manual reproduction dispatch documentation. Keep the completion fixes, Python interrupted-task cleanup and their regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
SDK Consistency Review — PR #2642Scope of change: This PR touches only test infrastructure (E2E test harnesses/helpers and their call sites) across Node.js, Python, Go, and .NET, plus a Python README note and What changed: Each touched language's E2E test helper (
Cross-SDK consistency assessment: ✅ No parity issues found.
Conclusion: This PR is a well-scoped, test-only race-condition fix applied uniformly to the languages that had the affected harness pattern. It does not introduce any single-language feature or behavior change that needs to be mirrored elsewhere. No inline review comments needed.
|
Summary
Fix completion-subscription races in SDK tests and two Python test-lifecycle defects, preserving the APIs under test, caller budgets, and output assertions.
All workflow files are unchanged from the PR base. At the author's request,
7f5737e9removes the investigation-only workflow edits, .NET/Go watchdogs and stack collectors, diagnostic GoTestMain, associated diagnostic-only tests/tool manifest, and reproduction documentation. Original CI commands, matrices, deadlines, and artifact behavior are restored. The actual completion fixes and Python interrupted-task cleanup remain.SDK product sources, runtime pins, and snapshots are unchanged.
Current head:
7f5737e9087f3a1b74d53a6297f66ef504a80acd. SDK run 34789202978 is pending. The preceding revision77127f86passed all 61 SDK jobs and all 82 reported PR checks; those results are historical, not a current-head green claim.Completion-subscription repair
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 caller timeouts/error policies across abort, pending-work, mode, and todo tests.Fake-RPC regressions force early terminal events before send replies, with idle omitted from durable history. Preserve prompts, session setup, output assertions, and nonblocking-send coverage. No forced already-idle flags, sleeps, timeout increases, or assertion weakening.
Python test-lifecycle repairs
Abandoned task after pytest-timeout
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 preserves the timeout report 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. Actual-plugin/module-fixture controls prove that old behavior reproduces the lock-only teardown stall, the fix removes it, and a runtime that continues withholding replies still causes teardown failure.
The active Python harness still reports suspended coroutine await chains, pending RPC methods/identifiers, lock/transport state, and bounded thread diagnostics before cancellation. Payloads and arbitrary locals are excluded. These reports remain in pytest output/local files; the experimental workflow uploader and manual repetition modes have been removed.
The original session-config test's initial 300-second timeout is not conclusively attributed to this cleanup defect. Its exact stalled await was absent from the original log. Six SDK RPC and six proxy probes confirmed the cleanup defect but did not identify that primary await. The proxy's HTTPX calls do not share the SDK's stdout response-future map.
Complete the persisted-session fixture before cleanup
Python Alpine ARM64/inprocess job 103801413979 failed
test_should_list_find_and_inspect_persisted_session_state. Diagnostics identifysession.disconnect()in the test'sfinally, awaitingsession.detachfor 299.922 seconds. This test creates an explicit stdio client even in the inprocess matrix cell; the shared fixture's FFI state is not the blocked connection.The fixture enqueued an uncontrolled model turn merely to make persisted-session listings nonempty, then inspected metadata and detached without observing completion. It now reuses existing synthetic inference-response helpers, awaits
send_and_waitwith the same prompt and a 60-second completion budget, verifies the synthetic assistant response, and then saves/lists metadata. All nonempty-list, exact-session,LocalSessionMetadataValue, andis_remote=Falsediscriminator assertions remain. The obsolete persistence polling loop is removed. A nestedfinallyalso stops the per-test client if detach raises, without suppressing that detach failure.Two controls invoke this exact E2E scenario with a controlled client: both fail against the original fixture and pass after repair, proving completion-before-save and cleanup-on-detach-failure. This fixes the fixture's unobserved inference/cleanup overlap; it does not claim to identify or repair the native reason that the original detach reply was withheld.
Validation
TestMain/package (empty test selection; not a full E2E run). Worktree clean and branch pushed.77127f86: SDK run 34787031867 passed all 61 jobs, including Python Alpine ARM64/inprocess. All 82 reported PR checks passed. No retry.fa8fa0d4: both new ordering cases fail with old code; all 22 focused cases and all 16 permission E2Es pass after repair. net472 build, formatting, and .NET CI passed. Local E2E used an isolated verified copy of the supplied runtime build, not the exact failing CI binary.Full-suite validation uses CI as requested. No existing SDK tests were disabled or assertions weakened. Diagnostic-only controls were removed together with the reverted investigation infrastructure. No test-failure reruns-to-green or manual CI cancellations were used; only the two confirmed external download failures below received targeted retries.
Preserved investigation history and limitations
The removed instrumentation and its evidence remain in commit history/session artifacts. Passing subsequent runs do not establish the primary cause of earlier unattributed hangs.
c9aee7d9df3457bdadded the existingsendStartedfence before completing fake sends; subsequent Node configurations passed. An earlier Go Windows/default timeout later passed unchanged without attribution.The removed diagnostics were independently validated before removal: 21 local Node watchdog controls (four existing POSIX-only controls inapplicable on Windows), seven Go focused test functions, Go vet, a zero-warning Windows helper build, and actual macOS/Windows CI checks. Their removal restores the original CI behavior rather than claiming the diagnostics themselves fixed an underlying runtime hang.
Origin
This follows github/copilot-agent-runtime#20254 and its original optional CAPI C# SDK failure, using #2635's 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.
Generated by Copilot