Skip to content

Test coverage: NuGet/update/install files reverted by #654 (issue #630) - #671

Merged
Alexandre Zollinger Chohfi (azchohfi) merged 3 commits into
mainfrom
azchohfi-azchohfi-tests-nuget-revert
Jul 17, 2026
Merged

Alexandre Zollinger Chohfi (azchohfi) merged 3 commits into
mainfrom
azchohfi-azchohfi-tests-nuget-revert

Conversation

@azchohfi

Copy link
Copy Markdown
Collaborator

What

Raises per-file test coverage to ≥95% for three files that PR #654 restored to their pre-migration state (issue #630):

File Before After
Commands/UpdateCommand.cs 3.4% 100%
Services/PackageInstallationService.cs 65.6% 100%
Services/NugetService.cs 85.2% 99.4%

NugetService.cs's only remaining uncovered lines are a genuinely-unreachable defensive guard in InstallPackageRecursiveAsync (public entry seeds an empty dict and ResolveDependenciesAsync skips present deps before recursing), documented in-source with a <remarks> referencing #630. No [ExcludeFromCodeCoverage], no coverage.runsettings edits.

Product changes (minimal, behavior-preserving)

  • Two internal static Func test seams on NugetService: HttpGetAsync (defaults to the shared HttpClient.GetAsync — the single real network boundary; tests inject canned in-memory responses) and GetUserProfileDirectory (defaults to Environment.GetFolderPath(UserProfile); lets the default %USERPROFILE%\.nuget\packages create-branch be exercised hermetically against a temp dir instead of mutating the real user profile). Both default delegates are byte-for-byte the original behavior.
  • One correctness fix (called out separately): ResolveDependenciesAsync now normalizes a dependency's version via ParseMinimumVersion before recursing into GetPackageDependenciesAsync. Previously the raw value from FetchDirectDependenciesAsync (which strips only brackets, "[2.0.0, )" → "2.0.0, ") built a malformed flat-container URL, silently dropping transitive dependencies nested under a range-versioned package. No-op for exact versions; the sibling on-disk path and the direct consumer already applied this normalization. The offline test fakes now match the slash-delimited /<id>/<version>/ URL segment so this path genuinely 404s without the fix (the transitive test fails if the fix is reverted).

Tests

  • NugetServiceOfflineTests — restore/download/version-resolution/transitive+diamond-dedup, listed-version filtering, nuspec parsing, HTTP-error paths, the hermetic default-fallback create-branch, and the range-versioned transitive walk. [DoNotParallelize]; all process-wide seams + NuGet env vars saved/restored per test.
  • PackageInstallationServiceTests, UpdateCommandTests — version resolution, already-present top-up merges, updates-found/all-up-to-date/preview, per-package + build-tools failure paths.

Validation

  • Debug solution + Release (both WinApp.Cli and WinApp.Cli.Tests, warnings-as-errors): 0 W / 0 E each.
  • Scoped coverage run: 97/97 passed, deterministic.
  • Reviewed with the repo pr-review skill (7 specialists + a GPT multi-model cross-check) before opening; the two accepted findings (real-profile mutation risk + a fake that masked the range-version bug) are fixed in this branch.

⚠️ Caveat

This wave re-covers the pre-migration NuGet client that #654 restored (#654 reverted the #629 migration + its #647 tests together). If maintainers re-attempt the migration, these three test files and the two seams will likely need rework. The ParseMinimumVersion correctness fix has standalone value while this code ships.

