Skip to content

test(gsheet-sync): #204 pin CSV<->grid round-trip contract (11 edge-case tests) - #507

Merged
jsboige merged 1 commit into
masterfrom
test/204-csv-grid-conversion-contract
Jun 16, 2026
Merged

test(gsheet-sync): #204 pin CSV<->grid round-trip contract (11 edge-case tests)#507
jsboige merged 1 commit into
masterfrom
test/204-csv-grid-conversion-contract

Conversation

@jsboige

@jsboige jsboige commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What

Secondaire of ai-01 dispatch bwpx5q (gate-safe #204 deep-queue). Pins the home-grown CsvToGrid / GridToCsv converters — the bridge between a Google Sheet download and a local CSV, and between a local CSV and a Sheets upload — with 11 edge-case contract tests. The invariant that matters most is round-trip symmetry (grid → csv → grid identity) on the multiline / quoted / ragged data that real Argumentum CSVs actually contain.

This is the 2nd pure target the dispatch offered ("CsvToGrid edge"). The converters are already pure (static CsvToGrid, instance GridToCsv — both plain string→grid / grid→string transforms), so there is no inline logic to extract: this PR is additive test coverage on the fragile CSV round-trip contract, matching the spirit of #204 (expand coverage on fragile contracts).

Two non-obvious behaviours now pinned

Reading the code, two things are easy to "clean up" into a regression. Both are documented in the test file's XML doc and pinned by tests:

  1. CsvToGrid is configured with HasHeaderRecord = true, but the manual while (csv.Read()) loop returns the header as the first grid row (there is no separate ReadHeader() skip). This is deliberate: Sheets expects the header row inside the uploaded grid. A "cleanup" that switched to GetRecords<T>() or added an explicit header skip would silently drop row 0 and break every upload. (Pinned by RoundTrip_* identity + the existing CsvToGrid_PreservesHeaderRow.)

  2. The read loop is bounded by csv.Parser.Count, which is the current record's field count (dynamic, per-row), not the header's. So ragged rows (rows with fewer/more fields than the header) are preserved without throwing MissingFieldException, and round-trip without padding or truncation. (Pinned by RoundTrip_PreservesRaggedRows.)

Tests — CsvGridConversionContractTests (11)

Parse direction (RFC 4180):

  • CsvToGrid_ParsesQuotedFieldWithEmbeddedNewline — a newline inside a quoted field is part of that field, not a record separator (highest-risk CSV edge case for Argumentum descriptions/examples).
  • CsvToGrid_ParsesEscapedDoubleQuotes""".

Round-trip symmetry (the sync invariant):

  • RoundTrip_PreservesQuotedFieldWithEmbeddedNewline — multiline cell survives grid→csv→grid.
  • RoundTrip_PreservesEscapedDoubleQuotes
  • RoundTrip_PreservesEmptyAndCommaFields — empty cells + comma-forcing cells.
  • RoundTrip_PreservesRaggedRows — rows of differing lengths.
  • RoundTrip_SingleColumnCsv_NoCommas — degenerate single-column CSV.

Degenerate inputs (no NRE, no phantom rows):

  • CsvToGrid_EmptyString_ReturnsEmptyGrid
  • GridToCsv_EmptyOrNullGrid_ReturnsEmptyString
  • GridToCsv_NullCells_BecomeEmptyStrings — pins the cell?.ToString() ?? "" guard (null → empty, not literal "null").
  • CsvToGrid_TrailingNewline_DoesNotCreatePhantomRow — the terminator newline must not add an empty row.

Verification

Scope notes

  • Test-only / output-neutral → mergeable during the release gate per ai-01's gate-safe framing (the gate holds even output-neutral pipeline behaviour changes; this PR changes no behaviour at all).
  • SyncSafetyChecker (the other dispatch candidate) already has 11 tests covering its thresholds/boundaries/zero-rows/custom-config — well-covered, lower value, so not touched.

Contributes to #204. Refs #193 (GSheetSync), #216.

🤖 Worker po-2024 — secondaire of dispatch bwpx5q.

…case tests

Secondaire of ai-01 dispatch bwpx5q (gate-safe #204 deep-queue). Pins the
home-grown CsvToGrid/GridToCsv converters - the bridge between a Sheets
download and local CSV, and between CSV and a Sheets upload - with edge-case
contract tests. The critical invariant is round-trip symmetry on the
multiline/quoted/ragged data that real Argumentum CSVs contain.

Two non-obvious behaviours are now pinned:
1. CsvToGrid has HasHeaderRecord=true but the manual while(Read()) loop returns
   the header as the first grid row (no ReadHeader() skip) - deliberate, since
   Sheets expects the header in the uploaded grid. A GetRecords<T>() "cleanup"
   would silently drop row 0 and break every upload.
2. The read loop is bounded by csv.Parser.Count (dynamic per-record), so ragged
   rows survive without MissingFieldException and round-trip unaltered.

CsvToGrid/GridToCsv are already pure (no extraction needed); this is additive
test coverage on the fragile CSV round-trip contract. Test-only, output-neutral.

- 11 new tests, 0 prod change
- Full suite: 329 passed / 0 failed / 5 skipped (was 318 + 11)

Contributes to #204. Refs #193 (GSheetSync), #216.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clusterManager-Myia

Copy link
Copy Markdown
Collaborator

[NanoClaw] LGTM — test-only, 1 file (+197)

12 contract tests pinning CSV↔grid round-trip symmetry for GSheetService. Covers the right edge cases: RFC 4180 embedded newlines, escaped quotes, ragged rows, null cells, trailing newlines. Class docstring documents two non-obvious fragile behaviours (header row preserved deliberately, dynamic field count bound). Clean test-only extraction.

No concerns.

@jsboige
jsboige merged commit 1cc6a54 into master Jun 16, 2026
3 checks passed
@jsboige
jsboige deleted the test/204-csv-grid-conversion-contract branch June 16, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants