with one click
apex-migration
>-
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
>-
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Reviews a NuGet/NuGet.Client pull request like a senior maintainer — reviews in an isolated workspace by default, or against the reviewer's existing clone (self-review / local changes), builds and tests the affected projects, traces changes across files, and returns high-signal, severity-tagged findings with a merge verdict. Invoke explicitly with a PR number or branch.
Cherry-pick (backport/forward-port) one or more commits from one branch onto another, creating a fresh branch and opening a pull request that follows the repository's conventions. Use this whenever the user wants to move, port, backport, or cherry-pick a commit or a merged PR to a release/servicing branch (e.g. "cherry-pick the latest dev commit to 7.9.x", "backport #7576 to release/8.0.x", "port that fix to the release branch"), even if they don't say the exact word "cherry-pick". If the cherry-pick hits conflicts, STOP and report back instead of trying to resolve them, unless the user explicitly asked you to resolve conflicts.
Create or update a pull request using the NuGet.Client PR template. Use when asked to: create PR, open PR, push and create PR, submit PR, open pull request, send changes for review, update PR description, fix PR body, edit PR description, update the PR.
| name | apex-migration |
| description | >- |
PowerShell E2E tests live in test/EndToEnd/tests/. Apex tests live in
test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/. The goal is to migrate PS tests
to C# Apex tests that run the exact same scenario, then remove the PS test function.
Cmdlets/ (see Unit test file placement below).get_errors or build the project to confirm it compiles cleanly.Choose the target file by interaction surface, not project type:
| Interaction surface | Apex file |
|---|---|
Get-Package command scenarios (-ListAvailable, -Updates, -Filter, path source variants, prerelease behaviors) | GetPackageTestCase.cs |
| Other PMC commands (Install-Package, Update-Package, Uninstall-Package, Get-Project) | NuGetConsoleTestCase.cs |
| NuGet UI / Package Manager dialog | NuGetUITestCase.cs |
| IVsPackageInstaller / IVsServices API | IVsServicesTestCase.cs |
| Sync/binding redirect scenarios | SyncPackageTestCase.cs |
| Audit / vulnerability scenarios | NuGetAuditTests.cs |
| .NET Core project-creation / restore / source-mapping | NetCoreProjectTestCase.cs |
PMC tests for PackageReference projects still follow command-based placement. For Get-Package
scenarios use GetPackageTestCase.cs; for other PMC commands use NuGetConsoleTestCase.cs.
| PowerShell function | Apex ProjectTemplate | Package management | Verified |
|---|---|---|---|
New-ConsoleApplication | ProjectTemplate.ConsoleApplication | packages.config | ✅ |
New-ClassLibrary | ProjectTemplate.ClassLibrary | packages.config | ✅ |
New-WebSite | ProjectTemplate.WebSiteEmpty | packages.config | ✅ |
New-WebApplication | ProjectTemplate.WebApplicationEmpty | packages.config | ❌ |
New-WPFApplication | ProjectTemplate.WPFApplication | packages.config | ❌ |
New-MvcApplication | ProjectTemplate.WebApplicationEmptyMvc | packages.config | ❌ |
New-FSharpLibrary | ProjectTemplate.FSharpLibrary | PackageReference | ❌ |
New-NetCoreConsoleApp | ProjectTemplate.NetCoreConsoleApp | PackageReference | ✅ |
New-NetStandardClassLib | ProjectTemplate.NetStandardClassLib | PackageReference | ✅ |
| PowerShell function | Apex equivalent |
|---|---|
New-SolutionFolder 'Name' | testContext.SolutionService.AddSolutionFolder("Name") |
The package management style determines which assertion methods to use — packages.config projects
use AssertPackageInPackagesConfig, while PackageReference projects use AssertPackageInAssetsFile.
Note: This table covers the most common PS project factories. Some PS tests use specialized factories like
New-ClassLibraryNET46,New-BuildIntegratedProj,New-UwpPackageRefClassLibrary, orNew-NetCoreConsoleMultipleTargetFrameworksApp. These don't have a 1:1ProjectTemplateenum value — check the ApexProjectTemplateenum and existing tests for the closest match, or create a standard template and modify the csproj afterward (e.g., for multi-targeting).
| Scenario | Apex API |
|---|---|
Standard install with -Version | nugetConsole.InstallPackageFromPMC(packageName, packageVersion) |
Install with extra flags (-Source, -WhatIf, -IgnoreDependencies) | nugetConsole.Execute($"Install-Package {packageName} -ProjectName {project.Name} -Source {source}") |
| Standard uninstall | nugetConsole.UninstallPackageFromPMC(packageName) |
Standard update with -Version | nugetConsole.UpdatePackageFromPMC(packageName, packageVersion) |
Update with -Safe, -Reinstall, etc. | nugetConsole.Execute($"Update-Package {packageName} -Safe") |
| Any raw PMC command | nugetConsole.Execute(command) |
Key rule: Both InstallPackageFromPMC() and UpdatePackageFromPMC() always inject -Version.
If the original PS test does not use -Version, use Execute() with the raw command string
instead — using the helper changes the semantics.
PowerShell session state is accessible. nugetConsole.Execute() runs in a live PMC PowerShell
session. It can execute any PowerShell command, not just NuGet commands. This means PS session
state — global variables ($global:InstallVar), registered functions
(Test-Path function:\Get-World), environment checks — can all be queried and asserted via
Execute() + IsMessageFoundInPMC(). Do not skip tests just because they assert PS session state.
Project-level build operations:
| Scenario | Apex API |
|---|---|
| Clean (deletes obj/bin) | testContext.Project.Clean() |
| Rebuild | testContext.Project.Rebuild() |
| Cache file path | CommonUtility.GetCacheFilePath(testContext.Project) |
| Wait for file to appear | CommonUtility.WaitForFileExists(path) |
| Wait for file to disappear | CommonUtility.WaitForFileNotExists(path) |
For single-project solutions, project-level Clean/Rebuild is equivalent to solution-level.
| PowerShell assertion | Apex equivalent |
|---|---|
Assert-Package $p PackageName Version (packages.config) | CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-Package $p PackageName (no version, packages.config) | CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger) |
Assert-Package $p PackageName Version (PackageReference) | CommonUtility.AssertPackageInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-Throws { ... } $expectedMessage | nugetConsole.IsMessageFoundInPMC(expectedMessage) — PMC errors appear as text, not C# exceptions |
Assert-Null (Get-ProjectPackage ...) / not installed | CommonUtility.AssertPackageNotInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger) |
Assert-NoPackage $p PackageName Version (PackageReference) | CommonUtility.AssertPackageNotInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-PackageReference $p PackageName Version | CommonUtility.AssertPackageReferenceExists(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-NoPackageReference $p PackageName | CommonUtility.AssertPackageReferenceDoesNotExist(VisualStudio, testContext.Project, packageName, Logger) |
| PowerShell source | Apex equivalent |
|---|---|
$context.RepositoryRoot / $context.RepositoryPath | testContext.PackageSource — create packages with CommonUtility.CreatePackageInSourceAsync() |
No -Source (uses nuget.org) | Create a local package with CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, ...) — never depend on nuget.org |
Hardcoded invalid sources (http://example.com, ftp://...) | Use the same hardcoded strings directly |
For simple packages:
await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion);
For packages with dependencies:
await CommonUtility.CreateDependenciesPackageInSourceAsync(
testContext.PackageSource, packageName, packageVersion, dependencyName, dependencyVersion);
For .NET Framework-specific packages:
await CommonUtility.CreateNetFrameworkPackageInSourceAsync(
testContext.PackageSource, packageName, packageVersion);
PS tests that use Get-VSComponentModel + ISettings to modify NuGet config at runtime can be
migrated by pre-configuring SimpleTestPathContext before passing it to ApexTestContext.
Via Settings API (preferred):
using var simpleTestPathContext = new SimpleTestPathContext();
simpleTestPathContext.Settings.AddSource("PrivateRepo", privatePath);
using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger,
simpleTestPathContext: simpleTestPathContext);
Via raw config file (for settings not covered by the API like dependencyVersion or bindingRedirects):
using var simpleTestPathContext = new SimpleTestPathContext();
File.WriteAllText(simpleTestPathContext.NuGetConfig,
$@"<?xml version=""1.0"" encoding=""utf-8""?>
<configuration>
<config>
<add key=""dependencyVersion"" value=""HighestPatch"" />
</config>
<packageSources>
<clear />
<add key=""source"" value=""{simpleTestPathContext.PackageSource}"" />
</packageSources>
</configuration>");
using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger,
simpleTestPathContext: simpleTestPathContext);
[TestMethod]
[Timeout(DefaultTimeout)]
public void DescriptiveTestName_Fails()
{
using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger);
var packageName = "Rules";
var source = @"c:\temp\data";
var expectedMessage = $"Unable to find package '{packageName}' at source '{source}'. Source not found.";
var nugetConsole = GetConsole(testContext.Project);
nugetConsole.Execute($"Install-Package {packageName} -ProjectName {testContext.Project.Name} -Source {source}");
Assert.IsTrue(
nugetConsole.IsMessageFoundInPMC(expectedMessage),
$"Expected error message was not found in PMC output. Actual output: {nugetConsole.GetText()}");
}
[TestMethod]
[Timeout(DefaultTimeout)]
public async Task DescriptiveTestNameAsync()
{
using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger);
var packageName = "TestPackage";
var packageVersion = "1.0.0";
await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion);
var nugetConsole = GetConsole(testContext.Project);
nugetConsole.InstallPackageFromPMC(packageName, packageVersion);
CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, packageVersion, Logger);
}
When the same scenario applies to multiple project types, use [DataTestMethod] or [DynamicData]:
[DataTestMethod]
[DynamicData(nameof(GetPackageReferenceTemplates), DynamicDataSourceType.Method)]
[Timeout(DefaultTimeout)]
public async Task InstallPackageAsync(ProjectTemplate projectTemplate)
{
using var simpleTestPathContext = new SimpleTestPathContext();
EnsurePackageReferenceFormat(simpleTestPathContext, projectTemplate);
using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger,
simpleTestPathContext: simpleTestPathContext);
// ... test body
}
// Yields both legacy and SDK-style PackageReference templates
private static IEnumerable<object[]> GetPackageReferenceTemplates()
{
yield return new object[] { ProjectTemplate.ConsoleApplication };
yield return new object[] { ProjectTemplate.NetCoreConsoleApp };
}
// Only legacy (ConsoleApplication) needs this; SDK projects ignore it
private static void EnsurePackageReferenceFormat(SimpleTestPathContext context, ProjectTemplate template)
{
if (template == ProjectTemplate.ConsoleApplication)
context.Settings.SetPackageFormatToPackageReference();
}
Use [DynamicData] over [DataRow] when the template set is reused across multiple tests in the
same class. This ensures one test method covers both legacy and SDK-style PackageReference projects.
To create a multi-targeted project, modify the csproj after project creation:
using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.NetCoreConsoleApp, Logger);
// Modify csproj to multi-target via XDocument:
// change <TargetFramework> to <TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
Follow the repo-wide coding guidelines first — they apply to all code, not just migrations:
docs/coding-guidelines.md (e.g. no #region blocks, no
reflection, var usage, nullable enabled). Don't restate or duplicate those rules here; this section
only lists conventions specific to test migration that supplement the common guidelines:
using var (inline using declaration), not using (var ...) { }.GetNetCoreTemplates, etc.) in the file.{Action}FromPMC{Scenario}[_Fails|Async]. Suffix with _Fails for error tests,
Async for async tests.[Timeout(DefaultTimeout)].nugetConsole.GetText() in assertion failure messages for diagnostics.SharedVisualStudioHostTestClass which provides VisualStudio and Logger.GetConsole(testContext.Project) helper method in the test class.Skip PS tests that:
Assert-BindingRedirect — binding redirect tests are already [SkipTest] in PS and not
worth migrating.Get-ProjectItem to check tree structure,
parent/child relationships). However, if the PS test only uses Get-ProjectItem /
Get-ProjectItemPath to verify a file exists on disk, migrate it using filesystem assertions
instead: File.Exists(path), XML reads on the project file, or
CommonUtility.WaitForFileExists().get_errors to verify it compiles cleanly.-Source implicitly use nuget.org. Always
replace this with local package creation via CreatePackageInSourceAsync — tests must not depend
on external feeds.NuGetApexTestService limitations: It does NOT expose ISolutionManager or VS DTE project
item inspection. Only IVsPackageInstaller, IVsSolutionRestoreStatusProvider,
IVsPackageUninstaller, IVsPathContextProvider2, and IVsUIShell are available.NuGetApexTestService.InstallPackage() swallows
InvalidOperationException and logs it — it does NOT rethrow. For error-path IVs tests, assert
that the package was NOT installed (AssertPackageNotInPackagesConfig) rather than trying to
catch exceptions.EnvDTE.Project: IVs API methods like InstallPackage() take
project.UniqueName (from EnvDTE.Project), not a ProjectTestExtension. Get it via
VisualStudio.Dte.Solution.Projects.Item(1)._pathContext vs testContext: IVsServicesTestCase uses a class-level
SimpleTestPathContext _pathContext (initialized in constructor), not per-test ApexTestContext.
PMC tests in NuGetConsoleTestCase use per-test ApexTestContext.SimpleTestPathContext's NuGet.config: Using CreateConfigurationFile to
write a full config replaces defaults like globalPackagesFolder, fallbackPackageFolders, and
httpCacheFolder — causing packages to pollute the user's real global packages folder. Instead
use simpleTestPathContext.Settings.AddSource() and AddPackageSourceMapping() to layer config
on top of the defaults.WaitForAutoRestore() for
ConsoleApplication with SetPackageFormatToPackageReference(). Only SDK-style projects
(e.g., NetCoreConsoleApp) auto-restore on project open.UpdatePackageFromPMCNotInstalled_Fails and
UninstallPackageFromPMCNotInstalled_Fails already exist. If covered, just delete the PS test.NuGet.Tests.Apex.Daily does NOT count as
coverage for removing an E2E test that runs on every PR. Only regular Apex tests
(NuGet.Tests.Apex) provide equivalent gating.This section captures lessons learned from actual migration runs that don't fit neatly into the sections above. Update this section after every migration run with new discoveries or corrections.
Test-NetCoreConsoleAppClean — it was claimed to be covered by Apex Daily's VerifyCacheFileInsideObjFolder, but that test is in Daily (different cadence) and bundles 3 behaviors. We properly covered it by adding NetCoreConsoleApp to GetPackageReferenceTemplates() in PackageReferenceTestCase.CleanDeletesCacheFile.Get-Package -ListAvailable defaults First to 50 (GetPackageCommand.DefaultFirstValue). The old E2E test Test-GetPackageRetunsMoreThanServerPagingLimit asserted >100 against nuget.org and only worked under an older default. When migrating "more than server paging limit" to a unit test, pass an explicit large -First (e.g. 105) over a local source with 105 packages and assert > 100 — this faithfully validates page aggregation in PackageFeedEnumerator. Without -First, the local result caps at 50.FirstKeywordPackage1..6) and searching the keyword returns all of them, so -First/-Skip paging is testable with a unit test.vs-tests.yml part: list is NOT maintained per-migration — e.g. SyncPackageTest.ps1 remains listed after deletion in #7270. Follow precedent: when fully migrating a PS test file, delete the file and remove it from NuGet.sln solution items, but leave the vs-tests.yml part list untouched.-Source by name and -Updates source filtering are UNIT-testable, not Apex-only. GetMatchingSource resolves -Source against ISourceRepositoryProvider.PackageSourceProvider.LoadPackageSources(), and -Updates queries PrimarySourceRepositories (= just the matched -Source when one is given). The unit harness's TestSourceRepositoryUtility.CreateSourceRepositoryProvider(IEnumerable<PackageSource>) builds a real provider — pass new PackageSource(path, "FriendlyName") to test name resolution, and register a second empty source to verify -Updates -Source EmptySource returns 0. Don't reach for Apex just because a scenario mentions -Source.-Source 'All' is a host-level concept, NOT a cmdlet -Source value. The aggregate "All" is handled by PowerShellHost.SetPrivateDataOnHost (the PMC source dropdown), which maps it to an empty active source. Passing -Source 'All' literally to a cmdlet hits GetMatchingSource('All') → null → CheckSourceValidity → throws "Unknown source 'All'". The old GetPackageAcceptsAllAsSourceName PS function had no Test- prefix (never ran) and relied on this non-existent behavior — do not migrate it.Some E2E tests exercise cmdlet behavior (parameter handling, filtering, prerelease logic) rather than Visual Studio integration. These are better covered by fast unit tests that invoke the cmdlet directly in a PowerShell runspace — no VS instance needed.
| Scenario | Target |
|---|---|
| Tests that verify cmdlet parameter behavior (filtering, prerelease, -AllVersions, -Updates) | Unit test |
| Tests that verify VS integration (project creation, restore, UI, solution operations) | Apex test |
| Tests that exercise Install/Update/Uninstall side effects on actual project files | Apex test |
| Tests that only query package sources or list installed packages | Unit test |
The key distinction: if the test needs a real Visual Studio instance, solution, or project system, use Apex. If it only needs the cmdlet logic + a mock package source, use a unit test.
Unit tests live in:
test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/
The project (NuGetConsole.Host.PowerShell.Test.csproj) references:
NuGet.PackageManagement.PowerShellCmdlets (the source project)VisualStudio.Test.Utility (shared test helpers)Microsoft.VisualStudio.Sdk.TestFramework.Xunit (VS mocking infrastructure)Tests run sequentially via xunit.runner.json:
{
"maxParallelThreads": 1,
"parallelizeTestCollections": false
}
The reference implementation is in:
test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/GetPackageCommandTests.cs
Read this file for the full working patterns. Key architectural details below.
Creates a real PowerShell runspace with the cmdlet registered via SessionStateCmdletEntry. Key points:
activeSource (always pass pathContext.PackageSource — never nuget.org)typeof(GetPackageCommand)) into InitialSessionStateInvoke(cmdletName, parameters) runs the cmdlet and returns IList<PSObject>IDisposable — use with using varWhen adding tests for a different cmdlet (e.g., Install-Package), create a new fixture class that registers that cmdlet's type instead.
A minimal PSHost that provides PrivateData with two properties NuGet cmdlets require:
"activePackageSource" — the source URL/path the cmdlet defaults to"CancellationTokenKey" — a CancellationToken (use CancellationToken.None in tests)Without these, cmdlets throw during initialization.
The test class implements IAsyncServiceProvider and sets up mocks in the constructor:
GlobalServiceProvider (from xUnit collection MockedVS.Collection)IVsSolutionManager — must set IsSolutionOpen = true, IsSolutionAvailableAsync = true, and provide a default project via GetDefaultNuGetProjectAsync()IComponentModel — registers all services the cmdlet's Preprocess() method resolves: ISettings, IVsSolutionManager, ISourceControlManagerProvider, ICommonOperations, IPackageRestoreManager, IDeleteOnRestartManager, IRestoreProgressReporter, and ISourceRepositoryProviderServiceLocator.InitializePackageServiceProvider(this) to wire it all upIf a new cmdlet requires additional services, add them to the IComponentModel mock setup.
| Cmdlet | Test file |
|---|---|
Get-Package | Cmdlets/GetPackageCommandTests.cs |
Install-Package | Cmdlets/InstallPackageCommandTests.cs (create if needed) |
Update-Package | Cmdlets/UpdatePackageCommandTests.cs (create if needed) |
Uninstall-Package | Cmdlets/UninstallPackageCommandTests.cs (create if needed) |
Find-Package | Cmdlets/FindPackageCommandTests.cs (create if needed) |
When creating a new test file, follow the same structure as GetPackageCommandTests.cs — copy the class skeleton (constructor, IAsyncServiceProvider, inner fixture/host classes) and adapt the cmdlet type.
Use SimpleTestPathContext + SimpleTestPackageUtility (same utilities as other NuGet tests):
using var pathContext = new SimpleTestPathContext();
await SimpleTestPackageUtility.CreatePackagesAsync(
pathContext.PackageSource,
new SimpleTestPackageContext("PackageA", "1.0.0"),
new SimpleTestPackageContext("PackageA", "2.0.0-beta"));
Register a real ISourceRepositoryProvider pointing at the local test source via TestSourceRepositoryUtility.CreateSourceRepositoryProvider(new PackageSource(localPath)). See SetupSourceRepositoryProvider() in the reference file.
Create a mock NuGetProject with PackageReference entries and wire it into _solutionManager.GetNuGetProjectsAsync() / GetDefaultNuGetProjectAsync(). See CreateMockProject() and SetupProjectWithInstalledPackage() helpers in the reference file.
using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource);
var results = fixture.Invoke(
"Get-Package",
new Dictionary<string, object>
{
{ "ListAvailable", true },
{ "Source", pathContext.PackageSource },
{ "Filter", "TestPackage" },
});
Switch parameters (-ListAvailable, -AllVersions, -IncludePrerelease) are passed as true in the dictionary.
Results come back as PSObject wrappers. Cast BaseObject to the expected type:
| Scenario | BaseObject type |
|---|---|
-ListAvailable | PowerShellRemotePackage |
| Installed (no switch) | PowerShellInstalledPackage |
-Updates | PowerShellUpdatePackage |
Use FluentAssertions: results.Should().ContainSingle(), then cast and assert .Id, .Version, .Versions.
[Fact]) with FluentAssertions.[Collection(MockedVS.Collection)] and takes GlobalServiceProvider in constructor.{CmdletName}{Scenario}_{Expectation}Async (e.g., GetPackageListAvailable_WithFilter_ReturnsMatchingPackageAsync).using var for disposables.async Task (package creation is async).var for locals except value tuples.Cmdlets/ for the relevant command test file.dotnet build test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/NuGetConsole.Host.PowerShell.Test.csprojdotnet test test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/NuGetConsole.Host.PowerShell.Test.csprojGetPackageTestCase.cs has a lifecycle test that installs, lists, updates, and uninstalls all in one method.PS E2E tests that do any of the following are good candidates for unit tests:
Get-Package with -ListAvailable, -Filter, -AllVersions, -Updates, -Prerelease