PR #654 reverted the #629 NuGet-client migration and its coverage tests
(#647) together, restoring UpdateCommand, PackageInstallationService, and
NugetService to their pre-migration versions and deleting their dedicated
test files. This re-covers that current (pre-migration) code on main.

Product change (minimal, behavior-preserving test seam only):
- NugetService: add internal static HttpGetAsync delegate defaulting to the
  shared HttpClient.GetAsync (production behavior byte-for-byte unchanged);
  route the four flat-container/registration GETs through it so download,
  version-resolution, and dependency-parsing paths run offline in tests.
- Document two genuine, non-unit-coverable ceilings in source via <remarks>
  referencing issue #630: the %USERPROFILE%\.nuget\packages Create() branch
  (GetFolderPath ignores env redirection) and the defensive ContainsKey
  short-circuit in InstallPackageRecursiveAsync (unreachable via public API).

Tests (meaningful use-case first, then residual-branch units):
- NugetServiceOfflineTests.cs (29): restore/download/version-resolution/
  source-handling, transitive + diamond dedup, listed-version filtering,
  nuspec dependency parsing, error paths. [DoNotParallelize]; the HttpGetAsync
  seam and cache/env vars are saved and restored per test.
- PackageInstallationServiceTests.cs (16): version resolution (config/pinned/
  ignoreConfig), already-present transitive top-up, fresh-install merge,
  EnsurePackageAsync success/failure, workspace init.
- UpdateCommandTests.cs (11): version compare, updates found/none, preview
  mode, per-package + build-tools failures, runtime handling, JSON/output.

Coverage: UpdateCommand 147/147 (100%), PackageInstallationService 131/131
(100%), NugetService 302/307 (98.37%; the 5 uncovered lines are the two
documented ceilings above).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two review-driven fixes, both raising real coverage without gaming:

1. Correctness (transitive deps under a version range): ResolveDependenciesAsync
   now normalizes a dependency's version via ParseMinimumVersion before recursing
   into GetPackageDependenciesAsync. FetchDirectDependenciesAsync only strips
   brackets ("[2.0.0, )" -> "2.0.0, "), so the raw value built a malformed
   flat-container URL and silently dropped dependencies nested under a
   range-versioned package. The offline fakes now match the slash-delimited
   "/<id>/<version>/" segment so a malformed URL genuinely 404s, making the
   transitive test fail without the fix.

2. Test hygiene / no machine mutation: extract a GetUserProfileDirectory seam
   (defaults to Environment.GetFolderPath(UserProfile)) so the default
   %USERPROFILE%\.nuget\packages create-branch is exercised hermetically against
   a temp dir instead of being a documented ceiling that would otherwise mutate
   the real user profile. Added a test pinning the seam's production default.

NugetService.cs 98.4% -> 99.4% (only the genuinely-unreachable defensive guard
remains, documented). PackageInstallationService.cs / UpdateCommand.cs stay 100%.
Debug + Release (both projects) 0W/0E; 97/97 scoped tests pass.

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

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.

Pull request overview

Improves unit-test coverage for the restored (pre-migration) NuGet/update/install code paths, primarily by adding offline/deterministic tests and introducing limited test seams in NugetService, plus a small correctness fix for transitive dependency walking when dependency versions are specified as ranges.

Changes:

  • Add offline NugetService tests using an injectable HTTP GET seam and hermetic user-profile resolution.
  • Add behavior tests for winapp update and PackageInstallationService with controllable fakes.
  • Fix NugetService.GetPackageDependenciesAsync transitive recursion to normalize range versions via ParseMinimumVersion.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/winapp-CLI/WinApp.Cli/Services/NugetService.cs Adds test seams for HTTP/profile path and fixes transitive dependency recursion for range versions.
src/winapp-CLI/WinApp.Cli.Tests/UpdateCommandTests.cs Adds behavior coverage for winapp update handler paths via controllable fakes.
src/winapp-CLI/WinApp.Cli.Tests/PackageInstallationServiceTests.cs Adds branch-level coverage for install/version-resolution/top-up behavior using an in-memory INugetService fake.
src/winapp-CLI/WinApp.Cli.Tests/NugetServiceOfflineTests.cs Adds hermetic, offline coverage for NuGet download/extract/deps/version-listing/cache-path behaviors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// transitive nuspec URL is well-formed. FetchDirectDependenciesAsync only strips brackets
// (e.g. "[2.0.0, )" -> "2.0.0, "), which would otherwise build a malformed flat-container
// URL and silently drop transitive dependencies nested under a range-versioned package.
var transitiveDeps = await GetPackageDependenciesAsync(depId, ParseMinimumVersion(depVersion), cancellationToken);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in 23f80bd. An open-lower-bound/unparseable range (e.g. (,2.0.0]) normalizes to empty, so the loop now skips the transitive fetch when the minimum version is empty (mirroring the existing on-disk ResolveDependenciesAsync guard) instead of building a malformed .../<id>//<id>.nuspec URL. The direct dependency is still returned; added a covering test with a unique package id to avoid the static DependencyCache colliding with other tests.

@github-actions

github-actions Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 32.40 MB 32.40 MB ✅ 0.0 KB (0.00%)
CLI (x64) 32.70 MB 32.70 MB 📉 -0.5 KB (-0.00%)
MSIX (ARM64) N/A 13.57 MB N/A
MSIX (x64) N/A 14.43 MB N/A
NPM Package N/A 28.32 MB N/A
NuGet Package N/A 28.33 MB N/A

Test Results

✅ 3492 passed, 4 skipped out of 3496 tests in 601.9s (+58 tests, +73.9s vs. baseline)

Test Coverage

✅ 94.8% line coverage, 89% branch coverage · ✅ +1.0% vs. baseline

CLI Startup Time

33ms median (x64, winapp --version) · ✅ -9ms vs. baseline


Updated 2026-07-17 03:14:55 UTC · commit 23f80bd · workflow run

…review)

