Skip to content

Add in-process launch support to the MTP server-mode client - #10898

Merged
Amaury Levé (Evangelink) merged 13 commits into
mainfrom
dev/amauryleve/mtp-in-process-client-launch
Sep 1, 2026
Merged

Add in-process launch support to the MTP server-mode client#10898
Amaury Levé (Evangelink) merged 13 commits into
mainfrom
dev/amauryleve/mtp-in-process-client-launch

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Fixes #10890

Why

MtpServerClient.LaunchAsync(path) owns the loopback listener and starts the test application with Process.Start. That is right for IDE and desktop tooling, but unusable for embedded hosts — MAUI apps, Android/iOS test apps — where the MTP application already runs in the caller's process.

Today those hosts have to reimplement the listener, the --server/host/port arguments, the race between connection and startup failure, the serializer-before-formatter registration order, TcpMessageHandler + MtpJsonRpcConnection construction, and the exit / transport-close / server-completion shutdown dance. That defeats the point of shipping a canonical client and makes it easy to reintroduce bugs this package already solved: partial frame writes, ignored JSON-RPC errors, missing $/cancelRequest, unbounded waits, exception masking.

A concrete consumer is the DeviceRunners MSTest visual-runner work (mattleibow/DeviceRunners#157, #9809).

What

using IMtpServerClient client = await MtpServerClient.LaunchInProcessAsync(
    async (serverArgs, token) =>
    {
        ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(serverArgs);
        builder.AddMSTest(() => testAssemblies);
        using ITestApplication app = await builder.BuildAsync();
        return await app.RunAsync();
    },
    options,
    cancellationToken);

await client.InitializeAsync();
await client.DiscoverTestsAsync();
await client.RunTestsAsync();
await client.ExitAsync();
await client.ShutdownAsync();

The caller supplies only "how to run the application". The client keeps everything else: it binds the listener, generates the complete argument array (--server jsonrpc --client-host 127.0.0.1 --client-port <port> --no-banner), races the connect, builds the transport, and owns a bounded shutdown.

New API on the injected (still internal) surface:

Member Purpose
MtpServerClient.LaunchInProcessAsync(callback, options, ct) The embedded-host launch path. Async only — blocking the launching thread can deadlock the application being launched.
IMtpServerClient.ShutdownAsync() Non-blocking teardown.
IMtpServerClient.ServerExitCode The value the application returned.
MtpServerClientOptions.ServerShutdownTimeout Graceful shutdown bound (default 30s).

Internally, the transport setup was extracted from MtpServerProcess into MtpServerConnector, and both launch paths now sit behind IMtpServerHost, so they cannot drift.

Behavior worth reviewing

Ownership and shutdown. Both Dispose() and ShutdownAsync() join one lazily created shared teardown task, on both hosts. A Dispose that follows or races ShutdownAsync therefore returns only once the server has actually stopped, rather than reporting success while teardown is still running. Teardown runs on the thread pool, so ShutdownAsync never blocks and the synchronous Dispose cannot deadlock against a UI-thread continuation. Task.Run captures the execution context, so the connection's read-loop AsyncLocal marker still flows and disposing from a notification handler does not self-wait (covered by a test asserting < 4s against the connection's 5s read-loop timeout).

Bounded, and actually bounded. Teardown waits ServerShutdownTimeout, then cancels the callback's token, then a fixed 5s grace, then abandons and logs. CancellationTokenSource.Cancel() runs registrations synchronously, so the cancellation is started separately — otherwise a blocking caller registration would prevent the grace from ever starting and make the "bounded" wait unbounded. A failed launch skips the graceful wait entirely: nothing is connected, so there is no transport closure for the callback to observe.

Exception preservation. A callback that throws, is canceled, or returns before dialing back surfaces as MtpServerConnectionClosedException with the original as InnerException, instead of a misleading connection timeout. Every teardown helper is non-throwing, and the shared teardown task is wrapped so it can never fault — a faulted shared task would throw from every later disposal.

LaunchAsync(path) is unchanged. Verified against main: identical argument string, failure messages, stderr capture, exit fast-fail and teardown order. One earlier revision of this branch added a grace period to the accept race; review showed it only traded a precise failure for a vague one, so it was removed and the external path is now provably unchanged.

Browser/WASM. Both paths are loopback TCP. LaunchInProcessAsync fails fast with PlatformNotSupportedException there; this does not enable WASM hosting, and the docs say so.

Tests

  • 68 unit tests (net8.0) / 63 (net462, Jsonite path). New coverage: argument array shape; initialize → discover → run → exit; callback faulting synchronously and asynchronously; callback exiting with a code; null task; pre-canceled launch not invoking the callback; cancellation during connect canceling the callback token; connection timeout bounded by the grace rather than ServerShutdownTimeout; ShutdownAsync; ServerExitCode; disposal awaiting the callback; Dispose racing an in-flight ShutdownAsync; Dispose from a notification handler; idempotent disposal; callback faulting during shutdown; unresponsive callback abandoned within the bound; $/cancelRequest; stateful on/off; multi-request single connection; EnvironmentVariables ignored and warned. Stressed 6×/5× consecutively for flakiness.
  • New acceptance test where a single generated process is simultaneously the embedded host (compiling the packed source-only package) and a real MTP TestApplication with real MSTest over a real [TestClass] — discovering and running its own test over JSON-RPC with no Process.Start anywhere. It also asserts the server's reported process id equals its own.
  • Existing external-process acceptance test extended to cover ShutdownAsync + ServerExitCode; hostile-consumer compile oracle extended to bind the new API on net462/netstandard2.0/net5.0–net8.0.
  • Full regression: 13 platform source-package/consumer acceptance, 4 MSTest acceptance, 13 platform ServerTests. build.cmd -pack clean, 0 warnings.

Review history

Four independent review rounds (MTP/MSTest expert reviewer, a design reviewer, and two correctness reviewers) produced 3 major, 10 moderate and 10 minor findings, all addressed. Several were real bugs the tests then locked in — notably Process.ExitCode being unreadable after Process.Dispose(), a TcpListener socket leak when Start() failed, and a teardown path that could throw from a contract documented never to throw.

Open design questions

  1. No IAsyncDisposable. netstandard2.0/net462 would need Microsoft.Bcl.AsyncInterfaces, breaking the package's dependency-free promise. ShutdownAsync() is the substitute. Happy to revisit if the dependency is acceptable.
  2. MtpServerClientOptions is mode-mixed. EnvironmentVariables is external-process only; ServerShutdownTimeout is in-process only. Nesting per-transport options would age better, but EnvironmentVariables already shipped at the top level, so it cannot be done non-breakingly now.
  3. The 5s cancellation grace is a fixed constant, not an option.
  4. The two paths still build different argument shapes — deliberate, to keep the shipped external command line byte-identical. Commented at the call site.
  5. No ConnectAsync(TcpClient/Stream, options) factory — the existing MtpServerClient(MtpJsonRpcConnection, options) constructor already covers "wrap an existing transport", and the guidance is to minimize injected surface. Easy to add if wanted.

Embedded hosts such as MAUI or Android/iOS test apps cannot spawn a child
process, so `MtpServerClient.LaunchAsync(path)` was unusable for them and they
had to reimplement the listener, the server-mode arguments, the connect race,
the serializer/formatter ordering, the transport and the shutdown coordination
by hand -- reintroducing the bugs the canonical client already solves.

Add `MtpServerClient.LaunchInProcessAsync(callback, options, cancellationToken)`.
The client keeps ownership of everything except "how to run the application":
it binds the loopback listener, generates the complete server-mode argument
array, races the connect against callback failure/completion, caller
cancellation and the connection timeout, and owns a bounded shutdown that
closes the transport, then cancels the callback token, then abandons it rather
than hanging the caller. A callback failure before connection is surfaced as
`MtpServerConnectionClosedException` with the caller's exception preserved as
the inner exception, and teardown failures are only logged so they can never
replace the primary failure.

