Skip to content

fix(gcp): wire Count/pricing into GCP converters; refuse fabricated prices - #1047

Merged
cristim merged 10 commits into
feat/multicloud-web-frontendfrom
fix/gcp-recs-count-pricing
Jun 7, 2026
Merged

cristim merged 10 commits into
feat/multicloud-web-frontendfrom
fix/gcp-recs-count-pricing

Conversation

@cristim

@cristim cristim commented Jun 7, 2026 •

Copy link
Copy Markdown
Member

Summary

Verification

  • go build ./... and go test ./... in providers/gcp pass (231 tests).
  • go build ./... in the repo root passes.
  • gocyclo --over 10 finds no violations after the cyclomatic complexity refactor.
  • Four new regression tests in computeengine/client_test.go cover the full converter->purchase path:
    • TestConverterToInsert_CountNonZero_VCPUAmountSet: realistic Recommender payload -> converter -> Count > 0, VCPU Amount > 0 in insert request.
    • TestConverterFillsPricingForScorer: SavingsPercentage > 0; scorer MinSavingsPct=5 passes the recommendation.
    • TestBuildInsertRequest_RefusesZeroCount: Count=0 -> error before Insert is called.
    • TestGetComputePricing_NoCommitmentSKUReturnsError: missing commitment SKU returns error, no fabricated price.
    • TestConvertGCPRecommendation_EmptyParamsDefaultsToMonthly: PaymentOption defaults to "monthly" (fails on pre-fix "upfront").

Notes

Scope expanded

Additional commits added as part of the FOLD-1047 pass from docs/code-review/16-remaining-findings-plan.md:

