Repository navigation
test(purchase): prove the plan-step money path end to end and its crash points - #839
Conversation
…sh points S9: an Azure step 2 priced from the unsuppressed stored row buys through executeAndFinalize; the same step priced from the suppressed row, or repriced between notify and run, is refused before the provider and the step fails. S11: crash before commit persists nothing; crash after the email re-sends and promotes without resolving again; after the provider call a re-drive reuses DeriveIdempotencyToken(lineage, index) for every provider, and providers without a duplicate guard are refused on re-drive. Refs #631 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gate verdict (Opus 5.5
|
| Mutant | Killed by |
|---|---|
drop staleAzurePricingRefusal from executeAndFinalize (manager.go:246) |
S9 suppressed-row + repriced cases |
| price from the unscaled row (planstep/resolve.go:236) | S9 "resolved from the stored row buys" |
| token index fixed to 0 (execution.go:651) | TestRedriveReusesDerivedTokensForEveryProvider (4 providers) |
token from ExecutionID instead of lineage |
same |
token not propagated to PurchaseOptions |
same |
| attach sort reversed (plan_step_attach.go:156) | B3 attach tests (unit + integration) |
| Azure SP re-drive allowed (manager.go:~499) | TestRedriveRefusedWhereNoProviderGuardExists + existing guard tests |
| suppression write error ignored (plan_step_attach.go:244) | TestCrash_BeforeCommitPersistsNothing |
| lost token rotation treated as success (notifications.go:146) | TestCrash_AfterEmailBeforeNotifiedResendsAndPromotes |
Provider token trace in pinned cloud-commitments-go (ad67030387e4): AWS SP input.ClientToken providers/aws/services/savingsplans/client.go:247-248; RDS idempotencyGuard rds/client.go:150 and IdempotentReservationID :205; EC2 tag lookup ec2/client.go:139-140; Azure compute/cache/cosmosdb/database/managedredis/search/synapse all call reservations.DoIdempotentPurchaseTwoStep(..., opts.IdempotencyToken) (e.g. compute/client.go:508, managedredis/client.go:344; lookup in services/internal/reservations/purchase.go:647-657); GCP insertReq.RequestId = IdempotencyGUID(token) computeengine/client.go:931-932, tested client_test.go:778-810. Issue #838 is open and lists the gaps.
Runbook (S14): DEV only, small cap, notification_days_before >= 1, manual send_notifications, CANCEL, kill switch back off, never process_scheduled_purchases on real accounts, prod untouched. OK.
Linux parity: token capture uses a mutex and map equality (no order dependence); integration tests compare no DB timestamps. CI green on this SHA, including Integration Tests on Linux.
Non-blocking note (docs accuracy, no code risk): the PR body and #838 say Azure Savings Plans uses a UnixNano alias with no dedupe. In the pinned module, savingsplans/client.go:373 derives the alias from the token via common.ReservationOrderID(opts.IdempotencyToken, fallback) (pkg/common/tokens.go:115); UnixNano is only the empty-token (CLI) fallback. Whether Azure treats a repeated alias PUT as idempotent is not proven in the module, so keeping the platform re-drive refusal is still the safe call; the comment at manager.go:~496 ("timestamp-based alias name") and #838 should be updated.
Merge-result check: main advanced to 7657a38 (#837, ladder, touches internal/purchase) after this head; GitHub reports CLEAN. The would-be merge tree (fa08e58b) vets with and without -tags integration, and internal/purchase unit + integration tests pass on it locally.
Merging with --squash --match-head-commit 2a8045de58365254e16bcc1c1fc08b8c3ef002f5.
What (B4 of #631 PR2b-2b: proofs; tests only, no production change)
TestAzureStepTwo_ExecuteAndFinalize, mock provider, fixtures): step 2 after step-1 suppressions, priced from the unsuppressed stored row, buys; priced from the suppressed row it is refused before the provider (mutation: removing the stale-pricing call from executeAndFinalize fails 2 of 3 cases); a price change between notify and run is refused and the step status isfailed.TestRedriveReusesDerivedTokensForEveryProvidershows every recommendation getsDeriveIdempotencyToken(lineage, index)and a re-drive gets the identical tokens (aws rds, aws savings-plans, azure compute, gcp compute). The attach path persists recommendations sorted (B3), which is what keeps the index stable.Provider proof matrix (pinned cloud-commitments-go, go.mod ad67030387e4)
ClientToken(savingsplans/client.go:247)PurchaseCommitment_Idempotent_*DoIdempotentPurchaseTwoStep(token tag + reservation-order lookup)RequestIdfrom the token + deterministic name (computeengine/client.go:931)Double-buy risks (no provider-side proof): Azure Savings Plans (alias from
UnixNano, no dedupe; the platform refuses re-drive, so only a lost-response first attempt is exposed). EC2/Redshift: guarded by the platform refusal, not a risk. GCP Cloud SQL, Cloud Storage and Memorystore cannot purchase. Any unknown provider is refused. Tracked in #838.S14 DEV acceptance runbook (owner executes; nothing here turns anything on)
Do not run
process_scheduled_purchasesagainst real accounts. The kill switch stays OFF everywhere until you run this.PUT /api/configwithscheduling.auto_purchase=true; on one test plan set a smallmax_step_commitmentandnotification_days_before >= 1, and enable only the plan services you want resolved./api/scheduled/send_notificationsby hand (no Terraform schedule fires it;process_scheduled_purchasesis scheduled only in cloudformation template.yaml:~728).notified, recommendations sorted,scheduled_dateat leastnotification_days_beforedays out, suppressions present, the email shows count, total commitment, cap and per-row details.scheduling.auto_purchaseback to false.Prod: leave the kill switch off and caps NULL until you decide to flip them.
State
Kill switch OFF in every environment, caps NULL (refused). This PR changes no behavior. Linux parity: no new test compares database times; the attach fixtures already truncate to microseconds (#836). Remaining owner-listed items keep this at
Refs #631.Refs #631
🤖 Generated with Claude Code
Summary by CodeRabbit