The shared transport setup is extracted from `MtpServerProcess` into
`MtpServerConnector` and both launch paths now flow through `IMtpServerHost`,
so the external-process behavior is unchanged while the two paths cannot drift.
The one behavior change is an improvement shared by both: when the server is
seen to have stopped, a still-pending accept gets a bounded final grace so a
connection established just before the stop is not discarded in favor of a
misleading "stopped before connecting back" failure.

The path is loopback TCP, so it fails fast with `PlatformNotSupportedException`
on browser/WASM; it does not enable WASM hosting.

Fixes #10890

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two independent reviews of the in-process launch path raised the same top
finding: `Dispose()` waits for the hosted application synchronously on the
calling thread, and on the very platforms this feature targets (MAUI, Android,
iOS) a multi-second block on the UI thread trips the ANR / watchdog. Add
`IMtpServerClient.ShutdownAsync()` (and `IMtpServerHost.ShutdownAsync`) so the
same teardown can be awaited instead. `IAsyncDisposable` stays rejected --
netstandard2.0 and net462 would need `Microsoft.Bcl.AsyncInterfaces`, which
breaks the package's dependency-free promise -- but rejecting the interface is
not a reason to have no asynchronous shutdown at all. `Dispose()` remains, is
still idempotent, and returns immediately after `ShutdownAsync`.

Remove the accept grace period. The previous commit let a still-pending accept
win for ~100ms after the server was seen to have stopped, on the theory that a
connection established just before the stop should not be discarded. That
theory does not hold: if the server has stopped, the socket belongs to a dead
peer, so the grace only traded a precise failure (exit code plus captured
stderr) for a generic connection-closed error on the first request, and its
uncancellable delay could let the server failure beat a concurrent caller
cancellation. Dropping it also makes the external-process path provably
unchanged, so the changelog no longer needs a `Changed` entry.

Other review fixes:

* Expose `IMtpServerClient.ServerExitCode`. The callback signature already
  demanded a `Task<int>`, but after a successful session the value was
  unreachable, so an embedded host whose `Main` must return it had to capture
  it in a closure.
* Do not dispose the server's `CancellationTokenSource` when the callback was
  abandoned while still running: it holds the token, and `token.WaitHandle` or
  `CreateLinkedTokenSource` would then throw `ObjectDisposedException` inside
  the caller's own code.
* Skip the graceful wait entirely on a failed launch. Nothing is connected, so
  there is no transport closure for the callback to observe; only the fixed
  cancellation grace applies and an unwinding caller no longer pays
  `ServerShutdownTimeout`.
* Clamp bounded waits instead of trusting the caller's `TimeSpan`. A negative
  value (or `Timeout.InfiniteTimeSpan`) made `Task.Delay` throw from a path
  documented never to throw; an oversized one is capped to the largest delay
  .NET Framework accepts.
* Move `TcpListener.Start()` inside the cleanup `try`: it creates the socket
  before binding, so a bind failure leaked it because the caller never received
  a listener it could stop.
* Log a late failure from an abandoned callback rather than only observing it.
* Cache the in-process host's `ProcessId` instead of allocating a `Process` per
  property read.
* Document that `Dispose()` blocks, that cancellation is bounded rather than
  immediate, and why the two launch paths still build different argument
  shapes; fix the shadowed `cancellationToken` in the PACKAGE.md sample.

Tests: cover `ShutdownAsync`, `ServerExitCode`, and fail-fast on a server that
stops without connecting; make `Dispose_IsIdempotent` able to fail by asserting
on observed transport closes rather than callback invocations; replace the
hand-rolled throws helper with `Assert.ThrowsExactlyAsync`; tighten the
shutdown-bound assertion; and stop leaking `Process` handles.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A third review round found three ways the teardown contract did not hold up
under concurrency.

`Dispose()` and `ShutdownAsync()` each had their own `Interlocked.Exchange`
guard, so whichever call lost the race returned while teardown was still
running -- reporting to its caller that the application had stopped when it had
not. Replace both guards with one lazily started, shared teardown task: every
entry point now joins the same task, so `Dispose` after (or racing)
`ShutdownAsync` returns only once the application has actually stopped, and the
whole thing stays idempotent. `MtpServerClient` no longer keeps its own guard
either; it delegates to the host, whose teardown is the single source of truth.

`ShutdownAsync()` could still block the very thread it exists to protect: it
closed the transport before its first await, and closing the connection waits
up to five seconds for the read loop. The shared task is started with
`Task.Run`, so the whole teardown -- transport close included -- runs on the
thread pool and `ShutdownAsync` returns immediately. `MtpServerProcess`
likewise now runs its (bounded but synchronous) kill off the calling thread
rather than pretending to be async while blocking.

`CancellationTokenSource.Cancel()` executes registrations synchronously on the
calling thread, so a caller registration that blocked would prevent the
five-second cancellation grace from ever starting and make the "bounded"
shutdown unbounded. Start the cancellation separately and begin the grace
regardless. When a registration is still executing once the callback has
finished, the source is reported as unsafe to dispose for the same reason an
abandoned callback is: leaking one `CancellationTokenSource` beats a
use-after-dispose inside the caller's code.

Adds a test that a `Dispose` racing an in-flight `ShutdownAsync` blocks until
the shared teardown completes, and that both share one teardown.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… fix ExitCode

A fourth review round found that only the in-process host had been converted to
the shared-teardown design. `MtpServerProcess` kept its old early-return guard,
so on the external-process path a `Dispose()` that followed or raced
`ShutdownAsync()` returned while `SafeKill`'s bounded `WaitForExit` was still
running on another thread -- losing exactly the guarantee that wait exists for
(a caller may delete the application directory immediately after disposal), and
making `await ShutdownAsync()` report that the server had stopped when it had
not. That contradicted the comment the previous commit added to
`MtpServerClient.Dispose`, which claimed both paths joined an in-flight
teardown. Convert `MtpServerProcess` to the same lazily created shared task, so
the contract is now uniform across both implementations of `IMtpServerHost`,
and reconcile the interface docs, which previously described the opposite rule.

Adding real coverage for `ShutdownAsync` on the external-process path then
surfaced a genuine bug: `MtpServerProcess.ExitCode` was always `null` after
teardown, because a `Process` cannot be queried once disposed. Capture the exit
code during teardown instead -- before the kill, so an application that already
exited on its own reports its real code rather than the kill's, and before
`Process.Dispose()`, after which nothing is readable.

Also fixes a race in the in-process test fixture that made the suite flaky on
net462: the callback published its `FakeMtpServer` only after its own connect
call returned, but the client's accept can complete first, so a test could
reach `Value` before the callback had set it. The fixture now exposes a
`Connected` signal the launch helper awaits. 6/6 clean net462 runs and 5/5
clean net8.0 runs afterwards.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 31, 2026 19:22
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

GradeTestMutationNotesHow to improve
B (80–89) new MtpServerClientInProcessTests.
Dispose_CallbackFaultsDuringShutdown_
DoesNotThrow
2/3 killed Reads server.Completion.Exception but never asserts on it, so a mutation changing the faulted exception's identity/message survives. Assert the captured exception's message/type instead of discarding it after touching .Exception.
B (80–89) new MtpServerClientInProcessTests.
LaunchInProcessAsync_HonorsTheStatefulOption
3/4 killed Only asserts the sent initialize args; a mutation dropping the negotiated round-trip value on client.Capabilities is not caught. Also assert client.Capabilities.IsStateful reflects the negotiated round-trip value, not only the sent request.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_PassesCompleteServerModeArguments
5/5 killed Verifies exact ordered arguments, dynamic port and fixed count; strong regression guard for the generated argument array.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_DrivesInitializeDiscoverRunAndExit
6/6 killed End-to-end drive through initialize/discover/run/exit with distinct assertions per state transition.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_CallbackFaultsBeforeConnecting_
PreservesCallbackException
2/2 killed Asserts the exact exception instance is preserved as inner exception, not just its type.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_CallbackFaultsAsynchronouslyBeforeConnecting_
PreservesCallbackException
2/2 killed Covers the async-throw variant with the same identity assertion as the sync case.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_CallbackExitsWithoutConnecting_
FailsFastInsteadOfWaitingOutTheTimeout
3/3 killed Checks both the reported exit code in the message and a timing bound guarding against a slow-timeout regression.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_CallbackExitsBeforeConnecting_
ReportsExitCode
2/2 killed Asserts the specific exit code surfaces in the exception message.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_CallbackReturnsNullTask_Fails
2/2 killed Asserts the specific inner exception type for a misbehaving callback returning a null task.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_NullCallback_Throws
1/1 killed Focused single-assertion guard-clause test using the exact exception type.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_AlreadyCanceled_
DoesNotInvokeCallback
2/2 killed Verifies both the cancellation exception and, via an interlocked counter, that the callback never ran.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_CanceledWhileConnecting_
CancelsTheCallbackToken
2/2 killed Confirms both the caller-side cancellation and that the callback's own token observed cancellation.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_ConnectionTimeoutElapses_
FailsWithTimeoutMessage
3/3 killed Distinguishes the connection timeout from the deliberately huge shutdown timeout via message and elapsed-time bound.
A (90–100) new MtpServerClientInProcessTests.
Dispose_ClosesTransportAndAwaitsTheCallback
4/4 killed Checks pre/post completion state, the callback's returned exit code, and the client's reported exit code.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_ClosesTransportAndAwaitsTheCallback_
WithoutBlocking
4/4 killed Verifies await-completion, exit code, a fast follow-up Dispose, and single-teardown count together.
A (90–100) new MtpServerClientInProcessTests.
Dispose_FromANotificationHandler_
DoesNotSelfWaitOnTheReadLoop
1/1 killed Targeted timing assertion guards against a specific re-entrancy deadlock regression.
A (90–100) new MtpServerClientInProcessTests.
Dispose_WhileShutdownAsyncIsInFlight_
WaitsForTheSameTeardown
3/3 killed Uses a controlled release gate to prove the racing Dispose genuinely blocks, then confirms a single shared teardown.
A (90–100) new MtpServerClientInProcessTests.
Dispose_IsIdempotent
3/3 killed Confirms exactly one transport close and one completion despite three Dispose calls, plus a fast-return bound.
A (90–100) new MtpServerClientInProcessTests.
Dispose_CallbackIgnoresShutdown_
ReturnsWithinTheDocumentedBound
2/2 killed Verifies both the documented abandonment time bound and the logged "abandoning" diagnostic.
A (90–100) new MtpServerClientInProcessTests.
RunTestsAsync_Canceled_
SendsCancelRequestToTheHostedApplication
2/2 killed Confirms both the client-side cancellation exception and the wire-level cancel notification reaching the fake server.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_MultipleRequestsOnOneSession_
ReuseTheSameConnection
3/3 killed Checks keep-alive negotiation, single-connection reuse count, and total request count together.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_IgnoresEnvironmentVariablesAndWarns
1/1 killed Asserts the specific warning content naming the ignored option.
A (90–100) new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
5/5 killed Real end-to-end embedded-host run asserting build success, exit code, and each marker line the generated app emits.
A (90–100) mod MtpServerClientAcceptanceTests.
DiscoverAndRun_ViaSourcePackageClient_
ReportsExpectedTestNode
1/1 killed New lines assert the external-process ShutdownAsync/Dispose share a teardown and report an exit code.
A (90–100) mod MtpServerClientSourcePackageConsumerTests.
HostileConsumer_
CompilesAgainstPackedSource
N/A Compile-only guard; added lines correctly extend surface coverage to ShutdownAsync and the in-process launch path.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Suggestions on the Files
changed tab can be applied with one click. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 159.6 AIC · ⌖ 2.87 AIC · ⊞ 16.9K · [◷]( · )

@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 159.6 AIC · ⌖ 2.87 AIC · ⊞ 16.9K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

✅ 22/22 dimensions clean — no findings.

Summary: This PR adds MtpServerClient.LaunchInProcessAsync for embedded hosts (MAUI, Android/iOS) that cannot Process.Start. The implementation is thorough and well-structured:

  • Threading & Concurrency: Shutdown is idempotent via lock + lazy Task, Volatile.Read/Write for cross-thread exit code, Task.Run avoids sync-context deadlocks. No shared mutable state without synchronization.
  • Security & IPC: Loopback TCP only, no command-line injection surface (arguments are array-based), WASM fails fast with PlatformNotSupportedException.
  • Public API: All new API is internal (source-only package). No init accessors. No PublicAPI.Unshipped.txt changes needed.
  • Performance: GetCurrentProcessId() snapshotted once in constructor. No hot-path allocations. WaitBoundedAsync clamps timeouts correctly.
  • Cross-TFM: #if NET8_0_OR_GREATER guard on AcceptTcpClientAsync(CancellationToken). RuntimeInformation used for OS detection on net462.
  • Resource Management: Every disposable (TcpListener, TcpClient, CancellationTokenSource, Process) has cleanup in both success and error paths. Pending accepts are neutralized. ServerCancellation is only disposed when safe.
  • Defensive Coding: Callback exceptions are wrapped as InnerException in MtpServerConnectionClosedException. Null task from callback is caught. Cancel() runs on thread pool to avoid blocking registration.
  • Error Handling: Shared teardown task wrapped in catch-all so it never faults. ObserveFailure prevents UnobservedTaskException. Every teardown helper is non-throwing.
  • Tests: 68 unit tests + acceptance tests covering argument shape, lifecycle, faults, cancellation, timeouts, idempotent disposal, notification-handler disposal, and the real end-to-end in-process path.
  • Documentation: Changelog, protocol intro, PACKAGE.md all updated. XML doc comments are thorough.

The refactoring of shared transport logic into MtpServerConnector and the IMtpServerHost abstraction is clean and prevents the two launch paths from drifting. The existing LaunchAsync(path) path is provably unchanged (same argument string, same failure messages, same teardown order).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: 3 Medium severity

New issues introduced by this change (3)
Severity Finding
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.csMtpJsonRpcConnection.Dispose() closes the socket and then waits up to its 5-second read-loop…
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.csShutdownAsync writes this nullable property from the teardown worker while callers can read…
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — A canceled callback takes this branch without an inner exception, although the package…
What changed in this PR

Adds in-process launch support to the source-only MTP server-mode client for embedded/mobile hosts, addressing #10890 and enabling scenarios related to #9809.

Changes:

  • Adds callback-based in-process hosting with shared transport setup.
  • Adds bounded asynchronous shutdown and server exit-code reporting.
  • Adds documentation, unit coverage, and real-MSTest acceptance coverage.
File Description
MtpServerClientInProcessTests.cs Tests in-process lifecycle and protocol behavior.
FakeMtpServer.cs Supports server-to-client dial-back mode.
MtpServerClientInProcessAcceptanceTests.cs Exercises a real in-process MSTest application.
MtpServerClientAcceptanceTests.cs Covers external-process shutdown and exit code.
MtpServerClientSourcePackageConsumerTests.cs Extends source-package compile coverage.
PACKAGE.md Documents embedded-host usage and lifecycle.
MtpServerProcess.cs Adopts shared host and shutdown abstractions.
MtpServerInProcessHost.cs Implements callback hosting and teardown.
MtpServerConnector.cs Centralizes listener and transport setup.
MtpServerClientOptions.cs Adds the shutdown timeout option.
MtpServerClientExceptions.cs Supports preserved inner exceptions.
MtpServerClient.cs Exposes in-process launch and shutdown.
IMtpServerHost.cs Defines common host ownership behavior.
IMtpServerClient.cs Adds shutdown and exit-code members.
001-protocol-intro.md Documents reference-client launch modes.
Changelog-Platform.md Records the new embedded-host capability.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@Evangelink Amaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Sep 1, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 11:24
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

