Repository navigation
fix(scripts): repair three defects in the locked-restore diagnosis - #341
Conversation
Read NU1004 from both of the restore's streams. Only stdout was teed, so an NU1004 on stderr left the gate unset and the job kept the bare error the wrapper exists to explain. Measured against a drifted fixture and a stderr-only stand-in: no advice before, the stale lock file and the repair command after. 2>&1 alone does not do it -- Windows PowerShell turns a redirected stderr line into an error record and 'Stop' raises it before the advice can run, so the preference is lowered for that one call and restored after it. Both scripts have to keep running on 5.1, since nothing available before CI runs pwsh. Compare an interval range in the spelling NuGet records. "[1.0.0,2.0.0)" in the props was compared as written against the "[1.0.0, 2.0.0)" in the lock file, so an in-sync pair read as drift and the user was told to run --force-evaluate, which repairs nothing. Both sides now normalize. Measured: drift count 1 before, 0 after; a different bound is still reported. Stop the test script leaking an exit code. The silent-restore case left $LASTEXITCODE at the stand-in's 7, and `shell: pwsh` ends a step with `exit $LASTEXITCODE`. Measured with that epilogue: every assertion passed and the step exited 7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhR19gpMx4ZBuq6pkP4fgY
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
… code Review findings on the previous commit. The new stderr test only read the exit code, so deleting the stream merge -- the change it exists to guard -- left it green while the diagnosis silently disappeared. It now runs a copy of the wrapper rooted at a drifted fixture and asserts the advice text. Measured: with 2>&1 removed the case fails and the script exits 1; with it, exit 0. Reading that output needed ReadAllText. Get-Content -Raw emits no object for an empty file, and -notmatch against an empty collection filters instead of comparing, so it returns an empty array -- falsy -- and the first form of the assertion could not fail at all. ConvertTo-NormalizedRange's mandatory parameter rejected the empty string, so a props Version of only whitespace aborted the diagnosis of the restore that was already failing. AllowEmptyString keeps the textual comparison the function always did. The IsNullOrWhiteSpace skip was wider than the null case it was added for: an emptied requested range disagrees with any central version and is drift, which the previous code reported. It is skipped only when null now, and both cases are covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhR19gpMx4ZBuq6pkP4fgY
There was a problem hiding this comment.
Approved at e5490a9ff17ed4dcdfd5db4a3326c5c86163cef9. No outstanding findings.
code-review (high) and security-review each ran once, against 40560c8c — the HEAD before the fixes below. The security review found no HIGH or MEDIUM newly-introduced vulnerability: the merged stderr text is consumed only by a -like '*NU1004*' substring test and never reaches Invoke-Expression; the lowered $ErrorActionPreference is saved, scoped to one call and restored in a finally, and a non-launchable $Dotnet still terminates; the [xml] casts and ConvertFrom-Json are pre-existing context lines.
code-review raised three findings, all fixed in e5490a9f and re-verified against the updated code:
- Medium — the new stderr case asserted only the exit code. Deleting the stream merge, the change the case exists to guard, left it green while the diagnosis disappeared. It now runs a copy of the wrapper rooted at a drifted fixture and asserts the advice text. Verified by mutation: with
2>&1removed the case fails and the script exits 1; with it in place, exit 0. - Low —
ConvertTo-NormalizedRange's mandatory parameter rejected the empty string, so aPackageVersionwhoseVersionis only whitespace aborted the diagnosis of the restore that was already failing.[AllowEmptyString()]restores the textual comparison the function always did, and a case covers it. - Low — the
IsNullOrWhiteSpaceskip was wider than the null case it was added for. An emptiedrequestedrange disagrees with any central version and is drift, which the previous code reported. It is skipped only when$nullnow, with a case for each.
Fixing the first surfaced a defect in the fix itself: Get-Content -Raw emits no object for an empty file, and -notmatch against an empty collection filters rather than compares, returning an empty array — falsy. The first form of that assertion could not fail at all. It reads through [IO.File]::ReadAllText now, and the mutation run above is what established the assertion has teeth.
Checks: Tests (windows-latest), Tests (ubuntu-latest), Native AOT publish and Lifecycle against real WSLC all succeeded at this HEAD; 0 blocking. The two Tests jobs run scripts/Test-LockedRestore.ps1 under pwsh, so both PowerShell hosts are covered — which is what the lowered preference exists for, since the 5.1 behaviour it works around is what the suggested 2>&1-only fix would have tripped.
No verification gaps. Approve is not merge.
Closes #340.
Review leftovers from #339. Three defects, each measured before and after.
1. The NU1004 gate read only stdout
Invoke-LockedRestore.ps1teed only stdout, so an NU1004 written to stderr left$outputwithout it and the job kept the bare error the wrapper exists to explain. Both streams are read now.Measured against a drifted fixture and a stand-in that reports NU1004 on stderr only:
src/Lib/packages.lock.json is stale: Pkg (net10.0) locked [1.0.0, ), central 2.0.0+Fix with: dotnet restore wip.slnx --force-evaluate2>&1on its own is not the fix. Windows PowerShell turns every redirected stderr line into an error record, and$ErrorActionPreference = 'Stop'raises it before the advice can run — measured: the restore aborts with the stderr text as aRemoteException. #334 settled that both scripts stay Windows PowerShell 5.1 compatible, because nothing available before CI can runpwsh, so the preference is lowered for that one call and restored in afinally.SDK 10.0.403 writes NU1004 to stdout, so the diagnosis worked before this change. It was the gate that was fragile, not the message.
2. An interval range was compared as written
ConvertTo-RequestedRangereturned a bracketed interval with whitespace collapsed rather than in NuGet's spelling. ForVersion="[1.0.0,2.0.0)"NuGet records"requested": "[1.0.0, 2.0.0)", so an in-sync lock file read as drift — and since the advice still requires a real NU1004, the result was not a spurious failure but a wrong instruction: run--force-evaluate, which repairs nothing. Both sides now normalize to one spelling.Measured on a fixture whose props and lock hold the same interval: drift count 1 before, 0 after. A genuinely different bound is still reported.
Not reachable on
mainbefore this change — both central versions are plain versions — and the tests covered onlyVand[V].3. The test script leaked an exit code
The silent-restore case left
$LASTEXITCODEat the stand-in's 7.shell: pwshends a step withexit $LASTEXITCODE, so a run whose every assertion passed still reported failure; it only survived because a later real restore happened to reset the code.Measured with the epilogue GitHub appends: every assertion passed, step exit 7 before, 0 after.
Also
Test-LockedRestore.ps1covers all three: the interval pair in both directions, and a restore that fails on its error stream returning its own exit code.requestedrange that is present but empty is skipped rather than compared, which is what the function's description already claimed.Verification
./scripts/Test-LockedRestore.ps1passes on the host (Windows PowerShell 5.1, SDK 10.0.403):PASS: exact SDK, immutable locks, NU1004 on drift, the stale-lock diagnosis, and whole-solution lock refresh, exit 0. An unrelated restore failure still gets no--force-evaluateadvice.CI then ran the same script under
pwshon bothwindows-latestandubuntu-latestat this HEAD:Tests (windows-latest),Tests (ubuntu-latest),Native AOT publishandLifecycle against real WSLCall succeeded. Both hosts are covered, which is what the lowered preference exists for.🤖 Generated with Claude Code
https://claude.ai/code/session_01LhR19gpMx4ZBuq6pkP4fgY
Summary by CodeRabbit
Bug Fixes
Documentation