Show the files Deduplicate will delete before asking to confirm - #123
Conversation
Deduplicate printed only a count and its keep-the-shortest-name policy before the "Proceed with deletion? (y/N)" prompt; the paths appeared afterwards, one per file already deleted. Scan and DryRun both print the full per-group KEEP/DELETE listing, so the one verb where seeing it matters was the one that omitted it, and the confirmation gate asked the user to approve an outcome they could not see. Extract that listing into DuplicateReport.PlanDeletions, which computes the keeper with the same Deduplicator.SelectFileToKeep the delete path uses, and call it from Deduplicate before the prompt and from DryRun in place of its own copy. FormatBytes was duplicated in four verbs and moves there too. DryRun's and Scan's output is unchanged byte for byte. The listing is not capped or paged: the point is that every path about to be deleted is on screen before the question is asked. Fixes #114 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01By4ipNgZKvDwoS2HUmjupA
CI:
|
| Check | Result |
|---|---|
| Test on ubuntu-latest | ✅ success |
| Test on macos-latest | ✅ success |
| Test on windows-latest | ⏳ in progress |
| Analyze (csharp) ×2, Analyze (actions), CodeQL | ✅ success |
| Discover Test Projects | ✅ success |
| github-advanced-security | ❌ quota (402), see above |
I'm still watching the PR and will report on the Windows leg when it finishes.
Generated by Claude Code
SonarCloud's quality gate failed the PR at 68.3% coverage on new code against a required 80%. The uncovered lines were real gaps, not noise: Scan, DryRun and Stats had no test at all, so the FormatBytes call sites this change rewrote in each of them were never executed, and the Deduplicate tests all declined at the prompt, so the delete path below it was never reached either. Add VerbOutputTests, which runs Scan, DryRun and Stats through the console and pins what each reports, including that the two read-only verbs leave every file on disk. Add the confirming case to DeduplicateConfirmationTests -- what the listing named is deleted, what it did not name is not -- plus the totals shown above the prompt, and a FormatBytes row per unit. Console redirection moves to a shared ConsoleCapture helper. Every line this PR adds or changes is now executed by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01By4ipNgZKvDwoS2HUmjupA
|



Fixes #114
What was wrong
Deduplicate.Runprinted only a count and a policy sentence —Found N group(s) of duplicate files./Keeping the copy with the shortest filename in each group.— before askingProceed with deletion? (y/N):. The paths only appeared afterwards, viaDeleted: {file}, one per file that was already gone.ScanandDryRunboth print the full per-groupKEEP/DELETElisting. The one verb where seeing it matters was the one that omitted it, so the confirmation gate asked the user to approve an outcome they could not see. "Keep the shortest filename" makes that concrete: two identical filesreport-final-DO-NOT-DELETE.pdfandr.pdfkeepr.pdf, and nothing on screen said so until it was irreversible.What changed
DuplicateReport.PlanDeletionsbuilds the per-groupKEEP/DELETElisting plus its totals, computing each group's keeper with the sameDeduplicator.SelectFileToKeepthe delete path uses — so the preview and the deletion cannot drift apart.Deduplicate.Runprints that listing, plusFiles to delete:/Space to reclaim:, before they/Nprompt.DryRun.Runnow renders from the same helper instead of its own copy of the loop.FormatByteswas duplicated in four verbs; it moves toDuplicateReportalongside the listing.DryRunandScanoutput is unchanged byte for byte — verified by running both verbs before and after, and now pinned by tests.The listing is deliberately not capped or paged, which the triage comment raised as an option: the acceptance criterion is that every path about to be deleted is on screen before the question is asked, and a cap would silently reintroduce the defect for exactly the large runs where it is most costly.
Deduplicate, after
Tests
The suite goes from 27 tests to 45.
ConsoleCaptureruns a verb with the console redirected, which is the only surface these verbs have — they return nothing, and what they do is what they print.DeduplicateConfirmationTestsdrives the verb throughBaseVerb.Run()and asserts every doomed path appears before the prompt text. Answeringnis what makes that unambiguous: nothing is deleted, so a path appearing ahead of the prompt can only have come from the new listing.EveryPathToBeDeletedIsNamedBeforeTheConfirmationPrompt— the blunt-policy case (r.pdfvsreport-final-DO-NOT-DELETE.pdf), also checking the kept copy is namedEveryDuplicateGroupAppearsInTheListing— three groups, all coveredTheTotalsMatchTheListingShownAboveThemDecliningTheConfirmationLeavesEveryFileOnDisk— the listing did not turn the preview into the deletionConfirmingDeletesExactlyTheCopiesTheListingNamed— what the listing named is gone, what it did not name survivesVerbOutputTestscoversScan,DryRunandStats, which had no tests at all before this PR although it rewrote a call site in each — including that the two read-only verbs leave every file on disk, and thatDryRunstill reports exactly what it used to now that it renders from the shared helper.DuplicateReportTestspins the helper's counts, bytes and listing shape, plus aFormatBytesrow per unit.Verification
EveryPathToBeDeletedIsNamedBeforeTheConfirmationPromptandEveryDuplicateGroupAppearsInTheListingfail, and they pass again with it restored.dotnet test -- --coverage) cross-referenced against the diff: 54 of 54 added or changed lines executed. SonarCloud agrees at 100.0% coverage on new code.ACopyThatCannotBeDeletedIsReportedAndTheRunCarriesOnself-skips when the tests run as root, which was already true before this change.One red check that is not this PR's
github-advanced-securityfails withstatusCode: 402, errorType: 'quota'— "You have exceeded your monthly quota" — at analysis-session creation, before it reads the diff. Details and the blocked re-run are in this comment.🤖 Generated with Claude Code
https://claude.ai/code/session_01By4ipNgZKvDwoS2HUmjupA