All test methods reviewed are grade A or B; no new high-confidence actionable findings beyond those already raised (and fixed in 8bc3036) by prior review passes on this PR.

GradeTestMutationNotesHow to improve
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
PassesCompleteServerModeArguments
3/3 killed Asserts the exact ordered argument array and validates the dynamic port slot precisely.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
DrivesInitializeDiscoverRunAndExit
5/5 killed Exercises the full session lifecycle and verifies process-id identity plus per-uid state transitions.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsBeforeConnecting_
PreservesCallbackException
2/2 killed Asserts reference identity of the inner exception, so a rethrow-as-new-exception mutation is caught.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsAsynchronouslyBeforeConnecting_
PreservesCallbackException
2/2 killed Covers the async-fault path distinctly from the sync-fault test, both preserving exception identity.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackIsCanceledBeforeConnecting_
PreservesCancellationException
2/2 killed Verifies the inner exception is a real TaskCanceledException carrying its originating Task.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackExitsWithoutConnecting_
FailsFastInsteadOfWaitingOutTheTimeout
3/3 killed Bounds elapsed time with a stopwatch and checks the reported exit code, avoiding a hard-coded ms pattern.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackExitsBeforeConnecting_
ReportsExitCode
2/2 killed Asserts the precise exit code surfaces in the exception message rather than a generic timeout message.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackReturnsNullTask_
Fails
2/2 killed Confirms a misbehaving callback (null Task) is normalized into a well-typed client exception.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
NullCallback_
Throws
1/1 killed Simple, focused argument-validation guard test.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
AlreadyCanceled_
DoesNotInvokeCallback
2/2 killed Uses an interlocked counter to prove the callback never runs, not just that an exception was thrown.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CanceledWhileConnecting_
CancelsTheCallbackToken
3/3 killed Verifies both the caller-side OperationCanceledException and that the callback's own token was actually canceled.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
ConnectionTimeoutElapses_
FailsWithTimeoutMessage
3/3 killed Distinguishes the connection-timeout bound from the unrelated (deliberately large) ServerShutdownTimeout.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
ClosesTransportAndAwaitsTheCallback
4/4 killed Checks pre/post state for both the fixture's completion task and the client's own ServerExitCode.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_
ClosesTransportAndAwaitsTheCallback_
WithoutBlocking
3/3 killed Also verifies the follow-up Dispose is a fast no-op and that teardown ran exactly once (CompletionCount).
A (90–100) new MtpServerClientInProcessTests.
Dispose_
FromANotificationHandler_
DoesNotSelfWaitOnTheReadLoop
2/2 killed Regression-shaped test targeting a specific reentrancy hazard (AsyncLocal marker across thread-pool hop).
A (90–100) new MtpServerClientInProcessTests.
Dispose_
WhileShutdownAsyncIsInFlight_
WaitsForTheSameTeardown
3/3 killed Proves Dispose blocks until the concurrent ShutdownAsync completes, and both share one teardown via CompletionCount.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
IsIdempotent
3/3 killed Checks transport-close count and completion count both stay at 1 across repeated Dispose calls.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
CallbackFaultsDuringShutdown_
DoesNotThrow
3/3 killed Verifies the exact exception type/message on the faulted Completion task and that it is logged, not rethrown.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
CallbackAndNotificationHandlerIgnoreShutdown_
ReturnsWithinTheDocumentedBound
3/3 killed Tight timing bound (under 9s) targets the documented overlap of read-loop wait, ServerShutdownTimeout and cancellation grace, and checks the "abandoning it" log line.
A (90–100) new MtpServerClientInProcessTests.
RunTestsAsync_
Canceled_
SendsCancelRequestToTheHostedApplication
2/2 killed Confirms both the caller-visible cancellation and the wire-level cancel notification actually being sent.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
HonorsTheStatefulOption
4/4 killed Data-driven over both booleans; asserts the exact ClientInfo.Name, ProcessId and negotiated Capabilities identity.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
MultipleRequestsOnOneSession_
ReuseTheSameConnection
2/2 killed Verifies connection reuse via ConnectionCount and the exact request count across four calls.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
IgnoresEnvironmentVariablesAndWarns
1/1 killed Confirms the warning names the exact option being dropped.
A (90–100) new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
N/A Genuine end-to-end acceptance test: same process is embedded host and MTP application, with no Process.Start; asserts on discrete marker lines and exit code, avoiding brittle output parsing.
B (80–89) mod MtpServerClientAcceptanceTests.
DiscoverAndRun_
ViaSourcePackageClient_
ReportsExpectedTestNodes
N/A New ShutdownAsync/ServerExitCode assertions only check non-null, not the exact exit code value. Assert the exact expected exit code (e.g. 0) instead of only IsNotNull.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Suggestions on the Files
changed tab can be applied with one click. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 107.6 AIC · ⌖ 2.87 AIC · ⊞ 16.9K · [◷]( · )

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 107.6 AIC · ⌖ 2.87 AIC · ⊞ 16.9K ·

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: 1 High severity · 3 Medium severity

New issues introduced by this change (4)
Severity Finding
High severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerClient.cs — For clients created with the existing-connection constructor, ShutdownAsync() calls…
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — The boundedness here specifically depends on Cancel() not running inline, but the new tests only…
Medium severity test/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientAcceptanceTests.cs — This only proves that some exit code was captured, not that the external-process path preserved the…
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — The exact callback exit-code contract is only exercised with connected callbacks that return 0;…
Issues resolved since last review (3)
Severity Finding
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — A canceled callback takes this branch without an inner exception, although the package… View resolved comment
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.csShutdownAsync writes this nullable property from the teardown worker while callers can read… View resolved comment
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.csMtpJsonRpcConnection.Dispose() closes the socket and then waits up to its 5-second read-loop… View resolved comment

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 12:51
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