Also closes #1062 (grouped issue for findings 10-M2, 10-M3, 10-M5, 10-M7, 10-L1, 10-L2, 10-L3, 10-L4, 10-N3)

  • 10-M5 (+ auditor finding): Fix computeengine PaymentOption default from "upfront" to "monthly". GCP CUDs have no upfront-payment option; the prior default caused incorrect purchase-body construction. All four converters now consistently default to "monthly". This supersedes PR fix(gcp): stamp PaymentOption=monthly on all GCP recs (closes #718) #829 ("fix/committed-monthly"): fix(gcp): stamp PaymentOption=monthly on all GCP recs (closes #718) #829 stamped "monthly" in the scheduler purchase body but the converter-layer default remained "upfront", causing a mismatch when RecommendationParams.PaymentOption is empty. That mismatch is now closed at the converter layer. If fix(gcp): stamp PaymentOption=monthly on all GCP recs (closes #718) #829 is still open, it can be closed as superseded by this PR.
  • CI fix: Refactor four convertGCPRecommendation implementations (cloudsql, cloudstorage, memorystore -- scored 18 on gocyclo) and extractVCPUCountFromRecommendation (computeengine -- scored 17) by extracting extractResourceTypeFromContent, extractEstimatedSavings, fill*Pricing, isMemoryAmountOp, and vcpuCountFromOperationGroups helpers.
  • 10-M2: Add termPlan(term) helper and memMBPerVCPU const in computeengine; dedup the "TWELVE_MONTH"/"THIRTY_SIX_MONTH" and 4096 literals across all call sites.
  • 10-M3: Replace substring-match isResourceExhausted with typed checks (errors.As(*googleapi.Error) for REST, status.FromError(codes.ResourceExhausted) for gRPC); substring match retained as fallback.
  • 10-M7: GetAccounts returns empty slice when no ACTIVE projects are visible instead of synthesising a fallback account (which hid credential errors from callers).
  • 10-L1: Remove no-op commitmentType if-branch in convertGCPCommitmentToCommon (both arms were CommitmentCUD).
  • 10-L2: CloudStorage.GetExistingCommitments returns empty; GCS has no commitment API and enumerating regional buckets was not a commitment indicator.
  • 10-L3: CloudSQL.GetExistingCommitments returns empty; PricingPlan "PACKAGE" is a legacy per-instance billing mode, not a spend-based CUD indicator.
  • 10-L4: Wrap memorystore realRecommenderClient.ListRecommendations in realRecommenderIterator for diffability with the other three service clients.
  • 10-N3: Rename local var provider in recommendations.getRegions to p to avoid shadowing the imported provider package.

Deferred (logged to docs/code-review/open-questions/fold-1047.md):

Also closes #1078: CloudSQL unit-mismatch + term-propagation (identical pattern to Memorystore/CloudStorage fixes above; folded into this PR per issue #1078).


Money-path verification pass (independent)

Deep correctness verification of the GCP recommendation->purchase path against the
vendored GCP SDK schema. Found and fixed one real money-path defect; all other
items verified CORRECT.

DEFECT (fixed in this PR): buildInsertRequest and the public GroupCommitments
helper emitted "MEMORY_MB" as the ResourceCommitment.Type. GCP's enum is
VCPU/MEMORY/LOCAL_SSD/ACCELERATOR (confirmed in computepb.ResourceCommitment_Type),
so "MEMORY_MB" is invalid and RegionCommitments.Insert rejects it, failing every
Compute Engine CUD purchase. The Amount unit (MB) was already correct; only the enum
string was wrong. Fixed -> "MEMORY"; inbound isMemoryAmountOp now tolerates both
spellings. Regression tests added that fail on the pre-fix code.

Verified CORRECT: vCPU count extraction (units are a vCPU count, not core-seconds/
dollars, per the SDK ResourceCommitment doc); RequestId + deterministic Name
idempotency (no double-purchase on re-drive); term->plan mapping; Count<=0 guard;
pricing functions return errors (no fabricated prices) when no commitment SKU exists;
divide-by-zero guarded upstream of the savings calc; computeengine
GetExistingCommitments consumes all pages and errors (not truncates) at the cap;
cloudsql/cloudstorage/memorystore empty-return doc comments accurate; scorer
integration (SavingsPercentage/BreakEvenMonths/Count all populated).


Scope expanded (hardcode removal)

Two hardcoded constructs and one audit finding addressed in the latest commit (8f9a787):

TARGET 1 - MEMORY from Recommender payload (fail-loud)

  • Removed the memMBPerVCPU = 4096 const and its use as a ratio fallback.
  • Added memoryMBFromOperationGroups (parallel to vcpuCountFromOperationGroups) to extract the MEMORY resource amount from the Recommender payload's commitment operation group.
  • Added extractMemoryMBFromRecommendation to store the result in ComputeDetails.MemoryGB on the Recommendation (called from convertGCPRecommendation).
  • buildInsertRequest and GroupCommitments now call memoryMBFromDetails which returns an error (not a fallback) when Details.MemoryGB is absent. GCP Recommender does carry the MEMORY amount in the payload; the fail-loud policy ensures non-standard machine families (e.g. high-memory N2) never get an incorrect commitment.
  • Regression tests: TestBuildInsertRequest_RefusesMissingMemory, TestGroupCommitments_SkipsRecsWithoutMemory, TestConverterToInsert_CountNonZero_VCPUAmountSet (asserts the exact payload value 6144 MB, not ratio-derived 16384 MB).

TARGET 2 - termPlan uses SDK Commitment_Plan enum (fail-loud)

  • termPlan now returns (string, error) using computepb.Commitment_TWELVE_MONTH.String() and computepb.Commitment_THIRTY_SIX_MONTH.String().
  • Accepts all forms: 1yr/1/12mo (TWELVE_MONTH) and 3yr/3/36mo (THIRTY_SIX_MONTH).
  • Returns an error on unrecognised/empty term -- no silent 12-month default that could purchase the wrong duration.
  • Error threaded through buildInsertRequest, GroupCommitments, and convertGCPRecommendation (returns nil on bad term).
  • Regression tests: TestTermPlan_UsesSdkEnumConstants, TestTermPlan_RejectsUnknownTerm, TestTermPlan_AcceptsAllDocumentedForms.

H-3 audit finding - convertGCPRecommendation propagates params.Term

  • No longer hardcodes Term: "1yr" regardless of params.Term; propagates the requested term (defaults to "1yr" when empty) and validates it via termPlan, returning nil for unroutable recs.
  • Regression tests: TestConvertGCPRecommendation_PropagatesParamsTerm, TestConvertGCPRecommendation_RejectsUnknownTerm.

Verification: 247 GCP tests pass, 57 computeengine tests pass under -race. Repo-root go build ./... clean.


Scope expanded (H-1 and H-2 fixes)

Latest commit (95a390f) addresses two additional High findings from the GCP broad audit.

H-1 -- Rec state never filtered (double-purchase vector)

  • All four GCP service clients (computeengine, cloudsql, memorystore, cloudstorage) now skip recommendations whose StateInfo.State is not ACTIVE. The GCP Recommender returns all states (ACTIVE/CLAIMED/SUCCEEDED/FAILED/DISMISSED) unless filtered at the API layer; acting on a non-ACTIVE rec is a cross-run double-purchase vector and inflates actionable rec counts.
  • Uses typed enum constant recommenderpb.RecommendationStateInfo_ACTIVE (not a string). The nil-safe proto getter means nil-StateInfo recs (STATE_UNSPECIFIED) are also filtered.
  • Regression tests: TestGetRecommendations_FiltersNonActiveStates (five-rec mock, only ACTIVE passes) and TestGetRecommendations_ActiveRecIncluded.

H-2 -- Cache/storage advertised but produce zero recs

  • Decision: wire (not trim). Both memorystore and cloudstorage have complete, working GetRecommendations implementations; their PurchaseCommitment paths being advisory-only no-ops is orthogonal to surfacing recommendations.
  • collectRegion now fans out to all four services concurrently (guarded by shouldIncludeService). regionResult gains cache and storage slices; the merge in GetRecommendations appends them per region.
  • Regression tests: TestRegionResult_HasCacheAndStorageFields and TestShouldIncludeService_Cache_Storage.

257 GCP tests pass under -race. go build/vet ./... clean.

Remaining Medium/Low/Nit findings from the broad audit tracked in issue LeanerCloud/cloud-commitments-go#6.

…rices

Closes #1022. Addresses the GCP portion of #1020.

**C1 (issue #1022):** `convertGCPRecommendation` (all 4 services) never set
`Count`, causing `buildInsertRequest` to request a 0-vCPU / 0-MB commitment.
Now extracts the vCPU count from the Recommender operation's structpb numeric
value (or falls back to the overview `numericValue`). `buildInsertRequest` and
`PurchaseCommitment` now refuse `rec.Count <= 0` with an explicit error before
reaching the GCP API.

**C2 (issue #1022):** All four `convertGCPRecommendation` implementations now
call the service-specific pricing function to populate `CommitmentCost`,
`OnDemandCost`, `SavingsPercentage`, and `BreakEvenMonths`. A scorer with any
`MinSavingsPct` filter will no longer silently drop all GCP recommendations.
Pricing failures are logged and the recommendation is still returned (the
Recommender-derived `EstimatedSavings` is the authoritative figure).

**M4 (issue #1020, GCP side):** All four `get*Pricing` functions previously
fabricated a commitment price from hardcoded discount multipliers when no
commitment SKU existed in the catalog, surfacing it as a real `OfferingDetails`
figure. Following the `managedredis` precedent, these now return an error
("no commitment pricing found") instead of a fabricated number, consistent with
`feedback_nullable_not_zero` and `feedback_empty_string_vs_error`.

**M5 (issue #1022):** `PaymentOption` is now derived from
`RecommendationParams.PaymentOption` (with a service-appropriate default when
empty) rather than being hardcoded per service.

**H2 (cloudstorage):** Fixed the iterator error swallow (`break` on error ->
`return nil, err`) to match the other three service clients.

Regression tests added in `computeengine/client_test.go`:
- `TestConverterToInsert_CountNonZero_VCPUAmountSet`: realistic Recommender
  payload -> converter -> buildInsertRequest -> VCPU Amount > 0.
- `TestConverterFillsPricingForScorer`: converter fills SavingsPercentage;
  scorer with MinSavingsPct=5 passes the recommendation.
- `TestBuildInsertRequest_RefusesZeroCount`: Count <= 0 -> error, no Insert.
- `TestGetComputePricing_NoCommitmentSKUReturnsError`: missing commitment SKU
  -> error (no fabricated price).

Related: #718, #829 (payment-option monthly stamp).
@cristim cristim added bug Something isn't working triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/bug Defect labels Jun 7, 2026
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 23 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e66da404-733d-41df-89bb-3ee15ca0883d

📥 Commits

Reviewing files that changed from the base of the PR and between c6280c3 and 33c0c7d.

📒 Files selected for processing (10)
  • providers/gcp/recommendations.go
  • providers/gcp/recommendations_test.go
  • providers/gcp/services/cloudsql/client.go
  • providers/gcp/services/cloudsql/client_test.go
  • providers/gcp/services/cloudstorage/client.go
  • providers/gcp/services/cloudstorage/client_test.go
  • providers/gcp/services/computeengine/client.go
  • providers/gcp/services/computeengine/client_test.go
  • providers/gcp/services/memorystore/client.go
  • providers/gcp/services/memorystore/client_test.go
📝 Walkthrough

Walkthrough

This PR refactors GCP provider and service clients to remove synthesized commitments, thread RecommendationParams through recommendation conversion, require commitment SKUs in billing catalog (erroring when absent), enforce pagination/cancellation behavior, and populate pricing/savings fields while logging catalog failures.

Changes

GCP Pricing and Commitment Model Refactoring

Layer / File(s) Summary
Provider-level account and region handling
providers/gcp/provider.go, providers/gcp/provider_test.go, providers/gcp/recommendations.go
GetAccounts returns empty when no ACTIVE projects are found instead of synthesizing a fallback. Variable shadowing in getRegions is resolved by renaming the temporary provider to p.
Cloud SQL commitment stubs and pricing refactor
providers/gcp/services/cloudsql/client.go, providers/gcp/services/cloudsql/client_test.go
GetExistingCommitments now returns empty. SKU extraction classifies commitment vs on-demand pricing and errors when commitment pricing is absent. Recommendation conversion threads RecommendationParams, defaults payment option to monthly, and populates pricing/savings via helpers while logging failures.
Cloud Storage commitment stubs, iteration, and pricing refactor
providers/gcp/services/cloudstorage/client.go, providers/gcp/services/cloudstorage/client_test.go
GetExistingCommitments returns empty. Recommendation iteration enforces a page limit, checks context cancellation, and propagates iterator errors. Pricing errors when commitment catalog data is missing. Recommendation conversion is parameterized, defaults payment option to monthly, and fills pricing while logging failures; tests updated to require both SKU types.
Compute Engine commitment purchase and resource validation
providers/gcp/services/computeengine/client.go, providers/gcp/services/computeengine/client_test.go
Introduces fixed 4096 MB-per-vCPU memory ratio and term-to-plan mapping helper. buildInsertRequest validates positive recommendation count and uses canonical MEMORY enum for resource encoding. Request construction failures are propagated and quota errors are classified via typed checks.
Compute Engine pricing and recommendation conversion
providers/gcp/services/computeengine/client.go, providers/gcp/services/computeengine/client_test.go
Pricing errors on missing commitment catalog data. Recommendation conversion threads RecommendationParams, forces monthly payment option, extracts vCPU counts with overview fallback while skipping memory ops (legacy and canonical), and logs pricing failures while retaining recommendations.
Memorystore Redis pricing and recommendation conversion
providers/gcp/services/memorystore/client.go, providers/gcp/services/memorystore/client_test.go
Pricing errors when commitment catalog pricing is absent. Recommendation conversion accepts RecommendationParams, defaults term/payment, extracts resource type and estimated savings, and populates pricing/break-even while logging unavailable pricing; tests require commitment SKUs and validate term-total semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Poem

🐰 I chewed the fallback, nibbled the fake SKU,
Now invoices whisper only what is true.
Monthly beats the drum, terms counted right,
Commitments appear when catalog gives light.
Hopping on logs, I leave no silent clue—hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main objectives: wiring Count and pricing into GCP converters and refusing fabricated prices, which aligns with the primary fixes across all four service converters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gcp-recs-count-pricing

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

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added 2 commits June 7, 2026 04:19
GCP CUDs have no upfront-payment option; the previous default of "upfront"
caused downstream purchase-body construction to silently use a mode that
does not exist for CUDs. Changes to align with cloudsql/memorystore/
cloudstorage which already default to "monthly" and with the monthly stamp
introduced by PRs #718/#829 (which this supersedes in the converter layer).

Regression tests:
- TestConvertGCPRecommendation_EmptyParamsDefaultsToMonthly: FAILS on
  pre-fix code (paymentOption == "upfront"); PASSES after.
- TestConvertGCPRecommendation_ParamPaymentOptionRespected: caller-supplied
  value is forwarded unchanged.

Closes #1062
…ters

The four convertGCPRecommendation implementations (cloudsql, cloudstorage,
memorystore) scored 18 and extractVCPUCountFromRecommendation scored 17 on
gocyclo, failing the pre-commit hook. Refactor:

- Add extractResourceTypeFromContent/extractEstimatedSavings helpers shared
  by all three service converters; reduce each converter body.
- Add fillRedisPricing/fillSQLPricing/fillStoragePricing per-service helpers
  that isolate the if-pricing-err-ok branching.
- Add isMemoryAmountOp + vcpuCountFromOperationGroups to split the nested
  path-filter scan in extractVCPUCountFromRecommendation.

All 235 GCP tests pass; gocyclo --over 10 finds no violations.
10-M2: Add termPlan(term) helper and memMBPerVCPU const in computeengine;
       dedup the "TWELVE_MONTH"/"THIRTY_SIX_MONTH" and 4096 literals.

10-M3: Replace substring-match isResourceExhausted with typed checks:
       errors.As(*googleapi.Error, Code==429) for REST, status.FromError
       codes.ResourceExhausted for gRPC; substring match kept as fallback
       for non-standard wrapped errors.

10-M7: GetAccounts returns empty slice when no ACTIVE projects are visible
       instead of synthesising a fallback account from p.projectID, which
       would silently hide credential/permission errors from callers. Test
       updated to assert empty (not 1-element) on zero-projects input.

10-L1: Remove the no-op commitmentType if-branch in convertGCPCommitmentToCommon
       (both arms assigned CommitmentCUD).

10-L2: CloudStorage GetExistingCommitments returns empty; enumerating
       regional buckets is not a commitment indicator (GCS has no CUD API).
       Test updated accordingly.

10-L3: CloudSQL GetExistingCommitments returns empty; PricingPlan "PACKAGE"
       is a legacy per-instance billing mode, not a spend-based CUD. Test
       updated accordingly.

10-L4: Wrap memorystore realRecommenderClient.ListRecommendations in
       realRecommenderIterator for diffability with the other 3 service
       clients.

10-N3: Rename local var 'provider' in recommendations.getRegions to 'p' to
       avoid shadowing the imported 'provider' package.

Deferred (logged to docs/code-review/open-questions/fold-1047.md):
- 10-M1: dead GroupCommitments types -- no-delete: pending owner confirmation
- 10-M6: struct-stored ctx removal -- tests assert on ctx field; needs test
         update before removal (correctness impact: none, field is never read)
- 10-N1: common.Ptr[T] -- helper does not yet exist; lands with #1035

Closes #1062
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

FOLD-1047 additions summary

Three additional commits on this PR address the remaining FOLD-1047 findings from docs/code-review/16-remaining-findings-plan.md:

Commit 1: fix(gcp/computeengine): default PaymentOption to "monthly" (10-M5)

  • 10-M5 + auditor finding: computeengine/client.go -- default from "upfront" to "monthly". GCP CUDs have no upfront option. All four converters now consistently default to "monthly". Supersedes PR fix(gcp): stamp PaymentOption=monthly on all GCP recs (closes #718) #829 (monthly stamp in scheduler path; this closes the converter-layer gap).
  • Regression tests: TestConvertGCPRecommendation_EmptyParamsDefaultsToMonthly (FAILS pre-fix), TestConvertGCPRecommendation_ParamPaymentOptionRespected.

Commit 2: refactor(gcp): extract helpers to fix cyclomatic complexity (CI fix)

  • CI was failing: convertGCPRecommendation (cloudsql, cloudstorage, memorystore) scored 18; extractVCPUCountFromRecommendation scored 17. Both exceed the gocyclo threshold of 10.
  • Extracted: extractResourceTypeFromContent, extractEstimatedSavings, fillRedisPricing/fillSQLPricing/fillStoragePricing, isMemoryAmountOp, vcpuCountFromOperationGroups.
  • All violations resolved; gocyclo --over 10 passes clean.

Commit 3: fix(gcp): FOLD-1047 correctness findings (10-M2,M3,M7,L1,L2,L3,L4,N3)

  • 10-M2: termPlan(term) helper + memMBPerVCPU const; dedup inline literals.
  • 10-M3: Typed isResourceExhausted: errors.As(*googleapi.Error, Code==429) for REST, status.FromError(codes.ResourceExhausted) for gRPC.
  • 10-M7: GetAccounts returns empty on zero ACTIVE projects (no synthesised fallback).
  • 10-L1: Remove no-op commitmentType branch (both arms assigned CommitmentCUD).
  • 10-L2: CloudStorage.GetExistingCommitments returns empty; GCS has no CUD API.
  • 10-L3: CloudSQL.GetExistingCommitments returns empty; PricingPlan=="PACKAGE" is not a commitment indicator.
  • 10-L4: Wrap memorystore iterator in realRecommenderIterator for diffability.
  • 10-N3: Rename local provider var in recommendations.getRegions to p.

Deferred to docs/code-review/open-questions/fold-1047.md: 10-M1 (no-delete pending owner confirm), 10-M6 (ctx field, tests assert on it), 10-N1 (common.Ptr[T] not yet available).

Closes #1062

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough FOLD-1047 additions summary. The three commits cover a solid set of improvements — particularly the 10-M5 PaymentOption default correction (closing the converter-layer gap that PR #829 left open), the gocyclo CI fix via helper extraction, and the typed isResourceExhausted checks (10-M3). The deferred items (10-M1, 10-M6, 10-N1) are well-reasoned deferrals.

Kicking off the incremental review of the new commits now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), `internal/commitmentopts/normalizePayment` is unexported and AWS-spelling-focused (parses "All Upfront"/"ALL_UPFRONT" → canonical AWS tokens). `internal/api/validation.go:purchasePaymentWhitelist` is the purchase-execute boundary. `pkg/common/reservation_name.go:normalizeReservationPayment` produces short-form name segments. These are all distinct concerns from the cross-provider `NormalizePaymentOption` helper in `internal/config/validation.go`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…1022)

GCP's ResourceCommitment.Type enum is VCPU/MEMORY/LOCAL_SSD/ACCELERATOR.
buildInsertRequest and the public GroupCommitments helper emitted the
invalid type string "MEMORY_MB" for the memory member, so every Compute
Engine CUD purchase would be rejected by RegionCommitments.Insert. The
Amount unit (MB) was already correct; only the enum string was wrong.

Switch the outbound type to "MEMORY", make the inbound Recommender memory
detector (isMemoryAmountOp) tolerate both "MEMORY" and the legacy
"MEMORY_MB" spelling since the path_filter wire format is not documented,
and update the resource-pair doc comments.

Regression tests: TestGroupCommitments_UsesValidMemoryEnum and
TestIsMemoryAmountOp_MatchesBothSpellings added; the converter->insert
test now rejects any invalid ResourceCommitment.Type. All fail on the
pre-fix "MEMORY_MB" code and pass after.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Money-path verification + fix

Independent deep correctness verification of the GCP recommendation -> purchase
path against the vendored GCP SDK schema. One real money-path defect found and
fixed; everything else verified CORRECT.

Fixed (commit on this PR): buildInsertRequest / GroupCommitments used
Type: "MEMORY_MB" for the memory resource. The valid
computepb.ResourceCommitment_Type enum is VCPU/MEMORY/LOCAL_SSD/ACCELERATOR,
so "MEMORY_MB" is rejected by RegionCommitments.Insert and fails every
Compute Engine CUD purchase
. Switched to "MEMORY" (Amount stays in MB);
inbound isMemoryAmountOp now matches both "MEMORY" and the legacy
"MEMORY_MB" defensively. Three regression tests fail on the pre-fix enum and
pass after.

Verified CORRECT (no change needed):

  • vCPU count units: the Recommender VCPU amount is a vCPU count, not
    core-seconds/hours or dollars (per ResourceCommitment.Amount SDK doc).
  • Idempotency: RequestId via IdempotencyGUID + deterministic Name; a re-drive
    cannot double-purchase.
  • term->plan (1yr->TWELVE_MONTH, 3yr->THIRTY_SIX_MONTH), GENERAL_PURPOSE type,
    Count<=0 guard, 4096 MB/vCPU documented assumption.
  • All four pricing functions error out (no fabricated price) when no commitment
    SKU is present; savings divide-by-zero guarded upstream.
  • computeengine.GetExistingCommitments consumes all pages and errors (not
    truncates) at the page cap; cloudsql/cloudstorage/memorystore empty-return doc
    comments are accurate.
  • Scorer integration: SavingsPercentage / BreakEvenMonths / Count all populated.

Verification: providers/gcp go build/go vet clean, go test -race ./... 233
passed; repo-root go build ./... OK.

@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
providers/gcp/services/computeengine/client.go (1)

867-885: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize non-monthly GCP payment options instead of passing them through.

Line 867-Line 875 currently preserves any non-empty params.PaymentOption. If the scheduler provides "upfront", the recommendation carries an invalid GCP payment mode instead of canonical "monthly".

💡 Suggested fix
-	paymentOption := params.PaymentOption
-	if paymentOption == "" {
-		// GCP CUDs are billed monthly with no upfront option; "monthly" is
-		// the only valid default. Aligns with cloudsql/memorystore/cloudstorage
-		// and supersedes the monthly stamp introduced in PR `#829` (which
-		// stamped "monthly" in the purchase body but not in the converter
-		// default -- see PR `#1047` for the supersession note).
-		paymentOption = "monthly"
-	}
+	paymentOption := "monthly"
+	if params.PaymentOption != "" && !strings.EqualFold(params.PaymentOption, "monthly") {
+		log.Printf("computeengine: unsupported GCP payment option %q; forcing monthly", params.PaymentOption)
+	}

Based on learnings, GCP CUDs in this repository are monthly-only and non-monthly inputs (including legacy "upfront") should be collapsed to "monthly" at conversion/scheduler boundaries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/computeengine/client.go` around lines 867 - 885, The
code currently preserves any non-empty params.PaymentOption; change the
normalization so that only the canonical "monthly" is allowed: read
params.PaymentOption into paymentOption, and if it is empty or not exactly
"monthly" then set paymentOption = "monthly" before constructing the
common.Recommendation (the block that sets PaymentOption using paymentOption).
Ensure this logic touches the params.PaymentOption -> paymentOption handling in
client.go so legacy values like "upfront" are collapsed to "monthly" at
conversion time.

Source: Learnings

providers/gcp/services/memorystore/client.go (2)

490-518: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate the requested term instead of forcing 1yr.

This converter now accepts RecommendationParams, but it still hardcodes rec.Term to "1yr". Any caller requesting 3yr Memorystore recommendations will get the wrong term and the wrong pricing path.

Suggested fix
 func (c *MemorystoreClient) convertGCPRecommendation(ctx context.Context, gcpRec *recommenderpb.Recommendation, params common.RecommendationParams) *common.Recommendation {
 	paymentOption := params.PaymentOption
 	if paymentOption == "" {
 		paymentOption = "monthly"
 	}
+	term := params.Term
+	if term == "" {
+		term = "1yr"
+	}
 
 	rec := &common.Recommendation{
 		Provider:       common.ProviderGCP,
 		Service:        common.ServiceCache,
 		Account:        c.projectID,
 		Region:         c.region,
 		CommitmentType: common.CommitmentCUD,
 		Timestamp:      time.Now(),
-		Term:           "1yr",
+		Term:           term,
 		PaymentOption:  paymentOption,
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/memorystore/client.go` around lines 490 - 518, The
convertGCPRecommendation function currently hardcodes rec.Term = "1yr"; update
it to propagate the requested term from the RecommendationParams (use
params.Term) and default to "1yr" only when params.Term is empty, then use
rec.Term when computing termYears for pricing and when calling fillRedisPricing;
specifically, set rec.Term = params.Term || "1yr" (preserving existing handling
of "3yr" and "3" to map to termYears=3) so callers requesting "3yr" get the
correct term and pricing path in convertGCPRecommendation and fillRedisPricing.

312-340: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This is still mixing unit-price SKUs with term-total costs.

extractPriceFromSKU returns a raw UnitPrice, but only onDemandPrice is expanded to hoursInTerm. commitmentPrice remains in single-unit form, so the returned CommitmentPrice, HourlyRate, SavingsPercentage, BreakEvenMonths, and GetOfferingDetails.TotalCost are all off by the term multiplier. Normalize the commitment SKU to the same term-total basis before using it in any downstream math.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/memorystore/client.go` around lines 312 - 340,
getRedisPricing is mixing per-unit SKUs with term-total costs:
extractPricingFromSKUs / extractPriceFromSKU returns unit prices but only
onDemandPrice is multiplied by hoursInTerm; commitmentPrice must be normalized
to the same term-total basis before any math. Update getRedisPricing to convert
commitmentPrice to term total (e.g., multiply the commitment unit price by
hoursInTerm or the appropriate term multiplier returned by
extractPricingFromSKUs) before computing HourlyRate, CommitmentPrice,
SavingsPercentage, BreakEvenMonths and any GetOfferingDetails.TotalCost; keep
OnDemandPrice computed as now (onDemand unit * hoursInTerm) and then derive
HourlyRate as normalizedCommitmentTotal / hoursInTerm and pass the normalized
commitment total into RedisPricing.
providers/gcp/services/cloudstorage/client.go (2)

499-527: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor RecommendationParams.Term here.

rec.Term is hardcoded to "1yr", so a caller requesting 3yr still gets a 1-year recommendation and 1-year pricing. Callers already pass the desired term through RecommendationParams.Term, so this drops the user's selection for Cloud Storage recommendations.

Suggested fix
 func (c *CloudStorageClient) convertGCPRecommendation(ctx context.Context, gcpRec *recommenderpb.Recommendation, params common.RecommendationParams) *common.Recommendation {
 	paymentOption := params.PaymentOption
 	if paymentOption == "" {
 		paymentOption = "monthly"
 	}
+	term := params.Term
+	if term == "" {
+		term = "1yr"
+	}
 
 	rec := &common.Recommendation{
 		Provider:       common.ProviderGCP,
 		Service:        common.ServiceStorage,
 		Account:        c.projectID,
 		Region:         c.region,
 		CommitmentType: common.CommitmentReservedCapacity,
 		Timestamp:      time.Now(),
-		Term:           "1yr",
+		Term:           term,
 		PaymentOption:  paymentOption,
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/cloudstorage/client.go` around lines 499 - 527, In
convertGCPRecommendation, rec.Term is hardcoded to "1yr" which ignores
params.RecommendationParams.Term; change it to derive rec.Term from params.Term
(defaulting to "1yr" when empty) so callers requesting "3yr" are honored, and
keep the existing termYears logic (which reads rec.Term) and the call to
c.fillStoragePricing(ctx, rec, termYears) so pricing matches the selected term;
update any normalization (e.g., accept "3" or "3yr") when mapping params.Term to
rec.Term.

321-349: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize commitment pricing onto the same basis as on-demand pricing.

extractStoragePriceFromSKU returns a raw UnitPrice, but this function only converts onDemandPrice to a term-total. commitmentPrice stays in unit-price form, so SavingsPercentage, CommitmentPrice, HourlyRate, BreakEvenMonths, and GetOfferingDetails.TotalCost are computed from mismatched units and materially understate the commitment cost. Scale both sides to the same term basis before comparing or returning them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/cloudstorage/client.go` around lines 321 - 349,
getStoragePricing mixes unit and term totals: onDemandPrice is multiplied by
hoursInTerm but commitmentPrice is left as a unit price, causing all downstream
math to be inconsistent. Multiply the raw commitmentPrice returned by
extractStoragePricingFromSKUs by hoursInTerm (e.g., commitmentPriceTerm :=
commitmentPrice * hoursInTerm), then pass commitmentPriceTerm into
calculateStorageSavingsPercentage and set StoragePricing.CommitmentPrice =
commitmentPriceTerm; keep HourlyRate = commitmentPriceTerm / hoursInTerm (or the
original unit rate) and leave OnDemandPrice as onDemandPrice * hoursInTerm so
all comparisons and outputs in getStoragePricing,
calculateStorageSavingsPercentage, and any callers use the same term basis.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@providers/gcp/services/cloudstorage/client.go`:
- Around line 499-527: In convertGCPRecommendation, rec.Term is hardcoded to
"1yr" which ignores params.RecommendationParams.Term; change it to derive
rec.Term from params.Term (defaulting to "1yr" when empty) so callers requesting
"3yr" are honored, and keep the existing termYears logic (which reads rec.Term)
and the call to c.fillStoragePricing(ctx, rec, termYears) so pricing matches the
selected term; update any normalization (e.g., accept "3" or "3yr") when mapping
params.Term to rec.Term.
- Around line 321-349: getStoragePricing mixes unit and term totals:
onDemandPrice is multiplied by hoursInTerm but commitmentPrice is left as a unit
price, causing all downstream math to be inconsistent. Multiply the raw
commitmentPrice returned by extractStoragePricingFromSKUs by hoursInTerm (e.g.,
commitmentPriceTerm := commitmentPrice * hoursInTerm), then pass
commitmentPriceTerm into calculateStorageSavingsPercentage and set
StoragePricing.CommitmentPrice = commitmentPriceTerm; keep HourlyRate =
commitmentPriceTerm / hoursInTerm (or the original unit rate) and leave
OnDemandPrice as onDemandPrice * hoursInTerm so all comparisons and outputs in
getStoragePricing, calculateStorageSavingsPercentage, and any callers use the
same term basis.

In `@providers/gcp/services/computeengine/client.go`:
- Around line 867-885: The code currently preserves any non-empty
params.PaymentOption; change the normalization so that only the canonical
"monthly" is allowed: read params.PaymentOption into paymentOption, and if it is
empty or not exactly "monthly" then set paymentOption = "monthly" before
constructing the common.Recommendation (the block that sets PaymentOption using
paymentOption). Ensure this logic touches the params.PaymentOption ->
paymentOption handling in client.go so legacy values like "upfront" are
collapsed to "monthly" at conversion time.

In `@providers/gcp/services/memorystore/client.go`:
- Around line 490-518: The convertGCPRecommendation function currently hardcodes
rec.Term = "1yr"; update it to propagate the requested term from the
RecommendationParams (use params.Term) and default to "1yr" only when
params.Term is empty, then use rec.Term when computing termYears for pricing and
when calling fillRedisPricing; specifically, set rec.Term = params.Term || "1yr"
(preserving existing handling of "3yr" and "3" to map to termYears=3) so callers
requesting "3yr" get the correct term and pricing path in
convertGCPRecommendation and fillRedisPricing.
- Around line 312-340: getRedisPricing is mixing per-unit SKUs with term-total
costs: extractPricingFromSKUs / extractPriceFromSKU returns unit prices but only
onDemandPrice is multiplied by hoursInTerm; commitmentPrice must be normalized
to the same term-total basis before any math. Update getRedisPricing to convert
commitmentPrice to term total (e.g., multiply the commitment unit price by
hoursInTerm or the appropriate term multiplier returned by
extractPricingFromSKUs) before computing HourlyRate, CommitmentPrice,
SavingsPercentage, BreakEvenMonths and any GetOfferingDetails.TotalCost; keep
OnDemandPrice computed as now (onDemand unit * hoursInTerm) and then derive
HourlyRate as normalizedCommitmentTotal / hoursInTerm and pass the normalized
commitment total into RedisPricing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c522817-8270-45f6-b35c-17ca46075ad6

📥 Commits

Reviewing files that changed from the base of the PR and between cbdc4be and 3491d78.

📒 Files selected for processing (11)
  • providers/gcp/provider.go
  • providers/gcp/provider_test.go
  • providers/gcp/recommendations.go
  • providers/gcp/services/cloudsql/client.go
  • providers/gcp/services/cloudsql/client_test.go
  • providers/gcp/services/cloudstorage/client.go
  • providers/gcp/services/cloudstorage/client_test.go
  • providers/gcp/services/computeengine/client.go
  • providers/gcp/services/computeengine/client_test.go
  • providers/gcp/services/memorystore/client.go
  • providers/gcp/services/memorystore/client_test.go

…d pricing math

- computeengine: force paymentOption="monthly" unconditionally; log non-monthly
  inputs instead of passing them through (GCP CUDs are monthly-only).
- memorystore/cloudstorage: propagate params.Term to rec.Term in
  convertGCPRecommendation; hardcoded "1yr" dropped 3yr caller requests.
- memorystore/cloudstorage: fix unit-mismatch in getRedisPricing/getStoragePricing
  where per-hour commitmentPrice was passed to calculateSavingsPercentage as a
  term total, producing ~99.99% savings. Multiply by hoursInTerm before math;
  HourlyRate is now the raw per-hour SKU rate.

Regression tests: 5 new tests that fail on pre-fix code.
Follow-up for CloudSQL identical issue: closes #1078.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

CR post-commit findings triage (review 4445458345, 2026-06-07T16:47:56Z)

All 5 findings were in the "outside diff range" block but all touch files present in this PR's diff. All 5 are fixed in commit c6280c3.

F1 - computeengine/client.go:867-885 - Non-monthly payment option passed through

Disposition: FIXED

GCP CUDs are monthly-only. The prior code forwarded any non-empty params.PaymentOption (e.g. "upfront") to the recommendation. Now forces paymentOption = "monthly" unconditionally; logs a warning when a non-monthly value is supplied so scheduler misconfiguration is visible. Regression test TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly covers "upfront", "all-upfront", "UPFRONT", "partial-upfront" -- all must become "monthly".

F2 - memorystore/client.go:490-518 - rec.Term hardcoded to "1yr"

Disposition: FIXED

convertGCPRecommendation accepted RecommendationParams but ignored .Term, always emitting Term: "1yr". Fixed to derive from params.Term with "1yr" default. The downstream termYears derivation from rec.Term (already present) now correctly handles "3yr" callers. Regression test TestConvertGCPRecommendation_PropagatesTermFromParams.

F3 - memorystore/client.go:312-340 - commitmentPrice unit mismatch

Disposition: FIXED

extractPricingFromSKUs returns a per-hour SKU rate; calculateSavingsPercentage received it directly while comparing against onDemandPrice * hoursInTerm (term total), producing ~99.99% savings. Also HourlyRate = commitmentPrice / hoursInTerm divided an already-per-hour rate by hours, yielding a near-zero value. Fix: commitmentPriceTerm := commitmentPrice * hoursInTerm before all math; HourlyRate = commitmentPrice (the raw per-hour rate). Regression test TestGetRedisPricing_CommitmentPriceIsTermTotal asserts savings is a realistic fraction, not ~100%.

F4 - cloudstorage/client.go:499-527 - rec.Term hardcoded to "1yr"

Disposition: FIXED

Same as F2 for Cloud Storage. Regression test TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams.

F5 - cloudstorage/client.go:321-349 - commitmentPrice unit mismatch

Disposition: FIXED

Same as F3 for Cloud Storage. Regression test TestGetStoragePricing_CommitmentPriceIsTermTotal. Updated the existing TestCloudStorageClient_GetStoragePricing_WithCommitmentPrice assertion from CommitmentPrice == 0.02 (per-unit raw price) to CommitmentPrice ~= 0.02*8760 = 175.2 (term total).

Follow-up

CloudSQL getSQLPricing has the identical F3/F5 unit mismatch pattern. Not flagged by CR in this review. Filed as #1078 to keep this PR's scope bounded.


@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

@cristim — solid triage. All five dispositions are clear and well-reasoned. A few notes:

  • F1 aligns cleanly with the existing GCP monthly-only enforcement learning; the unconditional "monthly" clamp + WARN log is exactly the right pattern — it makes scheduler misconfiguration visible without silently accepting a wrong value.
  • F3/F5 are the most financially impactful: a per-hour rate compared against a term total produces ~99.99% savings figures that would have made CUD scoring nearly meaningless. The fix (commitmentPriceTerm = commitmentPrice * hoursInTerm, HourlyRate = commitmentPrice) is correct.
  • F2/F4 close a subtle API contract gap — RecommendationParams was plumbed in but .Term was silently ignored, making 3-year callers always produce 1-year commitments.
  • #1078 for CloudSQL getSQLPricing is the right call — keeping this PR bounded and deferring it avoids scope creep.

Recording the commitmentPrice unit semantics and term-propagation patterns for future reviews.

Triggering the incremental review of commit c6280c390 now.


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP services (memorystore, cloudstorage, and cloudsql), `extractPricingFromSKUs` (and equivalent billing SKU parsers) returns a **per-hour** SKU rate. All downstream savings/pricing math must multiply by `hoursInTerm` to get the term total before computing savings percentage or CommitmentPrice. `HourlyRate` on the recommendation should be set to the raw per-hour rate (not divided again by hoursInTerm). Failing to do this produces ~99.99% savings figures. Fixed in PR `#1047` commit c6280c390 for memorystore (F3) and cloudstorage (F5); CloudSQL `getSQLPricing` has the same bug tracked in `#1078`.

Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), `internal/commitmentopts/normalizePayment` is unexported and AWS-spelling-focused (parses "All Upfront"/"ALL_UPFRONT" → canonical AWS tokens). `internal/api/validation.go:purchasePaymentWhitelist` is the purchase-execute boundary. `pkg/common/reservation_name.go:normalizeReservationPayment` produces short-form name segments. These are all distinct concerns from the cross-provider `NormalizePaymentOption` helper in `internal/config/validation.go`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), `POST /api/plans/{id}/purchases` (`createPurchaseExecutionsTx`) must stamp `created_by_user_id` via `resolveCreatorUserID(session)` at creation time. Without this, every scheduled purchase created via the Plans-page "Create planned purchases" action ships a NULL creator, which the ownership gate treats as a legacy row and denies management to all non-`update-any` users — including the creator themselves. The admin API key path must set `CreatedByUserID` to nil (not the key identity). Regressions: `TestHandler_createPlannedPurchases_StampsCreator` and `TestHandler_createPlannedPurchases_AdminAPIKeyCreatorIsNil`. Fixed in commit 9ef1b4352.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), Azure service clients alias payment options in pairs: `{all-upfront, upfront}` and `{monthly, no-upfront}`. All 7 Azure service-client switches (compute, cache, cosmosdb, database, search, synapse, managedredis client.go) use these aliases. The canonical Azure payment option set in `ValidPaymentOptionsByProvider` is `{upfront, monthly}` (with synonyms `all-upfront → upfront`, `no-upfront → monthly`, `partial-upfront → upfront`).
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Two bugs mirroring the Memorystore/CloudStorage fixes from PR #1047:

1. Unit mismatch in getSQLPricing: commitmentPrice (per-hour SKU rate) was
   passed directly to calculateSQLSavingsPercentage which expects a term
   total, producing ~99.99% savings. Now multiplies by hoursInTerm first
   (commitmentPriceTerm = commitmentPrice * hoursInTerm). HourlyRate is now
   the raw per-hour rate instead of per-hour / hours (near-zero).

2. Term propagation in convertGCPRecommendation: rec.Term was hardcoded to
   "1yr", ignoring params.Term. A caller requesting "3yr" now gets "3yr".

PaymentOption was not affected: CloudSQL already defaults to "monthly" when
params.PaymentOption is empty, matching the Memorystore/CloudStorage pattern.

Regression tests: 2 new tests that fail pre-fix and pass after.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
providers/gcp/services/memorystore/client.go (1)

494-514: ⚡ Quick win

PaymentOption handling is inconsistent with computeengine client.

Per learnings, GCP CUDs are inherently monthly-billed, and the computeengine client forces paymentOption = "monthly" unconditionally, logging a WARN when a non-monthly value is supplied. Here, memorystore only defaults to "monthly" when params.PaymentOption is empty, allowing non-monthly values (e.g., "upfront") to pass through silently.

Consider aligning with the computeengine pattern to force monthly and log a warning for non-monthly input.

♻️ Suggested change to force monthly payment option
 func (c *MemorystoreClient) convertGCPRecommendation(ctx context.Context, gcpRec *recommenderpb.Recommendation, params common.RecommendationParams) *common.Recommendation {
 	paymentOption := params.PaymentOption
-	if paymentOption == "" {
-		paymentOption = "monthly"
+	if paymentOption != "monthly" && paymentOption != "" {
+		log.Printf("WARN: memorystore: coercing non-monthly payment option %q to monthly (GCP CUDs are monthly-only)", paymentOption)
 	}
+	paymentOption = "monthly"
 
 	term := params.Term
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/gcp/services/memorystore/client.go` around lines 494 - 514, In
convertGCPRecommendation, force the PaymentOption to "monthly" like
computeengine does: if params.PaymentOption is non-empty and not "monthly", emit
a warning log mentioning the supplied value (use the client logger available in
MemorystoreClient) and then set paymentOption = "monthly"; if empty, also set
paymentOption = "monthly". Update references to
params.PaymentOption/paymentOption in convertGCPRecommendation accordingly so no
non-monthly values are allowed through.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@providers/gcp/services/memorystore/client.go`:
- Around line 494-514: In convertGCPRecommendation, force the PaymentOption to
"monthly" like computeengine does: if params.PaymentOption is non-empty and not
"monthly", emit a warning log mentioning the supplied value (use the client
logger available in MemorystoreClient) and then set paymentOption = "monthly";
if empty, also set paymentOption = "monthly". Update references to
params.PaymentOption/paymentOption in convertGCPRecommendation accordingly so no
non-monthly values are allowed through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bbc1bb1c-09d8-44ff-9552-e68421ce0f8d

📥 Commits

Reviewing files that changed from the base of the PR and between 3491d78 and c6280c3.

📒 Files selected for processing (6)
  • providers/gcp/services/cloudstorage/client.go
  • providers/gcp/services/cloudstorage/client_test.go
  • providers/gcp/services/computeengine/client.go
  • providers/gcp/services/computeengine/client_test.go
  • providers/gcp/services/memorystore/client.go
  • providers/gcp/services/memorystore/client_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • providers/gcp/services/computeengine/client_test.go
  • providers/gcp/services/cloudstorage/client.go

@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

CloudSQL #1078 folded in (commit 20590c6)

Two bug classes fixed in providers/gcp/services/cloudsql/client.go, mirroring the Memorystore/CloudStorage fixes from the previous commit:

Bug 1 -- Unit mismatch (getSQLPricing): commitmentPrice (per-hour SKU rate) was passed directly to calculateSQLSavingsPercentage which expects a term total, producing ~99.99% savings. HourlyRate was also wrong (per-hour / hours = near-zero). Fixed: commitmentPriceTerm := commitmentPrice * hoursInTerm; HourlyRate is now the raw per-hour SKU rate.

Bug 2 -- Term propagation (convertGCPRecommendation): rec.Term was hardcoded to "1yr", ignoring params.Term. Fixed: term := params.Term; if term == "" { term = "1yr" }.

Not present: PaymentOption bug (F1 analog) -- CloudSQL already had if paymentOption == "" { paymentOption = "monthly" }, matching Memorystore/CloudStorage.

Regression tests: 2 new tests (TestGetSQLPricing_CommitmentPriceIsTermTotal, TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams) that fail on pre-fix code and pass after. Verified pre-fix failure manually.

Verification: go build ./..., go vet ./services/cloudsql/..., go test -race ./... (240 tests pass across 5 packages), repo-root go build ./... -- all green.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

@cristim — clean and symmetric close-out of #1078. Both bugs are exact mirrors of the F3/F5 and F2/F4 patterns already applied to Memorystore and Cloud Storage:

  • Unit mismatch: commitmentPriceTerm = commitmentPrice * hoursInTerm before all savings math; HourlyRate = raw per-hour SKU rate. Same root cause, same fix.
  • Term propagation: params.Term with "1yr" default, identical to the other two converters.
  • PaymentOption: already correct in CloudSQL — the asymmetry you noted is consistent with the earlier code, not a gap.

The two regression tests (TestGetSQLPricing_CommitmentPriceIsTermTotal, TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams) cover exactly the right failure modes. All four GCP service converters are now consistent.

Updating the learning for CloudSQL to reflect the fix is now merged into this PR.

Triggering the incremental review of commit 20590c6b4 now.


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T17:45:38.040Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


⛔ Learnings removed
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP services (memorystore, cloudstorage, and cloudsql), `extractPricingFromSKUs` (and equivalent billing SKU parsers) returns a **per-hour** SKU rate. All downstream savings/pricing math must multiply by `hoursInTerm` to get the term total before computing savings percentage or CommitmentPrice. `HourlyRate` on the recommendation should be set to the raw per-hour rate (not divided again by hoursInTerm). Failing to do this produces ~99.99% savings figures. Fixed in PR `#1047` commit c6280c390 for memorystore (F3) and cloudstorage (F5); CloudSQL `getSQLPricing` has the same bug tracked in `#1078`.

🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP services (memorystore, cloudstorage, and cloudsql), `extractPricingFromSKUs` (and equivalent billing SKU parsers) returns a **per-hour** SKU rate. All downstream savings/pricing math must multiply by `hoursInTerm` to get the term total before computing savings percentage or CommitmentPrice. `HourlyRate` on the recommendation should be set to the raw per-hour rate (not divided again by hoursInTerm). Failing to do this produces ~99.99% savings figures. Fixed in PR `#1047` commit c6280c390 for memorystore (F3) and cloudstorage (F5); CloudSQL `getSQLPricing` has the same bug tracked in `#1078`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), `internal/commitmentopts/normalizePayment` is unexported and AWS-spelling-focused (parses "All Upfront"/"ALL_UPFRONT" → canonical AWS tokens). `internal/api/validation.go:purchasePaymentWhitelist` is the purchase-execute boundary. `pkg/common/reservation_name.go:normalizeReservationPayment` produces short-form name segments. These are all distinct concerns from the cross-provider `NormalizePaymentOption` helper in `internal/config/validation.go`.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…um (de-hardcode)

Target 1 - MEMORY amount from Recommender payload:
- Add memoryMBFromOperationGroups (parallel to vcpuCountFromOperationGroups)
  to extract the MEMORY resource amount from the commitment operation group.
- Add extractMemoryMBFromRecommendation to store the payload memory in
  ComputeDetails.MemoryGB on the Recommendation (called from convertGCPRecommendation).
- Add memoryMBFromDetails to read the value back; returns an error (fail-loud)
  when absent -- no silent fallback to the 4096 MB/vCPU ratio.
- Remove the memMBPerVCPU const (no longer used).
- buildInsertRequest and GroupCommitments now use memoryMBFromDetails; both
  return/log an error for recs that lack the payload memory field.
- Regression tests: TestBuildInsertRequest_RefusesMissingMemory,
  TestGroupCommitments_SkipsRecsWithoutMemory, and strengthened
  TestConverterToInsert_CountNonZero_VCPUAmountSet (asserts 6144 MB from
  payload, not ratio-derived 16384 MB).

Target 2 - termPlan SDK enum constants:
- termPlan now returns (string, error) using
  computepb.Commitment_TWELVE_MONTH.String() and
  computepb.Commitment_THIRTY_SIX_MONTH.String().
- Accepts 1yr/1/12mo (TWELVE_MONTH) and 3yr/3/36mo (THIRTY_SIX_MONTH).
- Returns an error on unrecognised/empty term (no silent 12-month default).
- Error threaded through buildInsertRequest, GroupCommitments, and
  convertGCPRecommendation (which returns nil on bad term).
- Regression tests: TestTermPlan_UsesSdkEnumConstants,
  TestTermPlan_RejectsUnknownTerm, TestTermPlan_AcceptsAllDocumentedForms.

H-3 audit finding - convertGCPRecommendation propagates params.Term:
- No longer hardcodes Term: "1yr"; propagates params.Term (default "1yr").
- Validates the term via termPlan and returns nil (skips rec) when invalid.
- Regression tests: TestConvertGCPRecommendation_PropagatesParamsTerm,
  TestConvertGCPRecommendation_RejectsUnknownTerm.

All existing 247 GCP tests pass; 57 computeengine tests pass under -race.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Hardcode removal commit (8f9a787)

Three items addressed in the latest push:

TARGET 1 - MEMORY amount from Recommender payload (fail-loud, no ratio fallback)

  • Removed memMBPerVCPU = 4096 const entirely.
  • Added memoryMBFromOperationGroups (sibling of vcpuCountFromOperationGroups) to extract the MEMORY amount from the payload's commitment operation group. The GCP Recommender does carry this; extracting it ensures high-memory families (e.g. N2-highmem) get the correct MB rather than the GENERAL_PURPOSE approximation.
  • buildInsertRequest and GroupCommitments now call memoryMBFromDetails(rec) which returns an error when Details.MemoryGB is absent -- no silent fallback.
  • Regression: TestBuildInsertRequest_RefusesMissingMemory (fails on pre-fix silent-fallback code); TestConverterToInsert_CountNonZero_VCPUAmountSet strengthened to assert exact 6144 MB from payload (would be 16384 MB with ratio fallback).

TARGET 2 - termPlan uses Commitment_Plan SDK enum (fail-loud, no silent 12-month default)

  • termPlan returns (string, error) using computepb.Commitment_TWELVE_MONTH.String() / computepb.Commitment_THIRTY_SIX_MONTH.String().
  • Accepts 1yr/1/12mo and 3yr/3/36mo; errors on empty/unrecognised input.
  • Regression: TestTermPlan_UsesSdkEnumConstants, TestTermPlan_RejectsUnknownTerm, TestTermPlan_AcceptsAllDocumentedForms.

H-3 audit finding - params.Term propagation in convertGCPRecommendation

  • No longer hardcodes Term: "1yr"; propagates params.Term (defaults to "1yr", validates via termPlan, returns nil on unknown).
  • Regression: TestConvertGCPRecommendation_PropagatesParamsTerm, TestConvertGCPRecommendation_RejectsUnknownTerm.

Verification: go build ./... clean, go test -race ./providers/gcp/... 247 passed.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@cristim — solid hardening commit. All three items are well-structured:

  • TARGET 1: Extracting MEMORY MB from the Recommender payload is strictly more correct than the 4096 MB/vCPU ratio — the ratio was only accurate for GENERAL_PURPOSE and would silently mis-size high-memory families. The fail-loud memoryMBFromDetails pattern is consistent with the Count<=0 guard.
  • TARGET 2: Using computepb.Commitment_TWELVE_MONTH.String() / computepb.Commitment_THIRTY_SIX_MONTH.String() as the canonical plan strings eliminates a class of SDK-vs-hardstring drift bugs. The (string, error) signature + rejection of empty/unknown input is the right fail-loud contract.
  • H-3: The term propagation with termPlan validation is stricter than what memorystore/cloudstorage/cloudsql do (those default and continue; this returns nil on unknown) — appropriate since computeengine is on the purchase path.

Recording the new behaviors for future reviews.

Triggering the incremental review of commit 8f9a787 now.

[review]

(˳˘ ɜ˘)˳ ♡


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, the `memMBPerVCPU = 4096` const was removed in PR `#1047` commit 8f9a787. MEMORY MB is now extracted directly from the Recommender payload via `memoryMBFromOperationGroups` (sibling of `vcpuCountFromOperationGroups`). `buildInsertRequest` and `GroupCommitments` call `memoryMBFromDetails(rec)` which returns an error (no silent fallback) when `Details.MemoryGB` is absent. This ensures high-memory families (e.g. N2-highmem) get the correct MB from the payload rather than the GENERAL_PURPOSE approximation. Regression test: `TestBuildInsertRequest_RefusesMissingMemory`.

Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `termPlan` returns `(string, error)` using `computepb.Commitment_TWELVE_MONTH.String()` and `computepb.Commitment_THIRTY_SIX_MONTH.String()` as the canonical plan strings (fixed in PR `#1047` commit 8f9a787). It accepts 1yr/1/12mo and 3yr/3/36mo; it errors on empty or unrecognized input (no silent 12-month default). Regression tests: `TestTermPlan_UsesSdkEnumConstants`, `TestTermPlan_RejectsUnknownTerm`, `TestTermPlan_AcceptsAllDocumentedForms`.

Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` no longer hardcodes `Term = "1yr"` (fixed in PR `#1047` commit 8f9a787, H-3 audit finding). It propagates `params.Term` with a `"1yr"` default, validates via `termPlan`, and returns nil on unknown term. This is stricter than memorystore/cloudstorage/cloudsql (which default and continue): computeengine returns nil on unknown term because it is on the purchase path. Regression tests: `TestConvertGCPRecommendation_PropagatesParamsTerm`, `TestConvertGCPRecommendation_RejectsUnknownTerm`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:45:38.040Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), Azure service clients alias payment options in pairs: `{all-upfront, upfront}` and `{monthly, no-upfront}`. All 7 Azure service-client switches (compute, cache, cosmosdb, database, search, synapse, managedredis client.go) use these aliases. The canonical Azure payment option set in `ValidPaymentOptionsByProvider` is `{upfront, monthly}` (with synonyms `all-upfront → upfront`, `no-upfront → monthly`, `partial-upfront → upfront`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), `internal/commitmentopts/normalizePayment` is unexported and AWS-spelling-focused (parses "All Upfront"/"ALL_UPFRONT" → canonical AWS tokens). `internal/api/validation.go:purchasePaymentWhitelist` is the purchase-execute boundary. `pkg/common/reservation_name.go:normalizeReservationPayment` produces short-form name segments. These are all distinct concerns from the cross-provider `NormalizePaymentOption` helper in `internal/config/validation.go`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-04T14:28:19.903Z
Learning: In providers/azure/recommendations.go (Go, Azure provider), the service constructors (compute.NewClient, cache.NewClient, etc.) build concrete Azure SDK types with no interface injection seam. Introducing a concurrency timing test that asserts elapsed ≈ max-single-service latency would require adding package-level function variables — a refactor that is out of scope for small performance PRs.

H-1 (rec state never filtered): GetRecommendations in all four GCP
service clients (computeengine, cloudsql, memorystore, cloudstorage)
now skip recommendations whose StateInfo.State is not ACTIVE. The GCP
Recommender returns recommendations in ACTIVE/CLAIMED/SUCCEEDED/FAILED/
DISMISSED states; acting on a non-ACTIVE rec is a cross-run double-
purchase vector and inflates actionable rec counts. Uses typed enum
constant recommenderpb.RecommendationStateInfo_ACTIVE (not a string).
The proto GetStateInfo() getter is nil-safe so nil-StateInfo recs
(STATE_UNSPECIFIED) are also filtered.

H-2 (cache/storage advertised but produce zero recs): collectRegion in
recommendations.go now fans out to memorystore and cloudstorage in
addition to compute and cloudsql, guarded by shouldIncludeService.
Both services have complete GetRecommendations implementations; their
PurchaseCommitment paths are advisory-only no-ops (documented) which is
orthogonal to surfacing recommendations. regionResult gains cache and
storage slices; GetRecommendations merge appends them per region.
Decision log: docs/code-review/open-questions/fix-1047-h1h2.md.

Regression tests:
- TestGetRecommendations_FiltersNonActiveStates: five-rec mock (one
  ACTIVE, four non-ACTIVE); asserts only ACTIVE is returned. Fails
  pre-fix.
- TestGetRecommendations_ActiveRecIncluded: positive case.
- TestRegionResult_HasCacheAndStorageFields: compile-time + runtime
  check that regionResult carries all four service slices (H-2).
- TestShouldIncludeService_Cache_Storage: routing logic for cache/
  storage service types.
- Existing WithMock tests in all four services updated to set
  StateInfo=ACTIVE (they failed post-fix; pre-fix they passed against
  STATE_UNSPECIFIED=0 which was not filtered).

Follow-up: remaining Medium/Low/Nit findings from 10-gcp-and-pkg.md
tracked in issue #1083.

257 GCP tests pass under -race. go build/vet ./... clean.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

H-1 and H-2 fixes (commit 95a390f)

Two additional High findings from the GCP broad audit addressed:

H-1 -- Rec state filter (anti double-purchase)

  • All four GCP service clients now skip non-ACTIVE recommendations using recommenderpb.RecommendationStateInfo_ACTIVE typed enum (CLAIMED/SUCCEEDED/FAILED/DISMISSED are filtered). State nil-safe via proto getter.
  • Regression tests: TestGetRecommendations_FiltersNonActiveStates (5-rec mock, only ACTIVE survives; fails pre-fix) and TestGetRecommendations_ActiveRecIncluded.
  • Existing mock tests in all four services had StateInfo added to pass the new filter.

H-2 -- Cache/storage wired into fan-out

  • Decision: wire (not trim). Both memorystore and cloudstorage have complete GetRecommendations implementations; their no-op PurchaseCommitment paths are orthogonal to surfacing advisory recommendations.
  • collectRegion now fans out to all four services concurrently. regionResult gains cache/storage slices; merge updated accordingly.
  • Regression tests: TestRegionResult_HasCacheAndStorageFields and TestShouldIncludeService_Cache_Storage.

257 GCP tests pass under -race. go build/vet ./... clean.

Remaining Medium/Low/Nit findings tracked in issue LeanerCloud/cloud-commitments-go#6.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

@cristim — both H-1 and H-2 are well-reasoned:

  • H-1: Using recommenderpb.RecommendationStateInfo_ACTIVE as a typed enum filter is the right approach — it eliminates string-drift risk and ensures CLAIMED/SUCCEEDED/FAILED/DISMISSED are all correctly excluded. The nil-safe proto getter handling is important since synthetic/test recs often lack a StateInfo. Updating all four service clients to pass the new filter in their existing mock tests is the correct companion change.
  • H-2: Wire is the right call. GetRecommendations for memorystore and cloudstorage are complete and correct implementations; the no-op PurchaseCommitment is a separate concern. Surfacing advisory recommendations to users is independently valuable. The concurrent fan-out + merged result pattern is consistent with the compute engine path.

The #1083 deferral for M/L/N findings is a clean boundary.

Recording both patterns for future reviews.


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T18:28:28.112Z
Learning: In LeanerCloud/CUDly GCP service clients (computeengine, cloudsql, memorystore, cloudstorage), `GetRecommendations` filters out non-ACTIVE recommendations using the typed `recommenderpb.RecommendationStateInfo_ACTIVE` enum (CLAIMED/SUCCEEDED/FAILED/DISMISSED are skipped). State is accessed nil-safely via the proto getter. Fixed in PR `#1047` commit 95a390f (H-1 finding). Regression tests: `TestGetRecommendations_FiltersNonActiveStates` (5-rec mock, only ACTIVE survives) and `TestGetRecommendations_ActiveRecIncluded`. Existing mock tests in all four services had StateInfo added to satisfy the new filter.

Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1047

Timestamp: 2026-06-07T18:28:28.112Z
Learning: In LeanerCloud/CUDly `providers/gcp/recommendations.go`, `collectRegion` fans out to all four GCP service clients (computeengine, cloudsql, memorystore, cloudstorage) concurrently (fixed in PR `#1047` commit 95a390f, H-2 finding). `regionResult` gains `cache` and `storage` slices; merge is updated accordingly. The decision was to wire (not trim) memorystore and cloudstorage because both have complete `GetRecommendations` implementations and their no-op `PurchaseCommitment` paths are orthogonal to surfacing advisory recommendations. Regression tests: `TestRegionResult_HasCacheAndStorageFields` and `TestShouldIncludeService_Cache_Storage`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:45:38.040Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` no longer hardcodes `Term = "1yr"` (fixed in PR `#1047` commit 8f9a787, H-3 audit finding). It propagates `params.Term` with a `"1yr"` default, validates via `termPlan`, and returns nil on unknown term. This is stricter than memorystore/cloudstorage/cloudsql (which default and continue): computeengine returns nil on unknown term because it is on the purchase path. Regression tests: `TestConvertGCPRecommendation_PropagatesParamsTerm`, `TestConvertGCPRecommendation_RejectsUnknownTerm`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, the `memMBPerVCPU = 4096` const was removed in PR `#1047` commit 8f9a787. MEMORY MB is now extracted directly from the Recommender payload via `memoryMBFromOperationGroups` (sibling of `vcpuCountFromOperationGroups`). `buildInsertRequest` and `GroupCommitments` call `memoryMBFromDetails(rec)` which returns an error (no silent fallback) when `Details.MemoryGB` is absent. This ensures high-memory families (e.g. N2-highmem) get the correct MB from the payload rather than the GENERAL_PURPOSE approximation. Regression test: `TestBuildInsertRequest_RefusesMissingMemory`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `termPlan` returns `(string, error)` using `computepb.Commitment_TWELVE_MONTH.String()` and `computepb.Commitment_THIRTY_SIX_MONTH.String()` as the canonical plan strings (fixed in PR `#1047` commit 8f9a787). It accepts 1yr/1/12mo and 3yr/3/36mo; it errors on empty or unrecognized input (no silent 12-month default). Regression tests: `TestTermPlan_UsesSdkEnumConstants`, `TestTermPlan_RejectsUnknownTerm`, `TestTermPlan_AcceptsAllDocumentedForms`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), Azure service clients alias payment options in pairs: `{all-upfront, upfront}` and `{monthly, no-upfront}`. All 7 Azure service-client switches (compute, cache, cosmosdb, database, search, synapse, managedredis client.go) use these aliases. The canonical Azure payment option set in `ValidPaymentOptionsByProvider` is `{upfront, monthly}` (with synonyms `all-upfront → upfront`, `no-upfront → monthly`, `partial-upfront → upfront`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-04T14:28:19.903Z
Learning: In providers/azure/recommendations.go (Go, Azure provider), the service constructors (compute.NewClient, cache.NewClient, etc.) build concrete Azure SDK types with no interface injection seam. Introducing a concurrency timing test that asserts elapsed ≈ max-single-service latency would require adding package-level function variables — a refactor that is out of scope for small performance PRs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-05T12:28:20.780Z
Learning: In LeanerCloud/CUDly, `internal/mocks/stores.go` is the canonical `MockConfigStore` (testify/mock embedding, Fn-override fields, isExpected guards). Per-package files in `internal/api`, `internal/purchase`, and `internal/scheduler` were reduced to a single type alias line: `type MockConfigStore = mocks.MockConfigStore`. Four intentionally-local mocks remain: `mockConfigStore` (lowercase, hook-field only) in `internal/analytics/collector_test.go`; `mockConfigStoreForHealth` in `internal/server/test_helpers_test.go`; and `mockConfigStoreForExchange` / per-scenario variants in `internal/server/handler_ri_exchange_test.go` and `handler_coverage_test.go`. A compile-time check `var _ config.StoreInterface = (*MockConfigStore)(nil)` lives in the shared mock.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…commendation

Extract per-service fetch helpers (collectComputeRecs, collectSQLRecs,
collectCacheRecs, collectStorageRecs) from collectRegion so the
Acquire/Release and client-construction branches are in named functions
rather than anonymous closures, bringing collectRegion from 17 to 9.

Extract enrichRecWithPricing from convertGCPRecommendation to move the
pricing lookup and BreakEvenMonths calculation into a dedicated method,
bringing convertGCPRecommendation from 12 to 5.

Apply gofmt to client.go (trailing blank line) and client_test.go
(comment alignment). No logic changes.

Fixes the failing "Check cyclomatic complexity" and "Run gofmt"
pre-commit hooks in CI for PR #1047.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working effort/m Days impact/many Affects most users 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.

1 participant