diff --git a/cmd/csv_real_purchase_test.go b/cmd/csv_real_purchase_test.go index a3deb8aae..90def6f85 100644 --- a/cmd/csv_real_purchase_test.go +++ b/cmd/csv_real_purchase_test.go @@ -103,6 +103,8 @@ type csvPurchaseEnv struct { // expectPurchaseFailure makes run require errPurchasesFailed (#2157): // a real run with any failed purchase now returns it so the CLI exits non-zero. expectPurchaseFailure bool + // cfgMod adjusts the Config run builds, e.g. to mimic the flags of a real invocation. + cfgMod func(*Config) } // newCSVPurchaseEnv wires the stub with the RDS and ElastiCache offering and @@ -135,6 +137,9 @@ func newCSVPurchaseEnvFor(t *testing.T, csv string, fixtureSets ...map[string]aw recs, err := loadRecommendationsFromCSV(csvPath) require.NoError(t, err) cfg := Config{CallerAccountID: defaultTestAccount, CSVOutput: filepath.Join(dir, report), AuditLog: filepath.Join(dir, "audit.jsonl")} + if env.cfgMod != nil { + env.cfgMod(&cfg) + } err = runCSVPurchases(context.Background(), awsCfg, recs, false, cfg, "run-"+report) if env.expectPurchaseFailure { require.ErrorIs(t, err, errPurchasesFailed) @@ -392,3 +397,27 @@ func TestLoadRecommendationsFromCSV_MemoryDBEngineIsCanonical(t *testing.T) { assert.Equal(t, &common.CacheDetails{Engine: "redis", NodeType: "db.t4g.small"}, recs[0].Details, "engine=%q", engine) } } + +// TestCSVRealPurchase_IDLabelsAppliedCoverage pins #2248: the generated id's +// sizing label must match the coverage the CSV run applied. Without --coverage +// the flag default (80) is unused and the rows are bought at 100%. RDS uses the +// generated id as ReservedDBInstanceId; the ElastiCache client names its own. +func TestCSVRealPurchase_IDLabelsAppliedCoverage(t *testing.T) { + tests := []struct { + name string + cfg func(*Config) + want string + }{ + {"no --coverage buys 100%", func(c *Config) { c.Coverage = 80; c.CoverageFromDefault = true }, "-100pct-"}, + {"explicit --coverage 100", func(c *Config) { c.Coverage = 100 }, "-100pct-"}, + {"target-coverage wins", func(c *Config) { c.Coverage = 80; c.CoverageFromDefault = true; c.TargetCoverage = 100 }, "-100pct-"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := newCSVPurchaseEnv(t) + env.cfgMod = tt.cfg + env.run("report1.csv") + assert.Contains(t, env.purchaseForm(t, rdsHost+" "+rdsPurchase).Get("ReservedDBInstanceId"), tt.want) + }) + } +} diff --git a/cmd/multi_service.go b/cmd/multi_service.go index 3f7cb312d..0fb0eb9e1 100644 --- a/cmd/multi_service.go +++ b/cmd/multi_service.go @@ -772,6 +772,10 @@ func runToolFromCSV(ctx context.Context, cfg Config) error { // them, and writes the report and summary for whatever ran. A non-nil error // is errAuditTrailLost and/or errPurchasesFailed. func runCSVPurchases(ctx context.Context, awsCfg aws.Config, recs []common.Recommendation, isDryRun bool, cfg Config, runID string) error { + // Purchase IDs label the sizing via cfg.Coverage (effectiveSizingPct); + // pin it to the coverage the CSV sizing used so the label cannot drift (#2248). + cfg.Coverage = determineCSVCoverage(cfg) + // Create account alias cache for lookup accountCache := NewAccountAliasCache(awsCfg)