GradeTestMutationNotesHow to improve
B (80–89) new MtpServerClientInProcessTests.
Dispose_
BlockedHandlersAndCallback_
ReturnsWithinTheDocumentedBound
3/3 killed Correctly asserts the documented abandon-bound, cancellation propagation, and logging, but bundles three distinct contracts into one ~80-line test. Split into three focused tests: one for the timing bound, one for cancellation-token propagation, one for the log message.
A (90–100) mod MtpServerClientAcceptanceTests.
DiscoverAndRun_
ViaSourcePackageClient_
ReportsExpectedTestNode
1/1 killed Asserts the exact clean exit code after ShutdownAsync, matching the process-path teardown contract.
A (90–100) new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
N/A End-to-end acceptance test verifies discovery, run, and exit-code contract for the in-process host path.
A (90–100) MtpServerClientTests.
Dispose_
CalledFromNotificationHandler_
DoesNotSelfWaitOnTheReadLoop
1/1 killed Bounded elapsed-time assertion directly targets the re-entrancy self-wait regression.
A (90–100) MtpServerClientTests.
ShutdownAsync_
WithBlockedNotificationHandler_
ReturnsWithoutBlockingTheCaller
1/1 killed Verifies ShutdownAsync's Task.Run-based non-blocking teardown against a blocked handler.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
PassesCompleteServerModeArguments
2/2 killed Ordered sequence assertion plus exact argument count protects the CLI-argument contract.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
DrivesInitializeDiscoverRunAndExit
3/3 killed Full lifecycle test asserts capabilities, process id, and both discovered/passed node updates.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsBeforeConnecting_
PreservesCallbackException
1/1 killed AreSame on the inner exception verifies the exact instance is preserved, not just its type.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsAsynchronouslyBeforeConnecting_
PreservesCallbackException
1/1 killed Covers the async-fault variant of the callback-exception-preservation contract.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackIsCanceledBeforeConnecting_
PreservesCancellationException
1/1 killed Type-checks the inner cancellation exception and its Task property, not a broad catch.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackExitsWithoutConnecting_
FailsFastInsteadOfWaitingOutTheTimeout
2/2 killed Elapsed-time bound plus message assertion together kill both the fast-fail and message mutations.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackExitsBeforeConnecting_
ReportsExitCode
1/1 killed Exact exit-code substring assertion protects the reported-exit-code contract.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackReturnsNullTask_
Fails
1/1 killed Type-checks the inner exception for the misbehaving-callback edge case.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
NullCallback_
Throws
1/1 killed Exact exception type via ThrowsExactlyAsync guards the null-argument contract.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
AlreadyCanceled_
DoesNotInvokeCallback
1/1 killed Interlocked counter proves the callback body never ran, not merely that an exception surfaced.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CanceledWhileConnecting_
CancelsTheCallbackToken
2/2 killed Confirms both the caller-side cancellation exception and the server-side token propagation.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
ConnectionTimeoutElapses_
FailsWithTimeoutMessage
2/2 killed Bounded elapsed time isolates the connection-timeout path from the unrelated shutdown-timeout value.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
ClosesTransportAndAwaitsTheCallback
2/2 killed Verifies both the awaited completion and the exact exit code surfaced after Dispose.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_
ClosesTransportAndAwaitsTheCallback_
WithoutBlocking
3/3 killed Chains completion, exit code, and a bounded-time follow-up Dispose to prove the no-op re-teardown contract.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_
PreservesNonzeroCallbackExitCode
1/1 killed Exact nonzero exit-code assertion rules out a hard-coded zero-return mutation.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
FromANotificationHandler_
DoesNotSelfWaitOnTheReadLoop
1/1 killed Bounded elapsed time directly targets the AsyncLocal re-entrancy marker regression.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
WhileShutdownAsyncIsInFlight_
WaitsForTheSameTeardown
2/2 killed Proves both that Dispose blocks on the in-flight teardown and that only one teardown ever runs.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
IsIdempotent
2/2 killed Counts transport-close and completion exactly once across three Dispose calls.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
CallbackFaultsDuringShutdown_
DoesNotThrow
2/2 killed Checks the faulted task's base exception and the logged message rather than only that Dispose didn't throw.
A (90–100) new MtpServerClientInProcessTests.
RunTestsAsync_
Canceled_
SendsCancelRequestToTheHostedApplication
1/1 killed Confirms both the caller-visible cancellation and the wire-level cancel notification.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
HonorsTheStatefulOption
3/3 killed DataRow covers both stateful values and checks the actual serialized capability flag reaches the server.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
MultipleRequestsOnOneSession_
ReuseTheSameConnection
2/2 killed Exact connection count plus exact received-request count protects the single-connection reuse contract.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
IgnoresEnvironmentVariablesAndWarns
1/1 killed Asserts the warning names the ignored option, not just that some log line exists.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Suggestions on the Files
changed tab can be applied with one click. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 210.3 AIC · ⌖ 2.87 AIC · ⊞ 16.9K · [◷]( · )

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: 2 Medium severity

New issues introduced by this change (2)
Severity Finding
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.csSafeKill may forcibly terminate a still-running child, after which this second read publishes the…
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerProcess.cs — This adds a new five-second graceful wait before killing an unresponsive external process.…
Issues resolved since last review (4)
Severity Finding
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — The exact callback exit-code contract is only exercised with connected callbacks that return 0;… View resolved comment
Medium severity test/​IntegrationTests/​MSTest.Acceptance.IntegrationTests/​MtpServerClientAcceptanceTests.cs — This only proves that some exit code was captured, not that the external-process path preserved the… View resolved comment
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — The boundedness here specifically depends on Cancel() not running inline, but the new tests only… View resolved comment
High severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerClient.cs — For clients created with the existing-connection constructor, ShutdownAsync() calls… View resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:398

  • A callback fault after it has connected is only logged here; the shared teardown task completes successfully, so neither ShutdownAsync nor Dispose propagates the failure. This conflicts with issue #10890's acceptance criterion that callback failures before and after connection are propagated. Preserve non-throwing Dispose if needed, but provide an explicit way for ShutdownAsync or another client contract to surface the original post-connect callback exception, or revise the acceptance criterion.
        MtpServerConnector.ObserveFailure(serverTask, logger, "The in-process MTP application failed");
        return stopped;

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 13:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (1)

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:307

  • A callback that cancels after connecting is silently treated as successful teardown because this only rethrows faulted tasks. That contradicts the documented contract that ShutdownAsync rethrows a post-connect callback failure; the same callback cancellation is already preserved before connection. Distinguish cancellation that occurred before teardown from cancellation caused by _serverCancellation, rethrow the former as TaskCanceledException, and cover a connected callback that cancels itself before ShutdownAsync.
        if (_serverTask.IsFaulted)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 16:45
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

GradeTestMutationNotesHow to improve
B (80–89) mod MtpServerClientAcceptanceTests.
ShutdownAsync_
WhileExternalTeardownBlocks_
ReturnsImmediatelyAndHidesForcedExitCode
3/4 killed Proves async shutdown and forced-exit masking, but never verifies the blocked discover was actually in flight. Assert the server received testing/discoverTests before calling ShutdownAsync.
B (80–89) mod MtpServerClientSourcePackageConsumerTests.
HostileConsumer_
CompilesAgainstPackedSource
2/3 killed Good compile-oracle test, but only observes overall build success, not which API surface failed on drift. Assert output contains each target framework build success to catch partial-TFM regressions.
B (80–89) mod MtpServerClientTests.
ShutdownAsync_
WithBlockedNotificationHandler_
ReturnsWithoutBlockingTheCaller
3/4 killed Proves caller-side non-blocking shutdown, but never asserts the client actually detached/closed afterward. After awaiting shutdown, assert the fake server observes disconnect or exit notification completion.
B (80–89) new MtpServerClientInProcessTests.
Dispose_
FromANotificationHandler_
DoesNotSelfWaitOnTheReadLoop
3/4 killed Verifies re-entrant dispose stays bounded, but does not prove teardown fully completed after the handler. Assert callback completion / ServerExitCode after the handler-triggered dispose (inline suggestion posted).
B (80–89) new MtpServerClientInProcessTests.
ShutdownAsync_
CallbackHonorsTeardownCancellationThroughLinkedToken_
DoesNotThrow
3/4 killed Good linked-token coverage, but only checks a null exit code and not that shutdown completed via cancellation. Assert the callback-connected server disconnects to prove transport-driven shutdown really happened.
B (80–89) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
MultipleRequestsOnOneSession_
ReuseTheSameConnection
3/4 killed Reuse is checked by connection/request count only; a wrong request-type mix could still pass. Assert the recorded methods explicitly (initialize/discover/run/run-with-filter, in order) (inline suggestion posted).
B (80–89) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
IgnoresEnvironmentVariablesAndWarns
2/3 killed Checks warning presence, but not that launch still succeeds and connects despite ignoring the variables. Initialize the client and assert the callback received a connection after the warning.
A (90–100) mod MtpServerClientAcceptanceTests.
DiscoverAndRun_
ViaSourcePackageClient_
ReportsExpectedTestNode
5/5 killed Covers discover/run end-to-end and asserts exact observed node states on both sessions.
A (90–100) new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
6/6 killed Strong acceptance oracle checks same-process hosting, discover, run, and final clean shutdown markers.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
WhileShutdownAsyncIsInFlight_
WaitsForTheSameTeardown
5/5 killed Exercises the shared teardown task precisely and checks both non-early completion and single execution.
A (90–100) new MtpServerClientInProcessTests.
Dispose_IsIdempotent
5/5 killed Asserts first-close effects, repeated no-op behavior, and bounded repeated disposal time.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
CallbackFaultsDuringShutdown_
DoesNotThrow
5/5 killed Validates disposal swallows callback failure but still surfaces it through observable faulted state.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_
CallbackFaultsDuringShutdown_
PropagatesCallbackException
4/4 killed Exact-type and same-instance assertions tightly protect the post-connect failure propagation contract.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_
CallbackCancelsItselfAfterConnecting_
PropagatesCancellation
4/4 killed Confirms shutdown rethrows real callback cancellation, including preservation of the original token.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
BlockedHandlersAndCallback_
ReturnsWithinTheDocumentedBound
6/6 killed Strong adversarial teardown test covers overlapping waits, cancellation dispatch, and abandonment logging.
A (90–100) new MtpServerClientInProcessTests.
RunTestsAsync_
Canceled_
SendsCancelRequestToTheHostedApplication
4/4 killed Cancellation path is well pinned by both client-side task cancellation and wire-level cancel notification.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
HonorsTheStatefulOption
5/5 killed Data-driven test checks handshake payload, retained capabilities, and process identity for both option values.

