Skip to content

fix(cli): honor --idempotency-window in the duplicate-purchase check - #2111

Merged
cristim merged 1 commit into
mainfrom
fix/1262-idempotency-window
Sep 28, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/1262-idempotency-window

Conversation

@cristim

@cristim cristim commented Sep 28, 2026

Copy link
Copy Markdown
Member

Summary

--idempotency-window was 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 (checkDuplicates and adjustRecsForDuplicates in cmd/multi_service_helpers.go) called NewDuplicateChecker(0), which substitutes DefaultDuplicateCheckLookbackHours = 24. Config.IdempotencyWindow had no reader, and validateFlags never parsed it, so --idempotency-window=banana ran silently too.

Fix

  • validateIdempotencyWindow (called from validateFlags, the root command's PreRunE) parses the flag with time.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 into Config.IdempotencyWindowHours.
  • Both call sites pass toolCfg.IdempotencyWindowHours. The default stays 24h, so default behavior doesn't change.
  • Docs (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-azure and configure-gcp never reach the dedup path), so no production path skips the validation.

Regression test

  • TestCheckDuplicates_HonorsIdempotencyWindow covers the scenario from the issue: a 5-instance commitment bought 30h ago, --idempotency-window 72h, and a matching 5-instance recommendation. Both the --services path (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):

--- FAIL: TestCheckDuplicates_HonorsIdempotencyWindow
    checkDuplicates kept 5 instance(s); the 30h-old purchase is inside the 72h window and must be subtracted
    adjustRecsForDuplicates (CSV path) kept 5 instance(s), err=<nil>; want 0

It passes with the fix. The validator test can't compile on the parent commit because the function is new. On the parent, validateFlags has 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/: clean
  • go test -race -short ./cmd/: ok
  • go mod tidy -diff: empty
  • golangci-lint v2.10.1 (the ci.yml pin) run --timeout=10m: 0 issues
  • pre-commit hooks: all passed

Overlap 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

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
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/few Limited audience effort/m Days type/bug Defect labels Sep 28, 2026
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 7 billable files and costs up to $1.75.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

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.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: LeanerCloud/cloud-commitments-cli/.coderabbit.yaml

Review profile: CHILL

Plan: Essentials

Run ID: ce0f9626-4b03-4946-b653-79d734e3de17

📥 Commits

Reviewing files that changed from the base of the PR and between 00e6d3a and 28fb7c9.

📒 Files selected for processing (7)
  • README.md
  • cmd/main.go
  • cmd/multi_service_helpers.go
  • cmd/validators.go
  • cmd/validators_test.go
  • docs/cli/README.md
  • docs/cli/purchase-safety.md

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Sep 28, 2026

Copy link
Copy Markdown
Member Author

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).

@cristim
cristim merged commit 7951e77 into main Sep 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): --idempotency-window silently ignored in CLI direct-purchase path

1 participant