From 69a97e45b3a38bc2a269a8234659f3a433d87cc8 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 16 Oct 2026 03:04:51 +0100 Subject: [PATCH] fix(cmd): reject Savings Plan hourly commitments that round to 0.00 The library sends HourlyCommitment to CreateSavingsPlan formatted with "%.2f", so a positive commitment below 0.005 passed the `> 0` precondition and was submitted as "0.00". The precondition now rejects any commitment that formats as 0.00, on both the dry-run and real purchase paths. Closes #2163 Co-Authored-By: Claude Opus 5.5 --- cmd/savings_plan_purchase.go | 9 +++++++++ cmd/savings_plan_purchase_test.go | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/cmd/savings_plan_purchase.go b/cmd/savings_plan_purchase.go index 1f795f7d8..d293b839b 100644 --- a/cmd/savings_plan_purchase.go +++ b/cmd/savings_plan_purchase.go @@ -16,6 +16,12 @@ import ( // regions and are not supported for Savings Plan purchases. const spAPIRegion = "us-east-1" +// spCommitmentFormat is how the library formats HourlyCommitment for the +// CreateSavingsPlan call (savingsplans/client.go). A positive value that +// formats as "0.00" would be sent as a zero commitment, so preconditions +// reject it. +const spCommitmentFormat = "%.2f" + // savingsPlanRowKey identifies one Savings Plan purchase. Two rows with the // same key buy the same plan twice. func savingsPlanRowKey(rec common.Recommendation) string { @@ -142,6 +148,9 @@ func validateSavingsPlanPreconditions(rec common.Recommendation) error { if d.HourlyCommitment <= 0 { return fmt.Errorf("%s recommendation has no positive hourly commitment (got %v)", rec.Service, d.HourlyCommitment) } + if fmt.Sprintf(spCommitmentFormat, d.HourlyCommitment) == "0.00" { + return fmt.Errorf("%s recommendation's hourly commitment %v would be sent as 0.00; it must be at least 0.005", rec.Service, d.HourlyCommitment) + } if rec.Service == common.ServiceSavingsPlansEC2Instance && d.Region == "" { return fmt.Errorf("EC2 Instance Savings Plan needs a region to pick the offering") } diff --git a/cmd/savings_plan_purchase_test.go b/cmd/savings_plan_purchase_test.go index dbf095228..d4b7455c5 100644 --- a/cmd/savings_plan_purchase_test.go +++ b/cmd/savings_plan_purchase_test.go @@ -286,6 +286,25 @@ func TestValidatePurchasePreconditions(t *testing.T) { } } +// TestValidatePurchasePreconditions_RejectsCommitmentThatRoundsToZero covers +// issue #2163: the library sends the hourly commitment as "%.2f", so a positive +// value below 0.005 would reach CreateSavingsPlan as "0.00". +func TestValidatePurchasePreconditions_RejectsCommitmentThatRoundsToZero(t *testing.T) { + withCommitment := func(c float64) common.Recommendation { + rec := parserShapedSP() + rec.Details = &common.SavingsPlanDetails{PlanType: "Compute", HourlyCommitment: c} + return rec + } + for _, c := range []float64{0.001, 0.004, 0.00499} { + err := validatePurchasePreconditions(withCommitment(c), "", defaultTestAccount) + require.Error(t, err, "commitment %v rounds to 0.00 and must be rejected", c) + assert.Contains(t, err.Error(), "0.00", "commitment %v", c) + } + for _, c := range []float64{0.005, 0.01, 1.5} { + assert.NoError(t, validatePurchasePreconditions(withCommitment(c), "", defaultTestAccount), "commitment %v", c) + } +} + func TestPurchaseSingleRec_DryRunReportsPreconditionFailure(t *testing.T) { cfg := Config{CallerAccountID: defaultTestAccount, AuditLog: filepath.Join(t.TempDir(), "audit.jsonl")} broken := parserShapedSP()