From a563df2a520156b2c0c13c6848fd828840775d6d Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 18:05:28 -0800 Subject: [PATCH] test: add edge-case tests for countInFile empty pattern, parseSessionMetadata CRLF, and updateAliasTitle empty string coercion (round 115) --- tests/lib/session-aliases.test.js | 34 +++++++++++++++++++ tests/lib/session-manager.test.js | 56 +++++++++++++++++++++++++++++++ tests/lib/utils.test.js | 34 +++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index 2b5bb21d..c5b0ccbb 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -1483,6 +1483,40 @@ function runTests() { ); })) passed++; else failed++; + // ── Round 115: updateAliasTitle with empty string — stored as null via || but returned as "" ── + console.log('\nRound 115: updateAliasTitle (empty string title — stored null, returned ""):'); + if (test('updateAliasTitle with empty string stores null but returns empty string (|| coercion mismatch)', () => { + resetAliases(); + + // Create alias with a title + aliases.setAlias('r115-alias', '/path/to/session', 'Original Title'); + const before = aliases.resolveAlias('r115-alias'); + assert.strictEqual(before.title, 'Original Title', 'Baseline: title should be set'); + + // Update title with empty string + // Line 383: typeof "" === 'string' → passes validation + // Line 393: "" || null → null (empty string is falsy in JS) + // Line 400: returns { title: "" } (original parameter, not stored value) + const result = aliases.updateAliasTitle('r115-alias', ''); + assert.strictEqual(result.success, true, 'Should succeed (empty string passes validation)'); + assert.strictEqual(result.title, '', 'Return value reflects the input parameter (empty string)'); + + // But what's actually stored? + const after = aliases.resolveAlias('r115-alias'); + assert.strictEqual(after.title, null, + 'Stored title should be null because "" || null evaluates to null'); + + // Contrast: non-empty string is stored as-is + aliases.updateAliasTitle('r115-alias', 'New Title'); + const withTitle = aliases.resolveAlias('r115-alias'); + assert.strictEqual(withTitle.title, 'New Title', 'Non-empty string stored as-is'); + + // null explicitly clears title + aliases.updateAliasTitle('r115-alias', null); + const cleared = aliases.resolveAlias('r115-alias'); + assert.strictEqual(cleared.title, null, 'null clears title'); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/session-manager.test.js b/tests/lib/session-manager.test.js index 1722968d..a6530257 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -2055,6 +2055,62 @@ file.ts 'Trailing whitespace should be trimmed'); })) passed++; else failed++; + // ── Round 115: parseSessionMetadata with CRLF line endings — section boundaries differ ── + console.log('\nRound 115: parseSessionMetadata (CRLF line endings — \\r\\n vs \\n in section regexes):'); + if (test('parseSessionMetadata handles CRLF content — title trimmed, sections may over-capture', () => { + // Title regex /^#\s+(.+)$/m: . matches \r, trim() removes it + const crlfTitle = '# My Session\r\n\r\n**Date:** 2026-01-15'; + const titleMeta = sessionManager.parseSessionMetadata(crlfTitle); + assert.strictEqual(titleMeta.title, 'My Session', + 'Title should be trimmed (\\r removed by .trim())'); + assert.strictEqual(titleMeta.date, '2026-01-15', + 'Date extraction unaffected by CRLF'); + + // Completed section with CRLF: regex ### Completed\s*\n works because \s* matches \r + // But the boundary (?=###|\n\n|$) — \n\n won't match \r\n\r\n + const crlfSections = [ + '# Session\r\n', + '\r\n', + '### Completed\r\n', + '- [x] Task A\r\n', + '- [x] Task B\r\n', + '\r\n', + '### In Progress\r\n', + '- [ ] Task C\r\n' + ].join(''); + + const sectionMeta = sessionManager.parseSessionMetadata(crlfSections); + + // \s* in "### Completed\s*\n" matches the \r before \n, so section header matches + assert.ok(sectionMeta.completed.length >= 2, + 'Should find at least 2 completed items (\\s* consumes \\r before \\n)'); + assert.ok(sectionMeta.completed.includes('Task A'), 'Should find Task A'); + assert.ok(sectionMeta.completed.includes('Task B'), 'Should find Task B'); + + // In Progress section: \n\n boundary fails on \r\n\r\n, so the lazy [\s\S]*? + // stops at ### instead — this still works because ### is present + assert.ok(sectionMeta.inProgress.length >= 1, + 'Should find at least 1 in-progress item'); + assert.ok(sectionMeta.inProgress.includes('Task C'), 'Should find Task C'); + + // Edge case: CRLF content with NO section headers after Completed — + // \n\n boundary fails, so [\s\S]*? falls through to $ (end of string) + const crlfNoNextSection = [ + '# Session\r\n', + '\r\n', + '### Completed\r\n', + '- [x] Only task\r\n', + '\r\n', + 'Some trailing text\r\n' + ].join(''); + + const noNextMeta = sessionManager.parseSessionMetadata(crlfNoNextSection); + // Without a ### boundary, the \n\n lookahead fails on \r\n\r\n, + // so [\s\S]*? extends to $ and captures everything including trailing text + assert.ok(noNextMeta.completed.length >= 1, + 'Should find at least 1 completed item in CRLF-only content'); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index 674af5f3..d9ecfeb2 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -1799,6 +1799,40 @@ function runTests() { } })) passed++; else failed++; + // ── Round 115: countInFile with empty string pattern — matches at every position boundary ── + console.log('\nRound 115: countInFile (empty string pattern — matches at every zero-width position):'); + if (test('countInFile with empty string pattern returns content.length + 1 (matches between every char)', () => { + const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r115-empty-pattern-')); + const testFile = path.join(tmpDir, 'test.txt'); + try { + // "hello" is 5 chars → 6 zero-width positions: |h|e|l|l|o| + fs.writeFileSync(testFile, 'hello'); + const count = utils.countInFile(testFile, ''); + assert.strictEqual(count, 6, + 'Empty string pattern creates /(?:)/g which matches at 6 position boundaries in "hello"'); + + // Empty file → "" has 1 zero-width position (the empty string itself) + fs.writeFileSync(testFile, ''); + const emptyCount = utils.countInFile(testFile, ''); + assert.strictEqual(emptyCount, 1, + 'Empty file still has 1 zero-width position boundary'); + + // Single char → 2 positions: |a| + fs.writeFileSync(testFile, 'a'); + const singleCount = utils.countInFile(testFile, ''); + assert.strictEqual(singleCount, 2, + 'Single character file has 2 position boundaries'); + + // Newlines count as characters too + fs.writeFileSync(testFile, 'a\nb'); + const newlineCount = utils.countInFile(testFile, ''); + assert.strictEqual(newlineCount, 4, + '"a\\nb" is 3 chars → 4 position boundaries'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`);