Addresses the Copilot PR-review finding on the ParseMinimumVersion normalization:
an open-lower-bound or otherwise unparseable range (e.g. "(,2.0.0]") normalizes to
an empty string, which would recurse into GetPackageDependenciesAsync with an empty
version and build a malformed ".../<id>//<id>.nuspec" flat-container URL (pointless
work + a bogus empty-version cache key). Skip the transitive fetch when the minimum
version is empty, mirroring the existing on-disk ResolveDependenciesAsync guard; the
direct dependency is already in the result set. Added a covering test (unique package
id to avoid the static DependencyCache colliding with other tests).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azchohfi
Alexandre Zollinger Chohfi (azchohfi) merged commit 5beb701 into main Jul 17, 2026
22 checks passed
@azchohfi
Alexandre Zollinger Chohfi (azchohfi) deleted the azchohfi-azchohfi-tests-nuget-revert branch July 17, 2026 03:26
Nikola Metulev (nmetulev) added a commit that referenced this pull request Sep 4, 2026
## Description

This reverts #654, which reverted #629 (and its stacked coverage PR
#647).

#629 migrated `NugetService` off hand-rolled NuGet v3 HTTP calls and
onto the official NuGet client libraries, so `init` / `restore` /
`update` honor the user's `nuget.config` hierarchy: custom and private
package sources, credentials, `packageSourceMapping`, and
`globalPackagesFolder`. Nothing was wrong with that migration. It was
reverted purely because it pinned `NuGet.*` to the prerelease
`7.9.0-rc.36120` from the public dnceng `dotnet-tools` feed, and the ADO
security gate rejects consuming a prerelease from a secondary public
feed. That was always documented as temporary: the
`NuGet.UseSystemTextJsonDeserialization` switch needed to keep the
Native AOT publish clean had not shipped in a nuget.org stable yet.

**`NuGet 7.9.0` is now stable on nuget.org**, which is exactly the
condition #654 named for re-landing. So the pins point at `7.9.0`
stable, and `nuget.config` plus `.pipelines/release-nuget.config` go
back to main's minimal form: no `dotnet-tools` source, no
`packageSourceMapping`, no upstream note.

### Beyond a plain re-revert

About 100 commits landed on main between the revert and now, so this is
not a clean re-application. The notable resolutions:

- **`NugetService.cs` takes the migrated implementation wholesale.** The
only two commits that touched it after the revert (#671, #715) added
seams and env-var endpoint overrides that exist solely to serve the
hand-rolled implementation, so nothing real is dropped.
- **`PackageInstallationServiceTests.cs` / `UpdateCommandTests.cs`**
were add/add conflicts. Main's versions came from #671 and cover the
"already present, resolve deps via feed" branch and the
string-inequality update gate, both of which the migration deletes. The
migrated versions win; one still-relevant test from main
(`UpdateCommand_ExposesNameShortDescriptionAndSetupSdksOption`) was
ported forward.
- **Deleted `NugetServiceOfflineTests.cs`** (748 lines). Every method
targeted the removed `HttpGetAsync` / `GetUserProfileDirectory` seams.
The restored NuGet suite is a superset for everything still reachable.
- **Interface drift ported into the restored tests**:
`IsPackageInstalled` on `FakeSignToolNugetService`, the widened
`IBuildToolsService.RunBuildToolAsync` signature, and the
runtime-install step moving from `IWorkspaceSetupService` to
`IWindowsAppRuntimeService`.

### Feed configuration, and why the pipeline env vars go away

#715 added `WINAPP_NUGET_FLAT_CONTAINER` / `_REGISTRATION` /
`_AUTH_PREFIX` so the network-isolated ADO legs could reach
`pde-oss_Internal`. Those variables only ever configured the hand-rolled
endpoints, which no longer exist. `ci.yml` and `release.yml` already
copy `release-nuget.config` to both `%APPDATA%\NuGet\NuGet.Config` and
the repo root and then run `NuGetAuthenticate@1`, and the NuGet client
picks all of that up on its own, including the credential provider. So
the env blocks are removed from the `Build CLI` and `Bundle MSIX
Packages` steps.

The one place that still needed an explicit feed is the live-feed
integration tests. Those helpers used to hand-roll registration and
flat-container HTTP against a hardcoded `api.nuget.org`; they now go
through `PackageMetadataResource` / `FindPackageByIdResource` against
the same source the test config declares, and that source is overridable
with a new `WINAPP_TEST_NUGET_SOURCE`. `AGENTS.md` is updated with the
`nuget.config`-based equivalent for corp machines, replacing the old
env-var recipe.

### Follow-on fixes from review and from testing the branch

These came out of the review passes and from running the branch against
a real app, and are in the later commits rather than the re-land itself:

- **A fully cached dependency graph now restores without querying
sources** (#762). Reading the dependency *list* from a cached package's
local `.nuspec` was not enough, because each declared range was still
resolved to a concrete version against the feeds only. So an
already-extracted graph failed offline, or under a
`packageSourceMapping` that no longer maps a transitive package
anywhere. Resolution now falls back to the lowest fully installed cache
entry (completion marker required) before failing. Deliberately confined
to the failure paths, so a reachable source always decides the version
and online restores select exactly what they did before.
- **`winapp restore` works after `winapp init` on a .NET project.** For
.NET projects `init` records versions as PackageReferences rather than a
`winapp.yaml`, and `restore` then exited 1 telling the user to run
`dotnet restore` by hand. It runs it instead.
- **Feed failures explain themselves.** On a project whose
`nuget.config` pins a blocked or unreachable feed, `init` reported a
bare `Failed to get version for Microsoft.WindowsAppSDK`; the NuGet
layer's message naming the source and the reason was logged at debug
level only. It is now surfaced and included in the returned error.
- **An unsatisfiable diamond involving a float and an exact prerelease
pin is no longer accepted.** `RangesHaveCommonVersion` decides a
single-point intersection exactly, since prerelease eligibility is not
expressible as a numeric interval (`1.*` and `[1.5.0-preview]` intersect
at a non-empty point yet share no version). Kept narrow on purpose:
widening the existing witness heuristic instead would wrongly fail cases
like `1.*` with `(1.2.0, 1.8.0)`.
- Code-quality findings: deterministic disposal of the test listeners'
port probes, cleanup catches narrowed to the exceptions those blocks
exist to swallow, explicit `.Where(...)` filters, and a guard on the one
`Path.Combine` whose segment comes from an externally supplied package
id.

## Usage Example

The behavior this restores: `winapp restore` (and `init` / `update`)
picks up a `nuget.config` like this and restores the SDK packages from a
private mirror.

```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="contoso" value="https://pkgs.dev.azure.com/contoso/_packaging/winsdk-mirror/nuget/v3/index.json" />
  </packageSources>
</configuration>
```

```powershell
winapp restore
```

## Related Issue

Closes #653.
Closes #762.
Re-lands #629 and #647, reverting #654.

## Type of Change

- ✨ New feature
- 🐛 Bug fix
- ♻️ Refactoring
- 🔧 Config/build

## Checklist

- [x] New tests added for new functionality (if applicable)
- [x] Tested locally on Windows
- [x] [docs/usage.md](../docs/usage.md) updated (if CLI commands
changed)
- [x] Shipped skills updated in `plugins/winapp/skills/` (if CLI
commands/workflows changed)

## Additional Notes

**Validation**

- `dotnet build winapp.sln -c Debug -p:TreatWarningsAsErrors=true`: 0
warnings, 0 errors.
- Full suite green on two consecutive runs: 4643 total, 4638 passed, 5
skipped, 0 failed.
- Release Native AOT publish clean for both `win-x64` and `win-arm64`.
Confirmed `NuGet.UseSystemTextJsonDeserialization` still reaches ILC as
a trim feature on 7.9.0 stable (`--feature:` and `--runtimeknob:` both
present in the generated `.ilc.rsp`), and no Newtonsoft.Json assets land
in the published output.
- End to end against the published AOT binary: `winapp init` in a
directory whose `nuget.config` clears nuget.org, declares a single
custom mirror, and sets `globalPackagesFolder` restored all 18 SDK
packages from that mirror into the project-local folder. This exercises
the actual feature, not just the build.
- The #762 tests were verified non-vacuous: the offline and
mapping-excluded cases fail without the fix and pass with it.

**For reviewers**

The highest-risk part is the conflict resolution, not the migration
itself, since the migration already went through review in #629. The
resolutions above are the places worth a careful look. I also ran an
independent review pass over the conflict resolution; it confirmed
`NugetService.cs` and friends are byte-identical to their pre-revert
state apart from the documented `UpdateCommand` port-forward, and it
caught the dropped `UpdateCommandTests` metadata test that is now
restored.

`NugetServiceCachedGraphTests` is marked `[DoNotParallelize]`: it warms
a real global-packages folder, then reconfigures and re-reads it, and
that two-phase sequence proved intermittently sensitive to the 32-way
parallel test run.

Two deliberate omissions: running `scripts/build-cli.ps1` also
regenerates `docs/npm-usage.md` (line endings only) and bumps a stale
schema-version string in `src/winapp-npm/src/winapp-commands.ts` from
`0.5.1` to `0.6.1`. Both are pre-existing drift on main and unrelated to
this change, so they are left out to keep the diff focused.

## Screenshots / Demo

N/A

## AI Description

<!-- ai-description-start -->
_This section is auto-generated by AI when the PR is opened or updated.
To opt out, delete this entire section including the marker comments._
<!-- ai-description-end -->

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Nikola Metulev <nmetulev@users.noreply.github.com>
Copilot-Session: e5368fbc-50a6-45dc-b933-31d87643b19a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants