Skip to content

fix(providers): correct Azure resourceType filters + fail-loud SP money parsing + nil covered-cost (adversarial-review)#1455

Open
cristim wants to merge 1 commit into
mainfrom
fix/provider-rec-correctness
Open

fix(providers): correct Azure resourceType filters + fail-loud SP money parsing + nil covered-cost (adversarial-review)#1455
cristim wants to merge 1 commit into
mainfrom
fix/provider-rec-correctness

Conversation

@cristim

@cristim cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

Four provider recommendation-correctness defects found by adversarial review of origin/main. All four are fixed with regression tests that fail on pre-fix code and pass on post-fix code.

H1 - Azure Search GetRecommendations returns empty (HIGH)

"AzureSearch" is not a valid resourceType in the Azure Consumption ReservationRecommendations API (valid list includes VirtualMachines, SQLDatabases, CosmosDB, SqlDataWarehouse, etc.). The pre-fix code queried with only a scope filter (properties/scope eq 'Shared'), causing Azure to default to returning VirtualMachine recommendations that were then mislabeled as Search recommendations.

Fix: GetRecommendations now returns empty immediately. Regression test TestSearchClient_GetRecommendations_AlwaysEmpty injects a pager containing VM recs and asserts empty is returned.

H2 - Three wrong Consumption API resourceType filter strings (HIGH)

The pre-fix code used hand-written string literals that are not valid resourceType enum members, causing the Consumption API to return no recommendations for those services:

Service Pre-fix (invalid) Post-fix (canonical)
Synapse "SQLDatabaseDTU" reservationResourceTypeSynapse = "SqlDataWarehouse"
SQL DB "SqlDatabase" reservationResourceTypeSQLDB = "SQLDatabases"
CosmosDB "CosmosDb" reservationResourceTypeCosmosDB = "CosmosDB"

Fix: Package-level typed constants per feedback_sdk_enum_string_literals. Regression tests TestReservationResourceType*_IsCanonical pin the exact string values.

M1 - nil RecurringMonthlyCost when CommitmentCost is absent (MEDIUM)

ExpandPaymentVariants unconditionally set RecurringMonthlyCost = &0.0 even when CommitmentCost is 0 (meaning data was absent), fabricating a non-nil pointer the frontend renders as "$0" instead of "-".

Fix: Guard added - RecurringMonthlyCost is nil for both variants when CommitmentCost == 0. Per feedback_nullable_not_zero: absent numbers must be nil, not 0. The existing test TestExpandPaymentVariants_ZeroCommitmentCost is updated to assert nil (it previously asserted the wrong behavior).

M2 - Fail-loud on unparseable SP money fields (MEDIUM)

parseOptionalFloat coerced present-but-unparseable money fields (HourlyCommitmentToPurchase, EstimatedMonthlySavingsAmount, UpfrontCost) to 0 with a warning, fabricating $0 figures that would reach the scheduler and frontend. The RI path (parseAWSCostDetails) already errored on the same class.

Fix: Two-tier treatment: parseOptionalFloat returns (float64, error) for money fields; parseOptionalFloatOrWarn (new) logs and returns 0 for non-money fields (percentages, utilization). Bad money fields cause the recommendation to be dropped (logged as WARNING). spPlanTypeDisplayString helper extracted to keep parseSavingsPlanDetail below the gocyclo-10 pre-commit threshold.

Test plan

  • go build ./... passes in providers/aws and providers/azure
  • go vet ./... passes in both modules
  • gocyclo -over 10 recommendations/ clean in providers/aws
  • 409 tests pass in providers/aws/recommendations
  • 43 tests pass in providers/azure/internal/recommendations
  • 199 tests pass across search, synapse, database, cosmosdb
  • Pre-commit hooks passed (gofmt, go mod tidy, go vet, gocyclo, gosec, trivy)

Summary by CodeRabbit

  • Bug Fixes
    • Improved AWS Savings Plans recommendation parsing by rejecting invalid monetary values while safely handling non-monetary parsing issues.
    • Corrected Azure reservation recommendation filters for Cosmos DB, SQL Database, and Synapse, restoring accurate results.
    • Prevented Azure payment details from displaying $0 when recurring cost data is unavailable.
    • Azure Search recommendations now correctly return no results when the service does not provide a valid resource-specific recommendation type.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/l Weeks type/bug Defect labels Jul 17, 2026
@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

AWS Savings Plans parsing now rejects invalid money fields while warning and defaulting invalid non-money fields. Azure recommendation filters use canonical resource types, zero commitment costs remain unset, and Search returns empty recommendations.

Changes

AWS Savings Plans parsing

Layer / File(s) Summary
Parsing contracts and error handling
providers/aws/recommendations/parser_sp.go
Optional numeric parsing now distinguishes propagated money errors from warning-only non-money errors, and Savings Plan details return (recommendation, error).
Recommendation handling and tests
providers/aws/recommendations/parser_sp.go, providers/aws/recommendations/*test.go
Invalid recommendation details are skipped, while tests cover successful parsing, non-money fallback, and invalid money fields.

Azure recommendation corrections

Layer / File(s) Summary
Canonical reservation filters
providers/azure/services/{cosmosdb,database,synapse}/*
Cosmos DB, SQL Database, and Synapse queries use canonical Azure resource type values with regression tests.
Recurring payment variant availability
providers/azure/internal/recommendations/*
RecurringMonthlyCost remains nil when commitment cost is zero, and tests verify both variants.
Search recommendation suppression
providers/azure/services/search/*
Search recommendation retrieval returns an empty slice without querying or converting Azure recommendations.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: Azure resourceType filter fixes, Savings Plan money parsing behavior, and nil covered-cost handling.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/provider-rec-correctness

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

@coderabbitai

coderabbitai Bot commented Jul 17, 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.

…ey parsing + nil covered-cost (adversarial-review)

H1: Azure Search GetRecommendations now returns empty immediately; "AzureSearch"
is not a valid Consumption API resourceType. Pre-fix queried with scope-only
filter, causing Azure to return VirtualMachine recs mislabeled as Search recs.

H2: Replace three hand-written Azure Consumption API resourceType strings with
package-level typed constants:
- Synapse: "SQLDatabaseDTU" -> reservationResourceTypeSynapse = "SqlDataWarehouse"
- SQL DB:  "SqlDatabase"    -> reservationResourceTypeSQLDB   = "SQLDatabases"
- CosmosDB:"CosmosDb"       -> reservationResourceTypeCosmosDB = "CosmosDB"
All three pre-fix strings are invalid enum members; the API returned nothing.

M1: ExpandPaymentVariants sets RecurringMonthlyCost to nil (not &0.0) when
CommitmentCost is 0, distinguishing "data absent" from "known-zero cost"
so the frontend renders "-" instead of fabricating "$0".

M2: parseOptionalFloat now returns (float64, error) for money fields
(HourlyCommitmentToPurchase, EstimatedMonthlySavingsAmount, UpfrontCost).
Present-but-unparseable money fields propagate as errors and drop the
recommendation; non-money fields (utilization percentages) still warn+0.
Mirrors the RI path (parseAWSCostDetails) which already errored on this class.
Extract spPlanTypeDisplayString helper to keep parseSavingsPlanDetail below
the gocyclo-10 pre-commit threshold.

Regression tests added for all four defects; all fail on pre-fix code and
pass on post-fix code.
@cristim
cristim force-pushed the fix/provider-rec-correctness branch from 5898bfb to ce62470 Compare July 17, 2026 18:26

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

Actionable comments posted: 3

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

Inline comments:
In `@providers/aws/recommendations/parser_sp.go`:
- Line 308: Update the recommendation parsing flow around the OnDemandCost
assignment to use the error-propagating monetary parser for
CurrentAverageHourlyOnDemandSpend, so malformed values reject or drop the
recommendation instead of becoming zero. Preserve the monthly conversion and add
this field to the malformed-money regression table.
- Around line 199-203: Update parseOptionalFloat to reject non-finite parsed
values by checking math.IsNaN(val) and math.IsInf(val, 0) after
strconv.ParseFloat succeeds. Return the existing parse error path for NaN and
infinities, while preserving the warn-and-zero behavior for non-money fields.

In `@providers/azure/services/search/client_test.go`:
- Around line 448-489: Strengthen
TestSearchClient_GetRecommendations_AlwaysEmpty to verify immediate return by
asserting pagerWithVMRecs.index remains unchanged after GetRecommendations.
Alternatively, configure MockRecommendationsPager to fail if pagination is
invoked, while preserving the existing empty-result and no-error assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4bff841-9642-4c03-80be-6d602b53ecbe

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9f1b8 and ce62470.

📒 Files selected for processing (13)
  • providers/aws/recommendations/parser_sp.go
  • providers/aws/recommendations/parser_sp_additional_test.go
  • providers/aws/recommendations/parser_sp_test.go
  • providers/azure/internal/recommendations/converter.go
  • providers/azure/internal/recommendations/converter_test.go
  • providers/azure/services/cosmosdb/client.go
  • providers/azure/services/cosmosdb/client_test.go
  • providers/azure/services/database/client.go
  • providers/azure/services/database/client_test.go
  • providers/azure/services/search/client.go
  • providers/azure/services/search/client_test.go
  • providers/azure/services/synapse/client.go
  • providers/azure/services/synapse/client_test.go

Comment on lines 199 to +203
val, err := strconv.ParseFloat(*s, 64)
if err != nil {
log.Printf("WARNING: failed to parse %s: %v", field, err)
return 0, fmt.Errorf("failed to parse %s %q: %w", field, *s, err)
}
return val, nil

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the Go strconv.ParseFloat documentation, do "NaN", "+Inf", and "-Inf" return a nil error?

💡 Result:

Yes, according to the Go strconv.ParseFloat documentation, the strings "NaN", "Inf", "Infinity", "+Inf", "+Infinity", "-Inf", and "-Infinity" are recognized as special floating-point values and return a nil error [1][2][3]. The Go documentation explicitly states that ParseFloat recognizes these strings as their respective special floating-point values [1][2][3]. When these valid strings are provided, the function successfully returns the corresponding float64 value and a nil error [1][2]. This is distinct from cases where a string is syntactically well-formed but represents a value exceeding the representable range of the target bitSize, which would return a non-nil error (ErrRange) along with the resulting ±Inf value [1][4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant parser and nearby helpers.
file="providers/aws/recommendations/parser_sp.go"
wc -l "$file"
sed -n '150,260p' "$file"

# Find the helper and its call sites.
rg -n "parseOptionalFloatOrWarn|ParseFloat|IsNaN|IsInf|money" providers/aws/recommendations -n

Repository: LeanerCloud/CUDly

Length of output: 9018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '260,340p' providers/aws/recommendations/parser_sp.go
sed -n '492,520p' providers/aws/recommendations/sp_coverage.go
sed -n '1,260p' providers/aws/recommendations/parser_sp_test.go | sed -n '300,380p'

Repository: LeanerCloud/CUDly

Length of output: 5653


Reject non-finite numeric values in parseOptionalFloat. strconv.ParseFloat accepts NaN and +/-Inf here, so malformed SP money fields can still flow through as valid recommendations. Reject math.IsNaN(val) and math.IsInf(val, 0) so only non-money fields keep the warn-and-zero path.

🤖 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/aws/recommendations/parser_sp.go` around lines 199 - 203, Update
parseOptionalFloat to reject non-finite parsed values by checking
math.IsNaN(val) and math.IsInf(val, 0) after strconv.ParseFloat succeeds. Return
the existing parse error path for NaN and infinities, while preserving the
warn-and-zero behavior for non-money fields.

// where monthly_cost reflects only the no-upfront recurring charge, not
// the full on-demand baseline). See #303.
onDemandCost := parseOptionalFloat("CurrentAverageHourlyOnDemandSpend", detail.CurrentAverageHourlyOnDemandSpend) * hoursPerMonth
onDemandCost := parseOptionalFloatOrWarn("CurrentAverageHourlyOnDemandSpend", detail.CurrentAverageHourlyOnDemandSpend) * hoursPerMonth

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not fabricate a zero OnDemandCost.

CurrentAverageHourlyOnDemandSpend is a monetary rate used to populate OnDemandCost. Warning and returning zero forwards a corrupt baseline instead of dropping the recommendation, contradicting the money-field handling objective. Use the error-propagating parser and add this field to the malformed-money regression table.

Proposed fix
-	onDemandCost := parseOptionalFloatOrWarn("CurrentAverageHourlyOnDemandSpend", detail.CurrentAverageHourlyOnDemandSpend) * hoursPerMonth
+	onDemandHourlySpend, err := parseOptionalFloat(
+		"CurrentAverageHourlyOnDemandSpend",
+		detail.CurrentAverageHourlyOnDemandSpend,
+	)
+	if err != nil {
+		return nil, err
+	}
+	onDemandCost := onDemandHourlySpend * hoursPerMonth
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onDemandCost := parseOptionalFloatOrWarn("CurrentAverageHourlyOnDemandSpend", detail.CurrentAverageHourlyOnDemandSpend) * hoursPerMonth
onDemandHourlySpend, err := parseOptionalFloat(
"CurrentAverageHourlyOnDemandSpend",
detail.CurrentAverageHourlyOnDemandSpend,
)
if err != nil {
return nil, err
}
onDemandCost := onDemandHourlySpend * hoursPerMonth
🤖 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/aws/recommendations/parser_sp.go` at line 308, Update the
recommendation parsing flow around the OnDemandCost assignment to use the
error-propagating monetary parser for CurrentAverageHourlyOnDemandSpend, so
malformed values reject or drop the recommendation instead of becoming zero.
Preserve the monthly conversion and add this field to the malformed-money
regression table.

Comment on lines +448 to +489
// TestSearchClient_GetRecommendations_AlwaysEmpty is the H1 regression test.
//
// "AzureSearch" is not a valid resourceType in the Consumption
// ReservationRecommendations API (valid list: VirtualMachines, SQLDatabases,
// CosmosDB, SqlDataWarehouse, etc.). The pre-fix code queried with only a
// scope filter, causing Azure to return VirtualMachine recommendations that
// were then mislabeled as Search recommendations.
//
// Post-fix, GetRecommendations returns an empty slice immediately regardless
// of what the pager contains. This test injects a non-empty pager (mimicking
// the VM recs Azure would return) and asserts the result is empty.
func TestSearchClient_GetRecommendations_AlwaysEmpty(t *testing.T) {
ctx := context.Background()
client := NewClient(nil, "test-subscription", "eastus")

// Simulate what the Azure Consumption API returns when queried without a
// resourceType filter: a VirtualMachines recommendation that must NOT be
// forwarded as a Search recommendation.
vmRec := mocks.BuildLegacyReservationRecommendation(
mocks.WithRegion("eastus"),
mocks.WithTerm("P1Y"),
mocks.WithQuantity(1),
mocks.WithNormalizedSize("Standard_D2s_v3"),
mocks.WithCosts(100, 70, 30),
)
pagerWithVMRecs := &MockRecommendationsPager{
pages: []armconsumption.ReservationRecommendationsClientListResponse{
{
ReservationRecommendationsListResult: armconsumption.ReservationRecommendationsListResult{
Value: []armconsumption.ReservationRecommendationClassification{vmRec},
},
},
},
}
client.SetRecommendationsPager(pagerWithVMRecs)

recs, err := client.GetRecommendations(ctx, common.RecommendationParams{})
require.NoError(t, err)
assert.Empty(t, recs,
"Azure Search has no Consumption API reservation stream; GetRecommendations "+
"must always return empty regardless of what the injected pager contains")
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Verify that GetRecommendations does not query the pager.

The test proves that VM recommendations are not returned, but it does not enforce the “return immediately” behavior documented at Line [456] through Line [458]. A future implementation could query the pager and merely filter the result while still passing this test. Assert that pagerWithVMRecs.index remains unchanged, or make the mock fail if pagination is invoked.

Suggested assertion
  assert.Empty(t, recs,
    "Azure Search has no Consumption API reservation stream; GetRecommendations "+
      "must always return empty regardless of what the injected pager contains")
+ assert.Equal(t, 0, pagerWithVMRecs.index,
+   "GetRecommendations must not query the recommendations pager")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TestSearchClient_GetRecommendations_AlwaysEmpty is the H1 regression test.
//
// "AzureSearch" is not a valid resourceType in the Consumption
// ReservationRecommendations API (valid list: VirtualMachines, SQLDatabases,
// CosmosDB, SqlDataWarehouse, etc.). The pre-fix code queried with only a
// scope filter, causing Azure to return VirtualMachine recommendations that
// were then mislabeled as Search recommendations.
//
// Post-fix, GetRecommendations returns an empty slice immediately regardless
// of what the pager contains. This test injects a non-empty pager (mimicking
// the VM recs Azure would return) and asserts the result is empty.
func TestSearchClient_GetRecommendations_AlwaysEmpty(t *testing.T) {
ctx := context.Background()
client := NewClient(nil, "test-subscription", "eastus")
// Simulate what the Azure Consumption API returns when queried without a
// resourceType filter: a VirtualMachines recommendation that must NOT be
// forwarded as a Search recommendation.
vmRec := mocks.BuildLegacyReservationRecommendation(
mocks.WithRegion("eastus"),
mocks.WithTerm("P1Y"),
mocks.WithQuantity(1),
mocks.WithNormalizedSize("Standard_D2s_v3"),
mocks.WithCosts(100, 70, 30),
)
pagerWithVMRecs := &MockRecommendationsPager{
pages: []armconsumption.ReservationRecommendationsClientListResponse{
{
ReservationRecommendationsListResult: armconsumption.ReservationRecommendationsListResult{
Value: []armconsumption.ReservationRecommendationClassification{vmRec},
},
},
},
}
client.SetRecommendationsPager(pagerWithVMRecs)
recs, err := client.GetRecommendations(ctx, common.RecommendationParams{})
require.NoError(t, err)
assert.Empty(t, recs,
"Azure Search has no Consumption API reservation stream; GetRecommendations "+
"must always return empty regardless of what the injected pager contains")
}
// TestSearchClient_GetRecommendations_AlwaysEmpty is the H1 regression test.
//
// "AzureSearch" is not a valid resourceType in the Consumption
// ReservationRecommendations API (valid list: VirtualMachines, SQLDatabases,
// CosmosDB, SqlDataWarehouse, etc.). The pre-fix code queried with only a
// scope filter, causing Azure to return VirtualMachine recommendations that
// were then mislabeled as Search recommendations.
//
// Post-fix, GetRecommendations returns an empty slice immediately regardless
// of what the pager contains. This test injects a non-empty pager (mimicking
// the VM recs Azure would return) and asserts the result is empty.
func TestSearchClient_GetRecommendations_AlwaysEmpty(t *testing.T) {
ctx := context.Background()
client := NewClient(nil, "test-subscription", "eastus")
// Simulate what the Azure Consumption API returns when queried without a
// resourceType filter: a VirtualMachines recommendation that must NOT be
// forwarded as a Search recommendation.
vmRec := mocks.BuildLegacyReservationRecommendation(
mocks.WithRegion("eastus"),
mocks.WithTerm("P1Y"),
mocks.WithQuantity(1),
mocks.WithNormalizedSize("Standard_D2s_v3"),
mocks.WithCosts(100, 70, 30),
)
pagerWithVMRecs := &MockRecommendationsPager{
pages: []armconsumption.ReservationRecommendationsClientListResponse{
{
ReservationRecommendationsListResult: armconsumption.ReservationRecommendationsListResult{
Value: []armconsumption.ReservationRecommendationClassification{vmRec},
},
},
},
}
client.SetRecommendationsPager(pagerWithVMRecs)
recs, err := client.GetRecommendations(ctx, common.RecommendationParams{})
require.NoError(t, err)
assert.Empty(t, recs,
"Azure Search has no Consumption API reservation stream; GetRecommendations "+
"must always return empty regardless of what the injected pager contains")
assert.Equal(t, 0, pagerWithVMRecs.index,
"GetRecommendations must not query the recommendations pager")
}
🤖 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/azure/services/search/client_test.go` around lines 448 - 489,
Strengthen TestSearchClient_GetRecommendations_AlwaysEmpty to verify immediate
return by asserting pagerWithVMRecs.index remains unchanged after
GetRecommendations. Alternatively, configure MockRecommendationsPager to fail if
pagination is invoked, while preserving the existing empty-result and no-error
assertions.

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

Labels

effort/l Weeks 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