Imported from microsoft/vstest (
AGENTS.md). Install upstream withnpx skills add microsoft/vstest. Copyright stays with the author.
AGENTS.md — vstest
This file contains project-specific knowledge for AI agents working on the VSTest test platform.
Repository Overview
VSTest is the test platform that powers dotnet test, Visual Studio Test Explorer, and Azure DevOps test tasks. It discovers and executes tests written with MSTest, xUnit, NUnit, and other frameworks.
Architecture
vstest.console (entry point)
├── TestRequestManager (orchestration)
│ ├── ProxyDiscoveryManager ──IPC──► testhost (discovery)
│ ├── ProxyExecutionManager ──IPC──► testhost (execution)
│ └── ProxyOperationManager (shared IPC logic)
├── Translation Layer (bridges old/new handler interfaces for VS/AzDO)
└── Data Collectors (code coverage, blame, etc.)
Key IPC boundary: vstest.console and testhost communicate via JSON-RPC over stdin/stdout. Wire format changes must be backward-compatible.
Key Directories
| Directory | Purpose | Sensitivity |
|---|---|---|
src/Microsoft.TestPlatform.ObjectModel/ |
Public API surface (NuGet-shipped) | Binary compat critical |
src/Microsoft.TestPlatform.CommunicationUtilities/ |
JSON-RPC protocol, serialization | Wire compat critical |
src/Microsoft.TestPlatform.CrossPlatEngine/ |
Execution engine, parallel scheduling | Thread safety critical |
src/Microsoft.TestPlatform.CoreUtilities/ |
Shared utilities | Hot-path perf critical |
src/vstest.console/ |
CLI entry point, arg parsing, app.config | Binding redirects |
src/testhost*/ |
Test host processes | Assembly loading |
src/datacollector/ |
Data collector host | Binding redirects |
src/Microsoft.TestPlatform.Client/ |
Client-side test management | |
src/Microsoft.TestPlatform.Common/ |
Shared platform logic | |
src/Microsoft.TestPlatform.Extensions.*/ |
Loggers (HTML, TRX, blame) |
Build
| Action | Windows | Linux / macOS |
|---|---|---|
| Restore + Build | ./build.cmd |
./build.sh |
| Build + Pack | ./build.cmd -pack |
./build.sh --pack |
| Release config | ./build.cmd -c Release -pack |
./build.sh -c Release --pack |
| Unit tests | ./test.cmd |
./test.sh |
| Specific tests | ./test.cmd -projects <path-or-glob> |
./test.sh --projects <path-or-glob> |
| Several test projects | ./test.cmd -projects "test\A\A.csproj;test\B\B.csproj" |
./test.sh --projects "test/A/A.csproj;test/B/B.csproj" |
| Smoke tests | ./test.cmd -smokeTest |
./test.sh --integrationTest (smoke is a subset) |
| Single test by name | ./test.cmd -c Release -filter "FullyQualifiedName~TestName" |
./test.sh --property:'TestRunnerAdditionalArguments=--filter "FullyQualifiedName~TestName"' |
CI builds use -c Release. Always build with Release config before submitting PRs.
-projects is resolved with Resolve-Path, so it takes a path or a glob. A bare nickname such as smoke or htmllogger fails with Cannot find path. Test categories are switches instead: -smokeTest, -integrationTest, -compatibilityTest, -performanceTest.
Filtering by test name is a Windows-only wrapper parameter. test.cmd goes through eng/build.ps1, which owns -filter and the category switches; test.sh calls Arcade's eng/common/build.sh directly and has neither, so on Linux/macOS the filter is passed as an MSBuild property. Do not pass the filter as /p:TestRunnerAdditionalArguments="--filter ..." on Windows — eng/build.ps1 throws "Use --filter instead of passing filter as an additional argument to TestRunnerAdditionalArguments." See CONTRIBUTING.md for the Linux/macOS quoting rules.
test.cmd does not set DOTNET_ROOT, so test executables built against the repo's preview TFM fail with "You must install or update .NET to run this application." This hits unit tests too, not only integration and smoke tests. Set it first:
$env:DOTNET_ROOT = "$PWD\.dotnet"
test.cmd does not restore either. On a fresh clone, build once (./build.cmd -c Release) before running it, or pass -restore -build, otherwise it stops with Toolset version <version> has not been restored.
The .cmd wrappers pass arguments to PowerShell literally. See Wrapper scripts pass arguments literally before changing one.
.github/skills/vstest-build-test/SKILL.md is the fuller build and test reference. Keep it and this section in agreement.
Test Structure
Test projects mirror source projects under test/:
src/Microsoft.TestPlatform.ObjectModel/ → test/Microsoft.TestPlatform.ObjectModel.UnitTests/
src/Microsoft.TestPlatform.CrossPlatEngine/ → test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/
src/vstest.console/ → test/vstest.console.UnitTests/
Test categories: Unit (fast, default), Smoke (P0 e2e), Acceptance (full e2e with --integrationTest).
Known Gotchas
Wrapper scripts pass arguments literally
build.cmd, test.cmd, restore.cmd, open-vs.cmd, open-code.cmd, and eng/RestoreInternal.cmd invoke PowerShell with -File, so everything after the script path reaches the target script as a literal argument. The first five call eng/build.ps1, eng/RestoreInternal.cmd calls eng/common/build.ps1.
They previously used the form -command "& """<script>""" %*", which spliced %* into a string that PowerShell then parsed as source code. That caused two problems, both fixed:
;in an argument became a statement separator../test.cmd -projects "test\A\A.csproj;test\B\B.csproj"ranBuild.ps1 -projects test\A\A.csprojand then executedtest\B\B.csprojas its own statement. On Windows.csprojis file-associated with Visual Studio, so every entry after the first opened a full IDE. Three agents ran this form at the same time and opened eighteen instances of Visual Studio.- Exit codes collapsed to
1.eng/build.ps1ends withexit $LastExitCodeto forward the real code, but-commanddiscarded it, so8(filter matched no tests) and every other code arrived as1.
Use -File in any new .cmd wrapper. Do not switch back to -command.
Because arguments are no longer re-parsed, one level of quoting is enough instead of two, for wrapper parameters and /p: MSBuild properties alike. A single pair of quotes now reaches the target script intact:
./test.cmd -c Release -projects "test\Microsoft.TestPlatform.CrossPlatEngine.UnitTests\Microsoft.TestPlatform.CrossPlatEngine.UnitTests.csproj" -filter "FullyQualifiedName~MtpProxyExecutionManagerTests"
eng/common/* comes from Arcade and still uses -command. Do not edit those files here; fix them in the Arcade repository.
Independent of the wrappers: never run a .csproj or .sln path as a command. The path is always an argument, as in dotnet test <path>.csproj.
Binding Redirects
Bumping a netstandard2.0 package cascades: transitive deps need binding redirects in ALL three app.configs (vstest.console/app.config, testhost.x86/app.config, datacollector/app.config). Miss one and you get FileLoadException in net462 DTA hosts.
Package Verification
After packaging changes, regenerate eng/expected-nupkg-file-counts.json and eng/expected-dll-frameworks.json from a clean Release build. Never hand-edit these files.
Assert.Contains
Assert.Contains(expected, actual) — first param is the needle. This is the opposite of old StringAssert. This has been a recurring mistake.
Localization
*.xlf files must be manually edited to match .resx changes. They are NOT auto-generated by the build.
CI
- CI runs on Azure DevOps, not GitHub Actions
DOTNET_ROLL_FORWARD=LatestMajormasks version mismatches — don't rely on it- Doc-only PRs skip CI builds
- Windows builds finish first (~15 min), macOS/Ubuntu take longer
Git Workflow
- Never commit to
main - Never force-push PR branches — squash-merge at the end
- Push to fork remote, PR against
microsoft/vstest - Don't create draft PRs — undrafting forces a rebuild
Agentic Workflows (gh-aw)
- Use
gh aw secrets setto manage secrets, NOTgh secret set. Plaingh secret setcreates the repo secret but gh-aw can't see it. - Auth is company-token first — no long-lived personal PATs. Copilot inference uses the
copilot-requests: writepermission (billed to the org Copilot subscription), soCOPILOT_GITHUB_TOKENis no longer referenced by any compiled workflow. Write-backs use an org-owned GitHub App (APP_IDvariable +APP_PRIVATE_KEYsecret) withignore-if-missing: true, falling back toGITHUB_TOKENuntil an org admin provisions it. See.github/workflows/README.mdfor the full secrets table and rationale (the Microsoft OSS enterprise now 403s fine-grained PATs older than 8 days). lockdown:has been removed repo-wide (deprecated upstream); workflows keepmin-integrity: noneand use the defaultGITHUB_TOKENfor MCP reads.- Workflow source files are
.mdin.github/workflows/. Compiled.lock.ymlfiles are generated — don't hand-edit them. - To recompile after editing a workflow:
gh aw compilefrom the repo root. .github/*andAGENTS.mdare excluded from CI path triggers — editing workflows won't trigger a full build.