Skip to content

fix(cli): keep CSV TOTAL normalized units in line with the per-row NU cells - #2250

Merged
cristim merged 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-2005
Oct 18, 2026
Merged

cristim merged 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-2005

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Root cause

buildTotalRow (cmd/multi_service_csv.go) added Count × RDSInstanceNUFromType(ResourceType) for every row, while formatNormalizedUnitsOrBlank only renders NU for rds / relational-db rows. RDSInstanceNUFromType accepts any type with three or more dot-separated parts, so an ElastiCache type such as cache.r7g.large resolves the large NU (4). In a mixed --all-services run, those rows show a blank NU cell but still feed the TOTAL. The TOTAL NU then comes out larger than the sum of the column above it.

Fix

I added rowNormalizedUnits(rec) float64 as the one place that computes a row's NU: Count × NU(size) for RDS rows and 0 for every other service. formatNormalizedUnitsOrBlank renders that value (blank when it is 0, same as before), and buildTotalRow sums it. The TOTAL is now built from the same values the cells show. Rendering of each row's NU cell is unchanged.

Closes #2005

Verification

All of this is unit/fixture evidence: CSV rendered in memory from hand-built PurchaseResult fixtures. No cloud APIs were called. I ran tests with AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null because the default AWS profile on this machine is invalid. These tests don't touch AWS config.

New tests in cmd/multi_service_csv_test.go:

  • TestBuildTotalRow_NUSkipsRowsWithBlankNUCell: mixes RDS (db.r6g.large ×5), relational-db (db.r6g.xlarge ×2), ElastiCache (cache.r7g.large ×3), OpenSearch and EC2 rows. It checks that the TOTAL NU equals the parsed sum of the visible NU cells (36).
  • TestBuildTotalRow_NUBlankWhenNoRowHasNU: with only an ElastiCache row, the TOTAL NU cell stays blank and Count still sums.

Fail before (on origin/main 1306c4d, test file only):

$ go test ./cmd/ -run 'TestBuildTotalRow' -count=1
    expected: "36"
    actual  : "48"        # +12 = 3 × cache.r7g.large
--- FAIL: TestBuildTotalRow_NUSkipsRowsWithBlankNUCell
    Should be empty, but was 12
--- FAIL: TestBuildTotalRow_NUBlankWhenNoRowHasNU
FAIL

Pass after:

$ go test ./cmd/ -run 'TestBuildTotalRow|TestWriteMultiServiceCSV|TestFormatNormalizedUnitsOrBlank' -count=1 -v
--- PASS: TestBuildTotalRow_NUSkipsRowsWithBlankNUCell
--- PASS: TestBuildTotalRow_NUBlankWhenNoRowHasNU
--- PASS: TestFormatNormalizedUnitsOrBlank
--- PASS: TestWriteMultiServiceCSVReport_SortAndTotal   (existing RDS-only TOTAL NU = 60 unchanged)
ok

Mutation probe: I removed the service gate from rowNormalizedUnits and both new tests failed again. I then restored it.

Other checks run on the final tree:

  • go build -o /dev/null ./cmd, go vet ./..., gofmt -l cmd/: clean
  • go test -short ./cmd/ -count=1: ok. go test -short -race on the CSV tests: ok
  • golangci-lint run --allow-parallel-runners ./cmd/... (v2.10.1): 0 issues
  • gocyclo -over 10 cmd/multi_service_csv.go: silent (buildTotalRow 5, rowNormalizedUnits 3, formatNormalizedUnitsOrBlank 2)
  • bash scripts/gosec-hook.sh <changed files> (gosec v2.28.0): 0 issues
  • git secrets --scan (awslabs 1.3.0, --register-aws) in a fresh git init copy with an isolated HOME: clean. scripts/test-git-secrets-allowlist.sh: 44 PASS, 0 FAIL

Not covered: I did not run a real --all-services purchase/report against an AWS account.

Out of scope / notes

  • RDSInstanceNUFromType (cloud-commitments-go) still accepts non-db. types. I left it alone. The CLI now gates on service before calling it, which matches the issue's suggested fix.
  • cmd/multi_service_csv.go was already over the CLAUDE.md 500-line guideline (607 lines before this PR). This PR adds about 10 lines and doesn't split the file.

Maintainer: please mirror the issue's triage labels onto this PR (I don't have label permissions on this repo): triaged, priority/p2, severity/medium, urgency/this-quarter, impact/few, effort/xs, type/bug.

🤖 Generated with Claude Code

… cells

buildTotalRow multiplied every row's Count by RDSInstanceNUFromType,
while formatNormalizedUnitsOrBlank only renders NU for RDS rows. The
lookup accepts any type with three dot-separated parts, so ElastiCache
types like cache.r7g.large resolved a size NU and inflated the TOTAL
above the sum of the visible column.

Extract rowNormalizedUnits as the single per-row NU source (0 for
non-RDS services) and use it for both the cell and the TOTAL.

Closes LeanerCloud#2005

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-cli/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c66fea29-57fb-4655-8d4a-951eb126c42b
📥 Commits

Reviewing files that changed from the base of the PR and between 1306c4d and ca55a3c.

📒 Files selected for processing: 2
  • cmd/multi_service_csv.go
  • cmd/multi_service_csv_test.go
+-----------------------------------+
| Faking self-awareness since 2023. |
+-----------------------------------+
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/few Limited audience effort/xs Trivial / one-liner type/bug Defect labels Oct 17, 2026
@cristim

cristim commented Oct 17, 2026

Copy link
Copy Markdown
Member

Independent review verdict: CLEAN at ca55a3c

Reviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief. Coordinator: claude-cc-reviewer.

Blocking findings: none.

Per-row cells and TOTAL now agree:

  • Per row: NU is rowNormalizedUnits (cmd/multi_service_csv.go:482): count × RDSInstanceNUFromType for RDS/RelationalDB rows, and 0 otherwise. formatNormalizedUnitsOrBlank (:468) renders 0 as blank, and anything else with %g.
  • TOTAL: buildTotalRow (:409) now sums the same function over the same rows with the same formatting, and is blank when the sum is 0 (:416-418).
  • Root cause: ElastiCache and OpenSearch types such as cache.r7g.large resolved a size in the RDS lookup and inflated the TOTAL while their own cell stayed blank.

Display only: TOTAL NU feeds no other code path. The CSV loader still skips the TOTAL row (:147, untouched) and ignores the NU column, so replay parsing is unaffected.

What was attacked and how:

  • go vet ./cmd/... is clean, and go test -race -count=1 ./cmd/... passes.
  • Fail-before: with the previous multi_service_csv.go, both new tests fail. The mixed-rows TOTAL comes out 48 where 36 is expected.

Gate:

@cristim
cristim marked this pull request as ready for review October 18, 2026 00:14
@cristim
cristim merged commit e47d605 into LeanerCloud:main Oct 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): the CSV TOTAL row sums normalized units for rows whose own NU cell is blank

2 participants