Skip to content

loadRecommendationsFromCSV silently tolerates missing required columns + CSV edge-case coverage #1327

Description

@cristim

Background

PR #1225 (TEST-02 fix) demonstrated that the previous TestRunToolFromCSV fixture used CSV headers (Instance Type, Instance Count) that buildColumnIndexMap did not recognize, and the parser produced Count=0 recommendations without an error. The PR corrects the FIXTURE; the underlying PARSER still silently tolerates wrong / missing required columns.

The defect

cmd/multi_service_csv.go:

func buildColumnIndexMap(header []string) map[string]int {
    colIdx := make(map[string]int)
    for i, col := range header {
        colIdx[col] = i
    }
    return colIdx
}

getCSVField returns "" when a column is missing from colIdx; parseCSVRecord then turns the empty string into Count=0, ResourceType="", etc. and loadRecommendationsFromCSV returns (recs, nil) happily. The downstream effect is the very bug TEST-02 surfaced: an entire run no-ops because every row decoded to zero count, with no error message anywhere.

This violates feedback_no_silent_fallbacks.md ("no fallbacks/defaults/fabricated values on money paths; return an explicit error to the user when something is broken"). Reservation purchase IS a money path -- silently treating a mis-formatted CSV as "zero work to do" is precisely the failure mode that document forbids.

Fix

Validate the header up front against the required set, and fail loudly on missing columns:

var requiredCSVColumns = []string{"Service", "Region", "ResourceType", "Count", "Term", "PaymentOption"}

func buildColumnIndexMap(header []string) (map[string]int, error) {
    colIdx := make(map[string]int)
    for i, col := range header {
        colIdx[col] = i
    }
    var missing []string
    for _, req := range requiredCSVColumns {
        if _, ok := colIdx[req]; !ok {
            missing = append(missing, req)
        }
    }
    if len(missing) > 0 {
        return nil, fmt.Errorf("CSV header missing required columns: %v", missing)
    }
    return colIdx, nil
}

And surface the error in loadRecommendationsFromCSV. Optional/derived columns (Engine, RecommendedCount, ExistingCoverage, ...) stay optional.

CSV edge-case coverage (fold into this issue)

The PR's _EmptyFile only covers 0-byte input. Following are all uncovered today and should land as table-driven cases against loadRecommendationsFromCSV:

  • Headers-only-no-rows -> loads 0 recs, currently silently returns success with empty slice. After fix, this remains success but should be tested.
  • Wrong column count on a data row -> csv.ErrFieldCount. Should propagate as "failed to read CSV record".
  • BOM-prefixed file ( before Service,Region,...) -> first column name becomes Service and the column-index lookup misses it. Fix: strip BOM before parsing.
  • CRLF vs LF line endings -> encoding/csv handles both, but lock it in.
  • UTF-16 / Latin-1 encoded files -> should be rejected with a clear error (probably already happens via csv.Reader choking on the BOM).
  • Quoted commas / quoted newlines inside fields -> encoding/csv handles both; lock it in.
  • Count overflow / non-numeric -> strconv.Atoi currently has a silent zero-on-error path somewhere in parseCSVRecord; trace and fix (memory feedback_strict_int_parse.md).
  • The trailing TOTAL summary row currently uses string match on Service == "TOTAL"; if the column header is missing, the TOTAL detection ALSO fails. Tied to the header-validation fix above.

Triage

P2 / sev medium (money path / silent no-op) / impact internal / effort s -- ~30 LOC parser fix + table-driven test additions in cmd/multi_service_csv_test.go.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions