mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
test: add 3 edge-case tests for countInFile overlapping, replaceInFile $& tokens, parseSessionMetadata CRLF
Round 123: Tests for countInFile non-overlapping regex match behavior (aaa with /aa/g returns 1 not 2), replaceInFile with $& and $$ substitution tokens in replacement strings, and parseSessionMetadata CRLF section boundary bleed where \n\n fails to match \r\n\r\n. Total: 929 tests, all passing.
This commit is contained in:
@@ -2368,6 +2368,80 @@ file.ts
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 123: parseSessionMetadata with CRLF line endings — section boundaries break ──
|
||||
console.log('\nRound 123: parseSessionMetadata (CRLF section boundaries — \\n\\n fails to match \\r\\n\\r\\n):');
|
||||
if (test('parseSessionMetadata CRLF content: \\n\\n boundary fails, lazy match bleeds across sections', () => {
|
||||
// session-manager.js lines 119-134: regex uses (?=###|\n\n|$) to delimit sections.
|
||||
// On CRLF content, a blank line is \r\n\r\n, NOT \n\n. The \n\n alternation
|
||||
// won't match, so the lazy [\s\S]*? extends past the blank line until it hits
|
||||
// ### or $. This means completed items may bleed into following sections.
|
||||
//
|
||||
// However, \s* in /### Completed\s*\n/ DOES match \r\n (since \r is whitespace),
|
||||
// so section headers still match — only blank-line boundaries fail.
|
||||
|
||||
// Test 1: CRLF with ### delimiter — works because ### is an alternation
|
||||
const crlfWithHash = [
|
||||
'# Session Title\r\n',
|
||||
'\r\n',
|
||||
'### Completed\r\n',
|
||||
'- [x] Task A\r\n',
|
||||
'### In Progress\r\n',
|
||||
'- [ ] Task B\r\n'
|
||||
].join('');
|
||||
const meta1 = sessionManager.parseSessionMetadata(crlfWithHash);
|
||||
// ### delimiter still works — lazy match stops at ### In Progress
|
||||
assert.ok(meta1.completed.length >= 1,
|
||||
'Completed section should find at least 1 item with ### boundary on CRLF');
|
||||
// Check that Task A is found (may include \r in the trimmed text)
|
||||
const taskA = meta1.completed[0];
|
||||
assert.ok(taskA.includes('Task A'),
|
||||
'Should extract Task A from completed section');
|
||||
|
||||
// Test 2: CRLF with \n\n (blank line) delimiter — this is where it breaks
|
||||
const crlfBlankLine = [
|
||||
'# Session\r\n',
|
||||
'\r\n',
|
||||
'### Completed\r\n',
|
||||
'- [x] First task\r\n',
|
||||
'\r\n', // Blank line = \r\n\r\n — won't match \n\n
|
||||
'Some other text\r\n'
|
||||
].join('');
|
||||
const meta2 = sessionManager.parseSessionMetadata(crlfBlankLine);
|
||||
// On LF, blank line stops the lazy match. On CRLF, it bleeds through.
|
||||
// The lazy [\s\S]*? stops at $ if no ### or \n\n matches,
|
||||
// so "Some other text" may end up captured in the raw section text.
|
||||
// But the items regex /- \[x\]\s*(.+)/g only captures checkbox lines,
|
||||
// so the count stays correct despite the bleed.
|
||||
assert.strictEqual(meta2.completed.length, 1,
|
||||
'Even with CRLF bleed, checkbox regex only matches "- [x]" lines');
|
||||
|
||||
// Test 3: LF version of same content — proves \n\n works normally
|
||||
const lfBlankLine = '# Session\n\n### Completed\n- [x] First task\n\nSome other text\n';
|
||||
const meta3 = sessionManager.parseSessionMetadata(lfBlankLine);
|
||||
assert.strictEqual(meta3.completed.length, 1,
|
||||
'LF version: blank line correctly delimits section');
|
||||
|
||||
// Test 4: CRLF notes section — lazy match goes to $ when \n\n fails
|
||||
const crlfNotes = [
|
||||
'# Session\r\n',
|
||||
'\r\n',
|
||||
'### Notes for Next Session\r\n',
|
||||
'Remember to review\r\n',
|
||||
'\r\n',
|
||||
'This should be separate\r\n'
|
||||
].join('');
|
||||
const meta4 = sessionManager.parseSessionMetadata(crlfNotes);
|
||||
// On CRLF, \n\n fails → lazy match extends to $ → includes "This should be separate"
|
||||
// On LF, \n\n works → notes = "Remember to review" only
|
||||
const lfNotes = '# Session\n\n### Notes for Next Session\nRemember to review\n\nThis should be separate\n';
|
||||
const meta5 = sessionManager.parseSessionMetadata(lfNotes);
|
||||
assert.strictEqual(meta5.notes, 'Remember to review',
|
||||
'LF: notes stop at blank line');
|
||||
// CRLF notes will be longer (bleed through blank line)
|
||||
assert.ok(meta4.notes.length >= meta5.notes.length,
|
||||
'CRLF notes >= LF notes length (CRLF may bleed past blank line)');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Summary
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
@@ -2110,6 +2110,85 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 123: countInFile with overlapping patterns — match(g) is non-overlapping ──
|
||||
console.log('\nRound 123: countInFile (overlapping patterns — String.match(/g/) is non-overlapping):');
|
||||
if (test('countInFile counts non-overlapping matches only — "aaa" with /aa/g returns 1 not 2', () => {
|
||||
// utils.js line 449: `content.match(regex)` with 'g' flag returns an array of
|
||||
// non-overlapping matches. After matching "aa" starting at index 0, the engine
|
||||
// advances to index 2, where only one "a" remains — no second match.
|
||||
// This is standard JS regex behavior but can surprise users expecting overlap.
|
||||
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r123-overlap-'));
|
||||
const testFile = path.join(tmpDir, 'test.txt');
|
||||
try {
|
||||
// "aaa" — a human might count 2 occurrences of "aa" (at 0,1) but match(g) finds 1
|
||||
fs.writeFileSync(testFile, 'aaa');
|
||||
const count1 = utils.countInFile(testFile, 'aa');
|
||||
assert.strictEqual(count1, 1,
|
||||
'"aaa".match(/aa/g) returns ["aa"] — only 1 non-overlapping match');
|
||||
|
||||
// "aaaa" — 2 non-overlapping matches (at 0,2), not 3 overlapping (at 0,1,2)
|
||||
fs.writeFileSync(testFile, 'aaaa');
|
||||
const count2 = utils.countInFile(testFile, 'aa');
|
||||
assert.strictEqual(count2, 2,
|
||||
'"aaaa".match(/aa/g) returns ["aa","aa"] — 2 non-overlapping, not 3 overlapping');
|
||||
|
||||
// "abab" with /aba/g — only 1 match (at 0), not 2 (overlapping at 0,2)
|
||||
fs.writeFileSync(testFile, 'ababab');
|
||||
const count3 = utils.countInFile(testFile, 'aba');
|
||||
assert.strictEqual(count3, 1,
|
||||
'"ababab".match(/aba/g) returns 1 — after match at 0, next try starts at 3');
|
||||
|
||||
// RegExp object behaves the same
|
||||
fs.writeFileSync(testFile, 'aaa');
|
||||
const count4 = utils.countInFile(testFile, /aa/);
|
||||
assert.strictEqual(count4, 1,
|
||||
'RegExp /aa/ also gives 1 non-overlapping match on "aaa" (g flag auto-added)');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 123: replaceInFile with $& and $$ substitution tokens in replacement string ──
|
||||
console.log('\nRound 123: replaceInFile ($& and $$ substitution tokens in replacement):');
|
||||
if (test('replaceInFile replacement string interprets $& as matched text and $$ as literal $', () => {
|
||||
// JS String.replace() interprets special patterns in the replacement string:
|
||||
// $& → inserts the entire matched substring
|
||||
// $$ → inserts a literal "$" character
|
||||
// $' → inserts the portion after the matched substring
|
||||
// $` → inserts the portion before the matched substring
|
||||
// This is different from capture groups ($1, $2) already tested in Round 91.
|
||||
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r123-dollar-'));
|
||||
const testFile = path.join(tmpDir, 'test.txt');
|
||||
try {
|
||||
// $& — inserts the matched text itself
|
||||
fs.writeFileSync(testFile, 'hello world');
|
||||
utils.replaceInFile(testFile, 'world', '[$&]');
|
||||
assert.strictEqual(utils.readFile(testFile), 'hello [world]',
|
||||
'$& in replacement inserts the matched text "world" → "[world]"');
|
||||
|
||||
// $$ — inserts a literal $ sign
|
||||
fs.writeFileSync(testFile, 'price is 100');
|
||||
utils.replaceInFile(testFile, '100', '$$100');
|
||||
assert.strictEqual(utils.readFile(testFile), 'price is $100',
|
||||
'$$ becomes literal $ → "100" replaced with "$100"');
|
||||
|
||||
// $& with options.all — applies to each match
|
||||
fs.writeFileSync(testFile, 'foo bar foo');
|
||||
utils.replaceInFile(testFile, 'foo', '($&)', { all: true });
|
||||
assert.strictEqual(utils.readFile(testFile), '(foo) bar (foo)',
|
||||
'$& in replaceAll inserts each respective matched text');
|
||||
|
||||
// Combined $$ and $& in same replacement (3 $ + &)
|
||||
fs.writeFileSync(testFile, 'item costs 50');
|
||||
utils.replaceInFile(testFile, '50', '$$$&');
|
||||
// In replacement string: $$ → "$" then $& → "50" so result is "$50"
|
||||
assert.strictEqual(utils.readFile(testFile), 'item costs $50',
|
||||
'$$$& (3 dollars + ampersand) means literal $ followed by matched text → "$50"');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Results ===');
|
||||
console.log(`Passed: ${passed}`);
|
||||
|
||||
Reference in New Issue
Block a user