From 791da32c6b5436ea01bd863ea6090b4b075e7c09 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 17:47:50 -0800 Subject: [PATCH] test: add 3 tests for Unicode alias rejection, newline-in-path heuristic, and read-only append (Round 112) - resolveAlias rejects Unicode characters (accented, CJK, emoji, Cyrillic homoglyphs) - getSessionStats treats absolute .tmp paths with embedded newlines as content, not file paths - appendSessionContent returns false on EACCES for read-only files Total: 896 tests --- tests/lib/session-aliases.test.js | 34 ++++++++++++++++++ tests/lib/session-manager.test.js | 60 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index 3ae74345..892accab 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -1418,6 +1418,40 @@ function runTests() { 'Error message should mention 128-char limit'); })) passed++; else failed++; + // ── Round 112: resolveAlias rejects Unicode characters in alias name ── + console.log('\nRound 112: resolveAlias (Unicode rejection):'); + if (test('resolveAlias returns null for alias names containing Unicode characters', () => { + resetAliases(); + // First create a valid alias to ensure the store works + aliases.setAlias('valid-alias', '/path/to/session'); + const validResult = aliases.resolveAlias('valid-alias'); + assert.notStrictEqual(validResult, null, 'Valid ASCII alias should resolve'); + + // Unicode accented characters — rejected by /^[a-zA-Z0-9_-]+$/ + const accentedResult = aliases.resolveAlias('café-session'); + assert.strictEqual(accentedResult, null, + 'Accented character "é" should be rejected by [a-zA-Z0-9_-]'); + + const umlautResult = aliases.resolveAlias('über-test'); + assert.strictEqual(umlautResult, null, + 'Umlaut "ü" should be rejected by [a-zA-Z0-9_-]'); + + // CJK characters + const cjkResult = aliases.resolveAlias('会議-notes'); + assert.strictEqual(cjkResult, null, + 'CJK characters should be rejected'); + + // Emoji + const emojiResult = aliases.resolveAlias('rocket-🚀'); + assert.strictEqual(emojiResult, null, + 'Emoji should be rejected by the ASCII-only regex'); + + // Cyrillic characters that look like Latin (homoglyphs) + const cyrillicResult = aliases.resolveAlias('tеst'); // 'е' is Cyrillic U+0435 + assert.strictEqual(cyrillicResult, null, + 'Cyrillic homoglyph "е" (U+0435) should be rejected even though it looks like "e"'); + })) 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 7717e6c2..81d3f612 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -1930,6 +1930,66 @@ file.ts 'Clean context should have second line'); })) passed++; else failed++; + // ── Round 112: getSessionStats with newline-containing absolute path — treated as content ── + console.log('\nRound 112: getSessionStats (newline-in-path heuristic):'); + if (test('getSessionStats treats absolute .tmp path containing newline as content, not a file path', () => { + // The looksLikePath heuristic at line 163-166 checks: + // !sessionPathOrContent.includes('\n') + // A string with embedded newline fails this check and is treated as content + const pathWithNewline = '/tmp/sessions/2026-01-15\n-abcd1234-session.tmp'; + + // This should NOT throw (it's treated as content, not a path that doesn't exist) + const stats = sessionManager.getSessionStats(pathWithNewline); + assert.ok(stats, 'Should return stats object (treating input as content)'); + // The "content" has 2 lines (split by the embedded \n) + assert.strictEqual(stats.lineCount, 2, + 'Should count 2 lines in the "content" (split at \\n)'); + // No markdown headings = no completed/in-progress items + assert.strictEqual(stats.totalItems, 0, + 'Should find 0 items in non-markdown content'); + + // Contrast: a real absolute path without newlines IS treated as a path + const realPath = '/tmp/nonexistent-session.tmp'; + const realStats = sessionManager.getSessionStats(realPath); + // getSessionContent returns '' for non-existent files, so lineCount = 1 (empty string split) + assert.ok(realStats, 'Should return stats even for nonexistent path'); + assert.strictEqual(realStats.lineCount, 0, + 'Non-existent file returns empty content with 0 lines'); + })) passed++; else failed++; + + // ── Round 112: appendSessionContent with read-only file — returns false ── + console.log('\nRound 112: appendSessionContent (read-only file):'); + if (test('appendSessionContent returns false when file is read-only (EACCES)', () => { + if (process.platform === 'win32') { + // chmod doesn't work reliably on Windows — skip + assert.ok(true, 'Skipped on Windows'); + return; + } + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r112-readonly-')); + const readOnlyFile = path.join(tmpDir, '2026-01-15-session.tmp'); + try { + fs.writeFileSync(readOnlyFile, '# Session\n\nInitial content\n'); + // Make file read-only + fs.chmodSync(readOnlyFile, 0o444); + // Verify it exists and is readable + const content = fs.readFileSync(readOnlyFile, 'utf8'); + assert.ok(content.includes('Initial content'), 'File should be readable'); + + // appendSessionContent should catch EACCES and return false + const result = sessionManager.appendSessionContent(readOnlyFile, '\nAppended data'); + assert.strictEqual(result, false, + 'Should return false when file is read-only (fs.appendFileSync throws EACCES)'); + + // Verify original content unchanged + const afterContent = fs.readFileSync(readOnlyFile, 'utf8'); + assert.ok(!afterContent.includes('Appended data'), + 'Original content should be unchanged'); + } finally { + try { fs.chmodSync(readOnlyFile, 0o644); } catch {} + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);