From 332d0f444bb72e24285b78a82b0dca187984d34d Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 16:45:47 -0800 Subject: [PATCH] test: add Round 104 edge-case tests (detectFromLockFile null, resolveSessionAlias traversal, whitespace notes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detectFromLockFile(null): throws TypeError — no input validation before path.join (package-manager.js:95) - resolveSessionAlias('../etc/passwd'): returns path-traversal input unchanged when alias lookup fails, documenting the passthrough behavior - parseSessionMetadata with whitespace-only notes: trim() → "" → hasNotes=false, whitespace-only notes treated as absent Total tests: 872 (all passing) --- tests/lib/package-manager.test.js | 14 ++++++++++++++ tests/lib/session-aliases.test.js | 20 ++++++++++++++++++++ tests/lib/session-manager.test.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index c321c5ec..409cbb6a 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -1455,6 +1455,20 @@ function runTests() { } })) passed++; else failed++; + // ── Round 104: detectFromLockFile with null projectDir (no input validation) ── + console.log('\nRound 104: detectFromLockFile (null projectDir — throws TypeError):'); + if (test('detectFromLockFile(null) throws TypeError (path.join rejects null)', () => { + // package-manager.js line 95: `path.join(projectDir, pm.lockFile)` — there is no + // guard checking that projectDir is a string before passing it to path.join(). + // When projectDir is null, path.join(null, 'package-lock.json') throws a TypeError + // because path.join only accepts string arguments. + assert.throws( + () => pm.detectFromLockFile(null), + { name: 'TypeError' }, + 'path.join(null, ...) should throw TypeError (no input validation in detectFromLockFile)' + ); + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index 58df696b..daadfb92 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -1355,6 +1355,26 @@ function runTests() { 'Object.keys of array returns numeric string indices, not named alias keys'); })) passed++; else failed++; + // ── Round 104: resolveSessionAlias with path-traversal input (passthrough without validation) ── + console.log('\nRound 104: resolveSessionAlias (path-traversal input — returned unchanged):'); + if (test('resolveSessionAlias returns path-traversal input as-is when alias lookup fails', () => { + // session-aliases.js lines 365-374: resolveSessionAlias first tries resolveAlias(), + // which rejects '../etc/passwd' because the regex /^[a-zA-Z0-9_-]+$/ fails on dots + // and slashes (returns null). Then the function falls through to line 373: + // `return aliasOrId` — returning the potentially dangerous input unchanged. + // Callers that blindly use this return value could be at risk. + resetAliases(); + const traversal = '../etc/passwd'; + const result = aliases.resolveSessionAlias(traversal); + assert.strictEqual(result, traversal, + 'Path-traversal input should be returned as-is (resolveAlias rejects it, fallback returns input)'); + // Also test with another invalid alias pattern + const dotSlash = './../../secrets'; + const result2 = aliases.resolveSessionAlias(dotSlash); + assert.strictEqual(result2, dotSlash, + 'Another path-traversal pattern also returned unchanged'); + })) 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 2dc5141a..57127378 100644 --- a/tests/lib/session-manager.test.js +++ b/tests/lib/session-manager.test.js @@ -1689,6 +1689,34 @@ src/main.ts 'Second unchecked item'); })) passed++; else failed++; + // ── Round 104: parseSessionMetadata with whitespace-only notes section ── + console.log('\nRound 104: parseSessionMetadata (whitespace-only notes — trim reduces to empty):'); + if (test('parseSessionMetadata treats whitespace-only notes as absent (trim → empty string → falsy)', () => { + // session-manager.js line 139: `metadata.notes = notesSection[1].trim()` — when the + // Notes section heading exists but only contains whitespace/newlines, trim() returns "". + // Then getSessionStats line 178: `hasNotes: !!metadata.notes` — `!!""` is `false`. + // So a notes section with only whitespace is treated as "no notes." + const content = `# Session + +### Notes for Next Session + \t + +### Context to Load +\`\`\` +file.ts +\`\`\` +`; + const meta = sessionManager.parseSessionMetadata(content); + assert.strictEqual(meta.notes, '', + 'Whitespace-only notes should trim to empty string'); + // Verify getSessionStats reports hasNotes as false + const stats = sessionManager.getSessionStats(content); + assert.strictEqual(stats.hasNotes, false, + 'hasNotes should be false because !!"" is false (whitespace-only notes treated as absent)'); + assert.strictEqual(stats.hasContext, true, + 'hasContext should be true (context section has actual content)'); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);