Note: FakeMtpServer.cs was also touched but is test infrastructure (no [TestMethod]s), so it was not graded. Its WaitForRequestAsync/WaitForNotificationAsync helpers still poll via Task.Delay(15) rather than being fully event-driven, which is worth a look if any tests using it become flaky.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Suggestions on the Files
changed tab can be applied with one click. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 137.2 AIC · ⌖ 2.93 AIC · ⊞ 16.9K · [◷]( · )

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 137.2 AIC · ⌖ 2.93 AIC · ⊞ 16.9K ·

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: None

Suppressed comments (6)

Previously missed (2) — in code that hasn't changed since the last review.

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerProcess.cs:495

  • null represents both “not captured yet” and “forced termination”, so ExitCode falls back to reading the live Process whenever this write publishes null. While SafeKill is completing—and again before _process.Dispose()—a concurrent ServerExitCode read can therefore expose the OS kill status, contrary to the documented forced-termination contract. Introduce an explicit captured-without-exit-code sentinel/state, publish it before killing, and have the getter return null rather than probing the process once teardown has begun.
            Volatile.Write(ref _capturedExitCode, exitCode ?? (!killed ? TryReadExitCode() : null));

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:126

  • The source-only package's pack transform performs a raw Microsoft.Testing.Platform replacement (Microsoft.Testing.Platform.ServerMode.Client.Sources.csproj:354), including inside string literals. Packed consumers therefore see “Microsoft.Testing.Platform.ServerMode.Client.Protocol application” here. Use “MTP application” so the platform-not-supported diagnostic remains readable after packing.

This issue also appears in the following locations of the same file:

  • line 160
  • line 353
  • line 362
  • line 367
            throw new PlatformNotSupportedException(
                "Hosting a Microsoft.Testing.Platform application in process requires a loopback TCP listener, "
                + "which is not available on browser/WASM.");

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:162

  • The pack-time namespace relocation is a raw text replacement (Microsoft.Testing.Platform.ServerMode.Client.Sources.csproj:354), so this new timeout message is emitted to package consumers as “The in-process Microsoft.Testing.Platform.ServerMode.Client.Protocol application…”. Use the MTP abbreviation to keep the diagnostic intact.
                () => new MtpServerConnectionClosedException(
                    $"The in-process Microsoft.Testing.Platform application did not connect back within {options.ConnectionTimeout.TotalSeconds:N0}s. "
                    + "Make sure the callback forwards the supplied server-mode arguments to the test application."),

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:355

  • In the packed source, the raw namespace transform rewrites this diagnostic's product name to Microsoft.Testing.Platform.ServerMode.Client.Protocol. Use “MTP application” so callback-cancellation failures remain clear to actual package consumers.
            return new MtpServerConnectionClosedException(
                "The in-process Microsoft.Testing.Platform application was canceled before connecting back.",
                new TaskCanceledException(serverTask));

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:369

  • Because packing applies namespace relocation as raw text, this exit message identifies the application as Microsoft.Testing.Platform.ServerMode.Client.Protocol in downstream builds. Use “MTP application” to preserve the intended diagnostic.
        return new MtpServerConnectionClosedException(
            $"The in-process Microsoft.Testing.Platform application exited with code {serverTask.Result} before connecting back. "
            + "Make sure the callback forwards the supplied server-mode arguments to the test application.");

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:364

  • The package transform rewrites Microsoft.Testing.Platform in string literals, so packed consumers receive a misleading Microsoft.Testing.Platform.ServerMode.Client.Protocol application failure message here. Use “MTP application” instead.
            return new MtpServerConnectionClosedException(
                "The in-process Microsoft.Testing.Platform application failed before connecting back.",
                failure);

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 17:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: None

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

GradeTestMutationNotesHow to improve
D (60–69) new MtpServerClientInProcessTests.
Dispose_
WhileShutdownAsyncIsInFlight_
WaitsForTheSameTeardown
3/4 killed Relies on a fixed `Task.Delay(200)` to prove Dispose hasn't returned yet — a timing race. Poll with short retries or use a signal set when the callback observes the block instead of a fixed delay.
B (80–89) new MtpServerClientInProcessTests.
Dispose_
BlockedHandlersAndCallback_
ReturnsWithinTheDocumentedBound
4/5 killed Correctly uses `[DoNotParallelize]` and Stopwatch bounds; log-message check is somewhat loose. Assert the exact abandonment log message content rather than a substring match.
B (80–89) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
ConnectionTimeoutElapses_
FailsWithTimeoutMessage
3/3 killed Stopwatch-based timing bound plus message assertion, well isolated.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
PassesCompleteServerModeArguments
4/4 killed Asserts every expected CLI argument is present with correct values.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
DrivesInitializeDiscoverRunAndExit
5/5 killed Exercises the full happy-path lifecycle and asserts node results and exit code.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
NullCallback_Throws
1/1 killed Focused ArgumentNullException guard test.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsBeforeConnecting_
PreservesCallbackException
3/3 killed Verifies the original exception instance/type propagates through the awaited task.
A (90–100) new MtpServerClientInProcessTests.
ShutdownAsync_
WithBlockedNotificationHandler_
ReturnsWithoutBlockingTheCaller
3/3 killed Confirms ShutdownAsync doesn't self-deadlock on its own notification handler.
A (90–100) mod MtpServerClientAcceptanceTests.
DiscoverAndRun_
ViaSourcePackageClient_
ReportsExpectedTestNode
N/A End-to-end acceptance coverage of the source-package client against a real host process; long body is expected/exempt for integration tests.
A (90–100) new MtpServerClientAcceptanceTests.
ShutdownAsync_
WhileExternalTeardownBlocks_
ReturnsImmediatelyAndHidesForcedExitCode
N/A Validates the documented non-blocking shutdown contract against a real external process.
A (90–100) new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
N/A Confirms the in-process host path discovers/runs real MSTest nodes without spawning a process; length is exempt per integration-test convention.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Suggestions on the Files
changed tab can be applied with one click. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 191.4 AIC · ⌖ 2.63 AIC · ⊞ 16.9K · [◷]( · )

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 191.4 AIC · ⌖ 2.63 AIC · ⊞ 16.9K ·

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 17:37
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

All 25 new/modified test methods across 6 files were reviewed (the in-process MTP host launch path: MtpServerClient.LaunchInProcessAsync, its shared teardown semantics, and the matching acceptance coverage). This is an exceptionally disciplined test suite: every test isolates one behavior, asserts on meaningful values/timing bounds/log content rather than tautologies, and production code was resolvable for every case (MtpServerInProcessHost, MtpServerConnector, MtpJsonRpcConnection). No Critical/High anti-pattern findings (no swallowed exceptions, no unseeded wall-clock waits used for synchronization — timing assertions are one-sided upper bounds guarding against regressions, which is idiomatic here). No inline suggestions were warranted; nothing scored below A.

GradeTestMutationNotesHow to improve
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
PassesCompleteServerModeArguments
3/3 killedAsserts exact ordered argument prefix, parses the dynamic port, and checks total arg count.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
DrivesInitializeDiscoverRunAndExit
4/4 killedEnd-to-end happy path verifies capabilities, process id, discovered/passed node states, and exit notification.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsBeforeConnecting_
PreservesCallbackException
1/1 killedAsserts the exact inner exception instance is preserved, not just its type.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsAsynchronouslyBeforeConnecting_
PreservesCallbackException
1/1 killedCovers the async-fault-before-connect variant distinctly from the sync one.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackIsCanceledBeforeConnecting_
PreservesCancellationException
2/2 killedVerifies both the exception type and the underlying canceled task are surfaced.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackExitsWithoutConnecting_
FailsFastInsteadOfWaitingOutTheTimeout
2/2 killedAsserts both the exit-code message and a fast-fail upper bound distinguishing it from timeout behavior.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackExitsBeforeConnecting_
ReportsExitCode
1/1 killedChecks the exact exit code is embedded in the failure message.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackReturnsNullTask_Fails
1/1 killedCovers a misbehaving callback contract (null task) distinct from a thrown exception.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
NullCallback_Throws
1/1 killedGuard-clause test with precise exception type via ThrowsExactlyAsync.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
AlreadyCanceled_
DoesNotInvokeCallback
1/1 killedProves the callback is never invoked (interlocked counter), not just that the call throws.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CanceledWhileConnecting_
CancelsTheCallbackToken
2/2 killedConfirms both the caller's OperationCanceledException and the callback's own token cancellation.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
ConnectionTimeoutElapses_
FailsWithTimeoutMessage
2/2 killedUses a large ServerShutdownTimeout as a control to prove the timeout, not the shutdown grace, bounds the wait.
A (90–100)new MtpServerClientInProcessTests.
Dispose_
ClosesTransportAndAwaitsTheCallback
3/3 killedVerifies pre/post completion state, awaited callback result, and captured exit code together.
A (90–100)new MtpServerClientInProcessTests.
ShutdownAsync_
ClosesTransportAndAwaitsTheCallback_
WithoutBlocking
3/3 killedAlso proves the follow-up Dispose after ShutdownAsync is a fast idempotent no-op.
A (90–100)new MtpServerClientInProcessTests.
ShutdownAsync_
PreservesNonzeroCallbackExitCode
1/1 killedDistinguishes exact non-zero exit code propagation from the zero-code happy path above.
A (90–100)new MtpServerClientInProcessTests.
Dispose_
FromANotificationHandler_
DoesNotSelfWaitOnTheReadLoop
2/2 killedRegression test for re-entrant disposal deadlock; asserts both a tight timing bound and completion.
A (90–100)new MtpServerClientInProcessTests.
Dispose_
WhileShutdownAsyncIsInFlight_
WaitsForTheSameTeardown
2/2 killedProves the racing Dispose call actually blocks (polls up to 10x) before releasing, not just that it eventually finishes.
A (90–100)new MtpServerClientInProcessTests.
Dispose_IsIdempotent
3/3 killedChecks transport-close count, completion count, and a fast-return bound all stay stable across repeats.
A (90–100)new MtpServerClientInProcessTests.
Dispose_
CallbackFaultsDuringShutdown_
DoesNotThrow
2/2 killedAsserts the fault is captured on the completion task and separately logged, and Dispose itself does not rethrow.
A (90–100)new MtpServerClientInProcessTests.
ShutdownAsync_
CallbackFaultsDuringShutdown_
PropagatesCallbackException
1/1 killedConfirms ShutdownAsync (unlike Dispose) rethrows the same exception instance.
A (90–100)new MtpServerClientInProcessTests.
ShutdownAsync_
CallbackCancelsItselfAfterConnecting_
PropagatesCancellation
1/1 killedVerifies the propagated exception carries the callback's own cancellation token, not a synthesized one.
A (90–100)new MtpServerClientInProcessTests.
ShutdownAsync_
CallbackHonorsTeardownCancellationThroughLinkedToken_
DoesNotThrow
1/1 killedExercises a linked-token cooperative-cancellation path distinct from the direct-token variant above.
A (90–100)new MtpServerClientInProcessTests.
Dispose_
BlockedHandlersAndCallback_
ReturnsWithinTheDocumentedBound
3/3 killedMarked [DoNotParallelize] appropriately for its overlapping-timeout measurement; asserts bound, cancellation delivery, and log text together.
A (90–100)new MtpServerClientInProcessTests.
RunTestsAsync_Canceled_
SendsCancelRequestToTheHostedApplication
2/2 killedConfirms both the client-side cancellation exception and the wire-level cancel notification reaching the server.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
HonorsTheStatefulOption
2/2 killedData-driven over both boolean states; asserts the negotiated capability plus client name and process id forwarding.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
MultipleRequestsOnOneSession_
ReuseTheSameConnection
2/2 killedAsserts both the connection-reuse count and the exact ordered sequence of received RPC methods.
A (90–100)new MtpServerClientInProcessTests.
LaunchInProcessAsync_
IgnoresEnvironmentVariablesAndWarns
1/1 killedConfirms the ignored-environment-variables warning is actually logged, matching production's opt-in guard.
A (90–100)mod MtpServerClientTests.
ShutdownAsync_
WithBlockedNotificationHandler_
ReturnsWithoutBlockingTheCaller
2/2 killedExternal-process counterpart proving ShutdownAsync call itself never blocks even with a stuck handler.
A (90–100)mod MtpServerClientAcceptanceTests.
DiscoverAndRun_
ViaSourcePackageClient_
ReportsExpectedTestNodes
N/AReal end-to-end process launch; new assertions confirm the process actually exits and ShutdownAsync joins cleanly.
A (90–100)new MtpServerClientAcceptanceTests.
ShutdownAsync_
WhileExternalTeardownBlocks_
ReturnsImmediatelyAndHidesForcedExitCode
N/AReal process test proving forced-kill exit status never leaks as an application exit code.
A (90–100)new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
N/AThe only test that can prove the no-Process.Start constraint; asserts build success plus 4 distinct marker lines.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 152.8 AIC · ⌖ 2.91 AIC · ⊞ 16.9K · [◷]( · )

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — The guarantee that the callback is never invoked inline is not covered with a callback that blocks…
Suppressed comments (1)

src/Platform/Microsoft.Testing.Platform.ServerMode.Client.Sources/Client/MtpServerInProcessHost.cs:25

  • The fixed post-cancellation grace is not behaviorally covered. The current linked-token test only checks that shutdown does not throw and leaves the exit code null, so changing this value to zero would still pass while ShutdownAsync could return before a callback finishes cooperative cleanup. Add a test whose callback observes cancellation and then waits on a release gate; verify shutdown remains incomplete until that gate is released within the grace period.
    private static readonly TimeSpan CancellationGrace = TimeSpan.FromSeconds(5);

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 1, 2026 17:59
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧵 Parallel-safety audit — PR #10898

Parallelization — one row per test assembly audited:

Test assembly Scope Workers Analyzer coverage
Microsoft.Testing.Platform.ServerMode.Client.Sources.UnitTests MethodLevel CPU count (Workers = 0) coverable once the parallel-safety analyzers ship (assembly attribute in Program.cs)
MSTest.Acceptance.IntegrationTests MethodLevel CPU count (Workers = 0) coverable once the parallel-safety analyzers ship (assembly attribute, unchanged by this PR)
Microsoft.Testing.Platform.Acceptance.IntegrationTests MethodLevel CPU count (Workers = 0) coverable once the parallel-safety analyzers ship (assembly attribute, unchanged by this PR)

Findings: A (global-state) 0 · B (paths) 0 · C (declaration) 0 · D (over-serialization) 0 — by severity: Critical 0 · High 0 · Warning 0 · Info 0.

No parallel-safety hazards found. Details:

  • New unit-test file MtpServerClientInProcessTests.cs (942 lines, entirely new). All fixtures rely on process-local, per-instance state: TaskCompletionSources created fresh per test method, an ephemeral TCP loopback port obtained via new TcpListener(IPAddress.Loopback, 0) in FakeMtpServer (port 0 ⇒ OS-assigned, no fixed-port collision), and the sole class-level static (CurrentProcessId) is readonly. The one [DoNotParallelize] (Dispose_BlockedHandlersAndCallback_ReturnsWithinTheDocumentedBound) is correctly justified — it measures overlapping timeout windows, so thread-pool contention from concurrent siblings would corrupt its own timing assertion; this is textbook-correct use, not over-serialization (a narrower [ResourceLock] wouldn't help here since the hazard is CPU/thread-pool contention, not a named shared resource).
  • FakeMtpServer.cs changes add a ConnectBackTo(host, port) dial-back constructor and a Disconnected completion signal; both are per-instance, no shared mutable state introduced.
  • MtpServerClientTests.cs — new test ShutdownAsync_WithBlockedNotificationHandler_ReturnsWithoutBlockingTheCaller follows the same per-instance TaskCompletionSource pattern as its neighbors; no hazard.
  • MtpServerClientAcceptanceTests.cs / MtpServerClientInProcessAcceptanceTests.cs (new) — the new options.EnvironmentVariables["MTP_SERVER_BLOCK_AFTER_RUN"] is consumed via ProcessStartInfo.EnvironmentVariables for the launched child process only (MtpServerProcess.cs:203), not Environment.SetEnvironmentVariable on the current process — not a category-A finding. The in-process acceptance test uses TestAsset.GenerateAssetAsync, the standard per-invocation-unique asset directory pattern used throughout this test suite — no path collision.
  • MtpServerClientSourcePackageConsumerTests.cs — additive driver-code changes (ServerShutdownTimeout, ShutdownAsync() calls, a new DriveInProcessAsync helper); no new shared state.

No production Program.cs / assembly-attribute / .runsettings file changed parallelization scope in this PR — all three affected assemblies remain at their pre-existing MethodLevel opt-in.

Advisory only — heuristic, non-blocking. Re-run with /parallel-audit. This audit answers "is it parallel-safe?"; for testability, smells, or flakiness see the detect-static-dependencies / test-smell-detection / test-anti-patterns analyses.

🤖 Automated content by GitHub Copilot. Generated by the Parallel-safety audit on PR (on open / sync) workflow. · auto · 96.9 AIC · ⌖ 7.44 AIC · ⊞ 24.8K · [◷]( · )

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

Review tier: Balanced
Findings: None

Issues resolved since last review (1)
Severity Finding
Medium severity src/​Platform/​Microsoft.Testing.Platform.ServerMode.Client.Sources/​Client/​MtpServerInProcessHost.cs — The guarantee that the callback is never invoked inline is not covered with a callback that blocks… View resolved comment

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10898

This PR adds the in-process launch path for MtpServerClient (LaunchInProcessAsync) plus a large new unit-test suite (MtpServerClientInProcessTests.cs, ~26 tests) exercising argument passing, callback fault/cancellation handling, dispose/shutdown idempotency and timing bounds, stateful session reuse, and environment-variable warnings, plus a new integration test (MtpServerClientInProcessAcceptanceTests.cs) and small additions to the external-process acceptance/unit suites. Overall the new tests are strong: they use a real wire-protocol fake server (FakeMtpServer), assert concrete exit codes/exceptions/timing bounds, and correctly apply [DoNotParallelize] where a shared timing budget matters.

GradeTestMutationNotesHow to improve
C (70–79) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
IgnoresEnvironmentVariablesAndWarns
1/2 killed Asserts a warning is logged but doesn't verify environment variables are actually absent from the in-process call. Assert the exact warning message content and that no env-var mutation reaches the callback.
B (80–89) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
MultipleRequestsOnOneSession_
ReuseTheSameConnection
2/3 killed Confirms connection reuse but not the ordering of requests/responses across the session. Assert the sequence of requests observed by the fake server, not just connection identity.
B (80–89) new MtpServerClientInProcessTests.
Dispose_
BlockedHandlersAndCallback_
ReturnsWithinTheDocumentedBound
2/3 killed Timing-bound assertion is inherently loose; correctly isolated with [DoNotParallelize]. Tighten the timing assertion window if the documented bound allows a narrower margin.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
PassesCompleteServerModeArguments
4/4 killed Verifies exact argv content passed to the hosted entry point.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
DrivesInitializeDiscoverRunAndExit
4/4 killed Exercises and asserts the full protocol handshake sequence end-to-end.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackFaultsBeforeConnecting_
PreservesCallbackException
3/3 killed Asserts the exact exception instance/type propagated from a failing callback.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
CallbackIsCanceledBeforeConnecting_
PreservesCancellationException
3/3 killed Correctly distinguishes cancellation from generic failure.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
ConnectionTimeoutElapses_
FailsWithTimeoutMessage
3/3 killed Asserts the specific timeout failure message and elapsed-bound behavior.
A (90–100) new MtpServerClientInProcessTests.
Dispose_
IsIdempotent
2/2 killed Directly verifies repeated Dispose calls are safe, matching the documented contract.
A (90–100) new MtpServerClientInProcessTests.
LaunchInProcessAsync_
HonorsTheStatefulOption
2/2 killed Data-driven coverage of both stateful states with a concrete capability assertion.
A (90–100) new MtpServerClientInProcessAcceptanceTests.
InProcessHost_
DiscoversAndRunsItsOwnMSTestNodes_
WithoutStartingAProcess
N/A End-to-end acceptance oracle; asserts exit code and console markers from a generated in-process host asset.
A (90–100) new MtpServerClientTests.
Dispose_
CalledFromNotificationHandler_
DoesNotSelfWaitOnTheReadLoop
2/2 killed Targets a genuine deadlock-avoidance regression scenario with a concrete completion assertion.
A (90–100) mod MtpServerClientAcceptanceTests.
ShutdownAsync_
WhileExternalTeardownBlocks_
ReturnsImmediatelyAndHidesForcedExitCode
N/A Real external-process oracle for the forced-exit-code hiding contract during teardown.

The remaining ~13 new tests in MtpServerClientInProcessTests.cs (callback-blocks-before-returning, null-task, null-callback, already-canceled, canceled-while-connecting, shutdown/dispose fault-during-shutdown variants, RunTestsAsync cancellation forwarding) follow the same solid pattern: concrete exception/state assertions against a real wire-protocol fake server, correctly isolated where timing matters. No high-confidence actionable findings were identified for them beyond the two noted above.

This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with /review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 190.7 AIC · ⌖ 2.75 AIC · ⊞ 16.9K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit 86727c2 into main Sep 1, 2026
39 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mtp-in-process-client-launch branch September 1, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add in-process launch support to the MTP server-mode client

3 participants