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.
Background
PR #1225 (TEST-02 fix) demonstrated that the previous
TestRunToolFromCSVfixture used CSV headers (Instance Type,Instance Count) thatbuildColumnIndexMapdid not recognize, and the parser producedCount=0recommendations 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:getCSVFieldreturns""when a column is missing fromcolIdx;parseCSVRecordthen turns the empty string intoCount=0,ResourceType="", etc. andloadRecommendationsFromCSVreturns(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:
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
_EmptyFileonly covers 0-byte input. Following are all uncovered today and should land as table-driven cases againstloadRecommendationsFromCSV:csv.ErrFieldCount. Should propagate as "failed to read CSV record".beforeService,Region,...) -> first column name becomesServiceand the column-index lookup misses it. Fix: strip BOM before parsing.encoding/csvhandles both, but lock it in.csv.Readerchoking on the BOM).encoding/csvhandles both; lock it in.Countoverflow / non-numeric ->strconv.Atoicurrently has a silent zero-on-error path somewhere inparseCSVRecord; trace and fix (memoryfeedback_strict_int_parse.md).TOTALsummary row currently uses string match onService == "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.