From 6737f3245b2d260cfd0bac605ec3b48ad9334b88 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 17:24:36 -0800 Subject: [PATCH] test: add 3 tests for appendFile new-file creation, getExecCommand traversal, getAllSessions non-session skip Round 109: - appendFile creating new file in non-existent directory (ensureDir + appendFileSync) - getExecCommand with ../ path traversal in binary (SAFE_NAME_REGEX allows ../) - getAllSessions skips .tmp files that don't match session filename format --- tests/lib/package-manager.test.js | 23 ++++++++++++++++++++ tests/lib/session-manager.test.js | 35 +++++++++++++++++++++++++++++++ tests/lib/utils.test.js | 23 ++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index 0eaed0b7..e2066f0f 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -1489,6 +1489,29 @@ function runTests() { 'Same string as explicit string arg is correctly rejected by SAFE_ARGS_REGEX'); })) passed++; else failed++; + // ── Round 109: getExecCommand with ../ path traversal in binary — SAFE_NAME_REGEX allows it ── + console.log('\nRound 109: getExecCommand (path traversal in binary — SAFE_NAME_REGEX permits ../ in binary name):'); + if (test('getExecCommand accepts ../../../etc/passwd as binary because SAFE_NAME_REGEX allows ../', () => { + const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; + try { + process.env.CLAUDE_PACKAGE_MANAGER = 'npm'; + // SAFE_NAME_REGEX = /^[@a-zA-Z0-9_.\/-]+$/ individually allows . and / + const cmd = pm.getExecCommand('../../../etc/passwd'); + assert.strictEqual(cmd, 'npx ../../../etc/passwd', + 'Path traversal in binary passes SAFE_NAME_REGEX because . and / are individually allowed'); + // Also verify scoped path traversal + const cmd2 = pm.getExecCommand('@scope/../../evil'); + assert.strictEqual(cmd2, 'npx @scope/../../evil', + 'Scoped path traversal also passes the regex'); + } finally { + if (originalEnv !== undefined) { + process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; + } else { + delete process.env.CLAUDE_PACKAGE_MANAGER; + } + } + })) passed++; else failed++; + // ── Round 108: getRunCommand with path traversal — SAFE_NAME_REGEX allows ../ sequences ── console.log('\nRound 108: getRunCommand (path traversal — SAFE_NAME_REGEX permits ../ via allowed / and . chars):'); if (test('getRunCommand accepts @scope/../../evil because SAFE_NAME_REGEX allows ../', () => { diff --git a/tests/lib/session-manager.test.js b/tests/lib/session-manager.test.js index e33da514..172fd674 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -1785,6 +1785,41 @@ file.ts } })) passed++; else failed++; + // ── Round 109: getAllSessions skips .tmp files that don't match session filename format ── + console.log('\nRound 109: getAllSessions (non-session .tmp files — parseSessionFilename returns null → skip):'); + if (test('getAllSessions ignores .tmp files with non-matching filenames', () => { + const isoHome = path.join(os.tmpdir(), `ecc-r109-nonsession-${Date.now()}`); + const isoSessionsDir = path.join(isoHome, '.claude', 'sessions'); + fs.mkdirSync(isoSessionsDir, { recursive: true }); + // Create one valid session file + const validName = '2026-03-01-abcd1234-session.tmp'; + fs.writeFileSync(path.join(isoSessionsDir, validName), '# Valid Session'); + // Create non-session .tmp files that don't match the expected pattern + fs.writeFileSync(path.join(isoSessionsDir, 'notes.tmp'), 'personal notes'); + fs.writeFileSync(path.join(isoSessionsDir, 'scratch.tmp'), 'scratch data'); + fs.writeFileSync(path.join(isoSessionsDir, 'backup-2026.tmp'), 'backup'); + const origHome = process.env.HOME; + const origUserProfile = process.env.USERPROFILE; + process.env.HOME = isoHome; + process.env.USERPROFILE = isoHome; + try { + delete require.cache[require.resolve('../../scripts/lib/session-manager')]; + delete require.cache[require.resolve('../../scripts/lib/utils')]; + const freshManager = require('../../scripts/lib/session-manager'); + const result = freshManager.getAllSessions({ limit: 100 }); + assert.strictEqual(result.total, 1, + 'Should find only 1 valid session (non-matching .tmp files skipped via !metadata continue)'); + assert.strictEqual(result.sessions[0].shortId, 'abcd1234', + 'The one valid session should have correct shortId'); + } finally { + process.env.HOME = origHome; + process.env.USERPROFILE = origUserProfile; + delete require.cache[require.resolve('../../scripts/lib/session-manager')]; + delete require.cache[require.resolve('../../scripts/lib/utils')]; + fs.rmSync(isoHome, { recursive: true, force: true }); + } + })) passed++; else failed++; + // ── Round 108: getSessionSize exact boundary at 1024 bytes — B→KB transition ── console.log('\nRound 108: getSessionSize (exact 1024-byte boundary — < means 1024 is KB, 1023 is B):'); if (test('getSessionSize returns KB at exactly 1024 bytes and B at 1023', () => { diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index 69ab1b84..74695e4c 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -1629,6 +1629,29 @@ function runTests() { } })) passed++; else failed++; + // ── Round 109: appendFile creating new file in non-existent directory (ensureDir + appendFileSync) ── + console.log('\nRound 109: appendFile (new file creation — ensureDir creates parent, appendFileSync creates file):'); + if (test('appendFile creates parent directory and new file when neither exist', () => { + const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r109-append-new-')); + const nestedPath = path.join(tmpDir, 'deep', 'nested', 'dir', 'newfile.txt'); + try { + // Parent directory 'deep/nested/dir' does not exist yet + assert.ok(!fs.existsSync(path.join(tmpDir, 'deep')), + 'Parent "deep" should not exist before appendFile'); + utils.appendFile(nestedPath, 'first line\n'); + assert.ok(fs.existsSync(nestedPath), + 'File should be created by appendFile'); + assert.strictEqual(utils.readFile(nestedPath), 'first line\n', + 'Content should match what was appended'); + // Append again to verify it adds to existing file + utils.appendFile(nestedPath, 'second line\n'); + assert.strictEqual(utils.readFile(nestedPath), 'first line\nsecond line\n', + 'Second append should add to existing file'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // ── Round 108: grepFile with Unicode/emoji content — UTF-16 string matching on split lines ── console.log('\nRound 108: grepFile (Unicode/emoji — regex matching on UTF-16 split lines):'); if (test('grepFile finds Unicode emoji patterns across lines', () => {