fix(cli): honor --idempotency-window in the duplicate-purchase check - #2111
Conversation
The flag was accepted unparsed and never read: both duplicate-check call sites passed 0, so the lookback was a hardcoded 24h. A re-run 30h after a purchase with --idempotency-window 72h rebought the same RIs. validateFlags now parses the window into whole hours and rejects unparseable, non-positive, or fractional-hour values; checkDuplicates and adjustRecsForDuplicates use the parsed value. Closes #1262
|
Warning Review limit reached
This review includes 7 billable files and costs up to $1.75.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 12 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Your 62 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Review configuration: ⚙️ Run configurationConfiguration used: Repository: LeanerCloud/cloud-commitments-cli/.coderabbit.yaml Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (7)
Comment |
|
Independent adversarial review (Opus 5.5) at 28fb7c9: MERGE. --idempotency-window is now parsed (validateIdempotencyWindow in PreRunE) into whole hours and used by both NewDuplicateChecker call sites (CSV and --services). An unset flag means 24h, and hours<=0 falls back to the library's 24h default, never 0 or unlimited. Sub-hour, zero, negative and garbage values are rejected with an explicit error, not rounded; huge values are bounded by ParseDuration and only reduce purchases. Pre-fix: TestCheckDuplicates_HonorsIdempotencyWindow fails on both paths on main (kept 5) and passes here. Local: build, vet and go test -race -short ./cmd/... clean; CI green (CodeRabbit rate-limited). |
Summary
--idempotency-windowwas accepted but never parsed or read. The CLI duplicate-purchase check always used a hardcoded 24h lookback, so a user who passed a longer window to cover an earlier run got no protection past 24h.Root cause
Both duplicate-check call sites (
checkDuplicatesandadjustRecsForDuplicatesincmd/multi_service_helpers.go) calledNewDuplicateChecker(0), which substitutesDefaultDuplicateCheckLookbackHours = 24.Config.IdempotencyWindowhad no reader, andvalidateFlagsnever parsed it, so--idempotency-window=bananaran silently too.Fix
validateIdempotencyWindow(called fromvalidateFlags, the root command's PreRunE) parses the flag withtime.ParseDuration. It rejects unparseable, zero, negative, and fractional-hour values (90m,1h30m) instead of rounding, because a shorter window than requested lets a duplicate through. The parsed hours go intoConfig.IdempotencyWindowHours.toolCfg.IdempotencyWindowHours. The default stays24h, so default behavior doesn't change.README.md,docs/cli/README.md,docs/cli/purchase-safety.md) no longer say the flag has no effect.Only the root command can purchase (
configure-azureandconfigure-gcpnever reach the dedup path), so no production path skips the validation.Regression test
TestCheckDuplicates_HonorsIdempotencyWindowcovers the scenario from the issue: a 5-instance commitment bought 30h ago,--idempotency-window 72h, and a matching 5-instance recommendation. Both the--servicespath (checkDuplicates) and the CSV path (adjustRecsForDuplicates) must subtract it down to 0.TestValidateIdempotencyWindow: valid 1h/24h/72h; rejects "", banana, 0h, -24h, 90m, 1h30m.I proved it fails pre-fix by reverting only the two call sites to
NewDuplicateChecker(0):It passes with the fix. The validator test can't compile on the parent commit because the function is new. On the parent,
validateFlagshas no idempotency-window check at all.Verification
All with GOTOOLCHAIN=go1.26.6 GOWORK=off, rebased on main after #2105 merged:
go build ./cmd,go vet ./cmd/: cleango test -race -short ./cmd/: okgo mod tidy -diff: emptygolangci-lintv2.10.1 (the ci.yml pin)run --timeout=10m: 0 issuesOverlap with #2105
#2105 (CSV purchase confirmation) has merged. This branch is rebased on it without conflicts. The only change here in the CSV path is the one-line window argument inside
adjustRecsForDuplicates.Closes #1262