vstest
AGENTS.md976 starsChanged 6 months agoMITView on GitHub
This file contains project-specific knowledge for AI agents working on the VSTest test platform. 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. Key IPC boundary: vstest.console and testhost communicate via JSON-RPC over stdin/stdout. Wire format changes must be backward-compatible. CI builds use -c Release. Always build with Release config before submitting PRs. -projects is resolved with Resolve-Path,…
- Reads credentials
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository7 other files it gives its agents.
- eng/common/AGENTS.mdAGENTS.md
- .github/copilot-instructions.mdCopilot instructions
- creating-skillsSkill
- expert-reviewingSkill
- trx-analysisSkill
- validate-skillsSkill
- vstest-build-testSkill
The file
We split this AGENTS.md into the rules it gives an agent
8 rules. About 0 are common, 3 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 71
Test projects mirror source projects under test/:
- Very specific: line 79
Test categories: Unit (fast, default), Smoke (P0 e2e), Acceptance (full e2e with --integrationTest).
- Very specific: line 92
Use -File in any new .cmd wrapper.
- Very specific: line 110
After packaging changes, regenerate eng/expected-nupkg-file-counts.json and eng/expected-dll-frameworks.json from a clean Release build.
- Distinctive: line 129
Never commit to main
- Distinctive: line 130
Never force-push PR branches — squash-merge at the end
- Distinctive: line 132
Don't create draft PRs — undrafting forces a rebuild
- Very specific: line 136
Use gh aw secrets set to manage secrets, NOT gh secret set.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

