From 3031d57d1befdf54ff9907768b777d348b8bdff5 Mon Sep 17 00:00:00 2001 From: mlischetti Date: Wed, 22 Jul 2026 15:46:49 -0300 Subject: [PATCH] W-23530974 fix(native-lib): normalize CRLF in TCK JSON string comparison The TCK lane failed on the Windows CI runner: a JSON output embedding a CSV value is written with CRLF line endings on Windows but the fixture uses LF, so the escaped \r\n vs \n inside the JSON string value made compareJson report a mismatch. macOS/Linux (LF) never hit it. Normalize CRLF -> LF within JSON string values during comparison (deepEqual gains a normEol flag used by the JSON path), matching the CLI's AssertionHelper EOL handling. XML text nodes are already normalized by fast-xml-parser, and csv/txt/etc. already EOL-normalize, so the fix is scoped to the JSON path. - compare.ts: deepEqual(a, b, normEol=false); compareJson passes normEol=true. - 2 unit tests: CRLF-vs-LF inside a string value matches; genuinely different string values still differ. TCK lane: 660 pass / 57 skip unchanged locally. Unit lane: 121. Co-Authored-By: Claude Opus 4.8 (1M context) --- native-lib/node/tests/tck/compare.ts | 21 ++++++++++++++----- .../node/tests/unit/tck-compare.test.ts | 15 +++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/native-lib/node/tests/tck/compare.ts b/native-lib/node/tests/tck/compare.ts index 495d3ba..9be27c8 100644 --- a/native-lib/node/tests/tck/compare.ts +++ b/native-lib/node/tests/tck/compare.ts @@ -73,7 +73,10 @@ function compareJson(actual: string, expected: string): CompareResult { } catch (err) { return fail(`expected is not valid JSON: ${err}`); } - return deepEqual(a, e) ? ok : fail(`JSON mismatch:\n actual: ${actual.trim()}\n expected: ${expected.trim()}`); + // Normalize CRLF → LF within string values so platform line endings don't + // cause a mismatch — e.g. an embedded CSV value the writer emits with CRLF on + // Windows but LF elsewhere. Matches the CLI's AssertionHelper EOL handling. + return deepEqual(a, e, true) ? ok : fail(`JSON mismatch:\n actual: ${actual.trim()}\n expected: ${expected.trim()}`); } const xmlParser = new XMLParser({ @@ -141,15 +144,23 @@ function stripAllWhitespace(s: string): string { return s.replace(/\s+/g, ""); } -/** Structural deep equality for JSON values (objects compared key-insensitively to order). */ -export function deepEqual(a: unknown, b: unknown): boolean { +/** + * Structural deep equality for JSON values (objects compared key-insensitively + * to order). When `normEol` is true, string values are compared with CRLF + * normalized to LF so platform line endings inside a value don't cause a + * mismatch. + */ +export function deepEqual(a: unknown, b: unknown, normEol = false): boolean { + if (typeof a === "string" && typeof b === "string") { + return normEol ? a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n") : a === b; + } if (a === b) return true; if (a === null || b === null) return a === b; if (typeof a !== typeof b) return false; if (Array.isArray(a) || Array.isArray(b)) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; - return a.every((v, i) => deepEqual(v, b[i])); + return a.every((v, i) => deepEqual(v, b[i], normEol)); } if (typeof a === "object" && typeof b === "object") { @@ -158,7 +169,7 @@ export function deepEqual(a: unknown, b: unknown): boolean { const ak = Object.keys(ao); const bk = Object.keys(bo); if (ak.length !== bk.length) return false; - return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k])); + return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k], normEol)); } return false; diff --git a/native-lib/node/tests/unit/tck-compare.test.ts b/native-lib/node/tests/unit/tck-compare.test.ts index 90699f2..dd9894b 100644 --- a/native-lib/node/tests/unit/tck-compare.test.ts +++ b/native-lib/node/tests/unit/tck-compare.test.ts @@ -28,6 +28,21 @@ describe("compareOutput — json (structural)", () => { expect(r.match).toBe(false); expect(r.detail).toMatch(/actual is not valid JSON/); }); + + it("ignores CRLF vs LF inside a string value (Windows line endings)", () => { + // Regression: a JSON output embedding CSV — the writer emits CRLF on Windows + // but the fixture has LF. The line breaks are escaped \r\n / \n inside the + // JSON string, so they must be normalized during value comparison. + const actual = buf(JSON.stringify({ csv: "a|b\r\nx|y\r\n" })); + const expected = buf(JSON.stringify({ csv: "a|b\nx|y\n" })); + expect(compareOutput("json", actual, expected).match).toBe(true); + }); + + it("still distinguishes genuinely different string values", () => { + const actual = buf(JSON.stringify({ v: "hello" })); + const expected = buf(JSON.stringify({ v: "world" })); + expect(compareOutput("json", actual, expected).match).toBe(false); + }); }); describe("compareOutput — xml (structural)", () => {