Run this during implementation, not only during review. Keep it bounded to the requested change and its reachable conflicts; do not turn it into a repository audit or speculative hardening.
Reuse the canonical analysis
Read .github/copilot-instructions.md, the owning project's configuration and
BannedSymbols.txt when present, and
.github/workflows/shared/parallel-safety-audit-shared.md.
Apply the shared rubric from # Parallel-safety audit through Step 3, including
its assembly-scoping rules. It owns scheduling semantics, the A-D taxonomy,
severity, confidence, culture/ExecutionContext distinctions, and declaration
reconciliation; do not duplicate or replace it.
Read .github/agents/parallel-safety-reviewer.agent.md for the specialist's
evidence and partial-analysis contract. No specialist invocation or GitHub
publication is required. Ignore review-only read-only instructions, event
expressions, extraction-tool assumptions, output formatting, and safe-output
calls: use the actual implementation scope and local diff instead. For existing
branch changes, compare against their merge base, not a moving base-branch tip.
Before editing: establish the isolation contract
-
Resolve the effective test engine, target frameworks, assembly scheduling scope, workers, and configuration sources using Step 0. Include constructors, initialization, cleanup, disposal, fixtures, linked sources, and production helpers reached by the affected tests. Do not assume that every project uses MSTest or that an attribute is the effective runtime configuration.
-
Record the existing resource coverage matrix in working notes, not a new repository planning file:
Resource and boundary Lifetime Direct test readers/writers Production readers/writers Other tests reaching those paths Declaration: key, mode, placement Exact state, path, source, or registry; assembly/process/external Per-test, class, assembly, or worker Named tests and lifecycle members Concrete helpers and call paths Named observers or mutators in the owning assembly Existing coverage or a confirmed gap Trace through production helpers and back to every reachable caller in the owning test assembly. A literal-key search is an inventory aid, not proof. Before claiming a race, name a concrete conflicting observer, the scheduling interleaving, and the observable wrong result. If no conflict is established, do not add a lock merely because a mutation exists; mark uncertainty or readiness-only concerns as such.
-
Choose elimination or the narrowest correct shared read/write declaration using categories C and D. Reuse the existing key and shared constants; include indirect readers and compatible producers, not only direct mutations. Justify method versus class placement against the whole lifecycle. Preserve assembly-level safety; a narrower method/class lock must not silently opt an assembly in or replace assembly-lifetime exclusion.
For a requested scheduling change, expand to the whole affected assembly as the shared rubric requires. Otherwise leave assembly scheduling unchanged. If coverage is incomplete, retain the existing exclusion and report the gap rather than serializing unrelated tests or weakening safety to finish a patch.
While implementing: close the assertion and lifetime boundaries
- Global observers: trace activity, metric, logging, and event listeners back
to every producer they can observe. A lock around a captured list only makes
the collection thread-safe; it does not prove an event belongs to this test.
Prefer a per-test identity filter and assert the intended producer. If an
unfiltered observer is necessary, reconcile all interfering producers with
the same coordination key. Compatible filtered tests/producers can share
Mode = ResourceAccessMode.Readwhen the established protocol permits it; use exclusive access only where required. Do not turn an entire class intoReadWritemerely because one method needs exclusion. - Inherited state: enumerate the environment values the exercised production
paths actually consult, including inherited CI gates such as
TF_BUILD. Explicitly arrange required present and absent values using existing fakes or child-process environment overrides where possible. A lock does not neutralize CI inheritance. When real process state must change, capture exact previous values (including absence), restore them infinallyinside the protected lifetime, and reconcile readers. Restore changed culture and static state too; use the rubric's target-framework/ExecutionContext distinctions rather than treating all current-culture changes as process-global races. - Rendezvous and cleanup: use deterministic started/ready/release signals,
not sleeps or elapsed-time guesses. Keep handles to the original worker tasks,
not only cancellation/timeout wrappers. In
finally, release blocked workers and request cancellation as appropriate, then await their termination before disposing gates/listeners, restoring shared state, or releasing test ownership. A release signal, cancellation request, or "reached finally" marker is not completion. Bound rendezvous and cleanup separately with named limits and target-framework-compatible existing helpers; cleanup must not immediately abandon the join because the test's cancellation token is already canceled. Surface worker faults and cleanup timeouts without hiding the original test failure. If in-process workers cannot terminate within the bound, use an existing isolated-process harness or keep the attempted refactor out; do not leave background mutation running and declare the test isolated.
Before finishing: reconcile the final diff
Rebuild the matrix from the final implementation, including removals of locks, opt-outs, restoration, or cleanup. Verify that each named conflicting observer is covered and that compatible readers remain concurrent. Missing evidence is partial, never clean. Do not remove an exclusion when any row is unknown.
Use the smallest existing build and focused tests for the affected behavior, following the repository-pinned toolchain and packing rules. Include the named conflicting tests and the relevant normal, assertion-failure, cancellation, and timeout cleanup paths, not just the changed method. Preserve the actual assembly scheduling in validation; a serial filtered run or passing stress run is not proof that a race is impossible. When environment-dependent behavior is in scope, cover the required present/absent CI values without changing unrelated host state.
Keep a concise handoff: assembly scope/workers, concrete conflict and declaration choice (or why none is needed), rendezvous/termination evidence, exact validation commands and results, and unresolved evidence. This skill does not authorize publication, unrelated fixes, or changing assembly scheduling.
Repository-backed examples
These are analysis examples, not requests to patch the referenced tests. Recheck their implementation at HEAD before reusing a pattern.
| Scenario | Preflight decision and reference |
|---|---|
A test writes ProcessKilledByHangDump; another calls the crash handler without mentioning that slot. |
Trace CrashDumpProcessLifetimeHandler.OnTestHostProcessExitedAsync to AppDomain.CurrentDomain.GetData. For example, the writer in CrashDumpTests could make CrashDumpArtifactPublisherMutationTests.OnTestHostProcessExitedAsync_MissingDumpPattern_ThrowsInvariantViolation skip its expected exception if exclusion were removed. Preserve the matching readers, writer's exclusive declaration, and exact restoration. |
| A raw activity listener could accept another test's builder event. | In OpenTelemetryProviderExtensionsTests, AddTestingPlatformDiagnostics_RawListenerObservesBuilderActivityWithoutProvider needs exclusion from the producer DiagnosticsAndProviderRegistration_IsIdempotentAndOrdered, not just a lock on its captured list. They use matching exclusive/read source-key declarations. OpenTelemetryPlatformServiceTests also uses a unique activity prefix and a compatible class-level read declaration; do not serialize all readers. |
| A test expects no CI resource attributes while running on an Azure Pipelines agent. | Inspect the environment read set, not the developer machine. OpenTelemetryProviderExtensionsTests.WithEnvironment / WithEnvironmentAsync explicitly clear observed values including TF_BUILD, then restore the snapshot. Prefer the existing fake when testing configuration alone. |
| A test's timeout/cancellation wrapper completes while its worker is still active. | src/Platform/Microsoft.Testing.Platform/Helpers/TaskExtensions.cs separates cancellation of the wait from completion of the original task. Retain and join that original worker after releasing its rendezvous, with bounded failure cleanup; observing the wrapper or a pre-exit marker is insufficient. |