Files
msd-core/tests/text-lines.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

169 lines
6.6 KiB
JavaScript

'use strict';
/**
* Tests for `src/text-lines.cts` — the line-terminator seam (#3212 Phase 2, #3413).
*
* Design: .msd/phase/chore-3413-text-lines-seam/40-design.md
* Test matrix: .msd/phase/chore-3413-text-lines-seam/50-test-matrix.md
* ADR: docs/adr/3212-lexical-seam-consolidation.md §3, §6, §7
*
* TDD RED: `src/text-lines.cts` does not exist yet — this file's
* `require('../msd-core/bin/lib/text-lines.cjs')` throws MODULE_NOT_FOUND until
* the implementing phase adds it. That is the intended starting state (mirrors
* tests/pattern.test.cjs's RED convention from Phase 1, #3412).
*
* Covers test-matrix rows 1-23: splitLines (1-7), normalizeEol (8-10),
* detectEol (11-16), joinLines (17-20), round-trip (21-23).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
// Seeded fast-check convention: require the shared setup helper (NOT
// 'fast-check' directly) so numRuns/seed are configured globally before any
// fc.assert() call — mirrors tests/pattern.test.cjs. seed: 42, overridable
// via MSD_FC_SEED.
const fc = require('./helpers/fast-check-setup.cjs');
const { splitLines, normalizeEol, detectEol, joinLines } = require('../msd-core/bin/lib/text-lines.cjs');
// ─── splitLines — rows 1-7 ─────────────────────────────────────────────────
describe('splitLines', () => {
test('row 1: splits LF-only content', () => {
assert.deepStrictEqual(splitLines('a\nb\nc'), ['a', 'b', 'c']);
});
test('row 2: splits CRLF content with no trailing \\r per line', () => {
const result = splitLines('a\r\nb\r\nc');
assert.deepStrictEqual(result, ['a', 'b', 'c']);
for (const line of result) {
assert.ok(!line.includes('\r'), `line ${JSON.stringify(line)} must not carry a trailing \\r`);
}
});
test('row 3: handles mixed CRLF/LF in one document', () => {
assert.deepStrictEqual(splitLines('a\r\nb\nc'), ['a', 'b', 'c']);
});
test('row 4: empty string matches native String#split(/\\r?\\n/) contract', () => {
assert.deepStrictEqual(splitLines(''), ['']);
assert.deepStrictEqual(splitLines(''), ''.split(/\r?\n/));
});
test('row 5: trailing terminator yields a trailing empty element', () => {
assert.deepStrictEqual(splitLines('a\n'), ['a', '']);
});
test('row 6: a lone \\r without a following \\n is not a delimiter', () => {
assert.deepStrictEqual(splitLines('a\rb'), ['a\rb']);
});
test('row 7: rejects a non-string input', () => {
assert.throws(() => splitLines(42), TypeError);
assert.throws(() => splitLines(null), TypeError);
});
});
// ─── normalizeEol — rows 8-10 ──────────────────────────────────────────────
describe('normalizeEol', () => {
test('row 8: converts CRLF and leaves LF alone', () => {
assert.strictEqual(normalizeEol('a\r\nb\nc'), 'a\nb\nc');
});
test('row 9: no-op on already-LF content', () => {
assert.strictEqual(normalizeEol('a\nb'), 'a\nb');
});
test('row 10: strips an unpaired bare \\r, matching the scripts/ copies it replaces', () => {
assert.strictEqual(normalizeEol('a\rb\rc'), 'abc');
});
});
// ─── detectEol — rows 11-16 ────────────────────────────────────────────────
describe('detectEol', () => {
test('row 11: all-CRLF content', () => {
assert.strictEqual(detectEol('a\r\nb\r\nc'), '\r\n');
});
test('row 12: all-LF content', () => {
assert.strictEqual(detectEol('a\nb\nc'), '\n');
});
test('row 13: mixed content, CRLF majority wins', () => {
assert.strictEqual(detectEol('a\r\nb\nc\r\n'), '\r\n');
});
test('row 14: mixed content, LF majority wins', () => {
assert.strictEqual(detectEol('a\nb\nc\r\n'), '\n');
});
test('row 14b: an exact 1:1 tie resolves to \\r\\n per the documented default (#3413 review fix)', () => {
assert.strictEqual(detectEol('a\nb\r\nc'), '\r\n');
});
test('row 15: no terminator present returns the documented default', () => {
assert.strictEqual(detectEol('no terminator at all'), '\r\n');
});
test('row 16: empty string returns the documented default', () => {
assert.strictEqual(detectEol(''), '\r\n');
});
});
// ─── joinLines — rows 17-20 ────────────────────────────────────────────────
describe('joinLines', () => {
test('row 17: defaults to LF', () => {
assert.strictEqual(joinLines(['a', 'b', 'c']), 'a\nb\nc');
});
test('row 18: explicit CRLF', () => {
assert.strictEqual(joinLines(['a', 'b', 'c'], '\r\n'), 'a\r\nb\r\nc');
});
test('row 19: empty array yields empty string', () => {
assert.strictEqual(joinLines([]), '');
});
test('row 20: single-element array has no terminator', () => {
assert.strictEqual(joinLines(['only']), 'only');
});
});
// ─── round-trip — rows 21-23 ───────────────────────────────────────────────
describe('round-trip', () => {
test('row 21: split->detect->join reproduces a CRLF document exactly (#3212 §3)', () => {
const x = '---\r\nphase: 01\r\nmust_haves:\r\n truths:\r\n - "first truth"\r\n---\r\n\r\nBody.\r\n';
assert.strictEqual(joinLines(splitLines(x), detectEol(x)), x);
});
test('row 22: split->detect->join reproduces an LF document exactly', () => {
const x = '---\nphase: 01\nmust_haves:\n truths:\n - "first truth"\n---\n\nBody.\n';
assert.strictEqual(joinLines(splitLines(x), detectEol(x)), x);
});
test('row 23: property — joinLines/splitLines are inverses over terminator-free line arrays (seeded)', () => {
// Generator per 50-test-matrix.md's "Property test note (row 23)": arrays
// of strings with no embedded \r/\n, 1 to 20 elements (0-element arrays
// are the known non-invertible edge already covered explicitly at rows
// 4/19 — joinLines([]) === '' but splitLines('') === [''], not [] — so
// that edge is excluded here rather than re-asserted as a property
// failure), eol drawn from ['\n', '\r\n'].
fc.assert(
fc.property(
fc.array(
fc.string().filter(s => !/[\r\n]/.test(s)),
{ minLength: 1, maxLength: 20 }
),
fc.constantFrom('\n', '\r\n'),
(lines, eol) => {
assert.deepStrictEqual(splitLines(joinLines(lines, eol)), lines);
}
)
);
});
});