From a95fb54ee4ef5c93fc35e9d7812d8f4bd4876163 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 14:44:40 -0800 Subject: [PATCH] test: add 3 tests for scoped pkg detection, empty env var, and tools-without-files (Round 94) - detectFromPackageJson with scoped package name (@scope/pkg@version) returns null because split('@')[0] yields empty string - getPackageManager skips empty string CLAUDE_PACKAGE_MANAGER via falsy short-circuit (distinct from unknown PM name test) - session-end buildSummarySection includes Tools Used but omits Files Modified when transcript has only Read/Grep tools Total tests: 842 --- tests/hooks/hooks.test.js | 43 +++++++++++++++++++++++++++++++ tests/lib/package-manager.test.js | 43 +++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 8ca5be8e..8e24bac9 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -3621,6 +3621,49 @@ Some random content without the expected ### Context to Load section }); })) passed++; else failed++; + // ── Round 94: session-end.js tools used but no files modified ── + console.log('\nRound 94: session-end.js (tools used without files modified):'); + + if (await asyncTest('session file includes Tools Used but omits Files Modified when only Read/Grep used', async () => { + // session-end.js buildSummarySection (lines 217-228): + // filesModified.length > 0 → include "### Files Modified" section + // toolsUsed.length > 0 → include "### Tools Used" section + // Previously tested: BOTH present (Round ~10) and NEITHER present (Round ~10). + // Untested combination: toolsUsed present, filesModified empty. + // Transcript with Read/Grep tools (don't add to filesModified) and user messages. + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":"Search the codebase for auth handlers"}', + '{"type":"tool_use","tool_name":"Read","tool_input":{"file_path":"/src/auth.ts"}}', + '{"type":"tool_use","tool_name":"Grep","tool_input":{"pattern":"handler"}}', + '{"type":"user","content":"Check the test file too"}', + '{"type":"tool_use","tool_name":"Read","tool_input":{"file_path":"/tests/auth.test.ts"}}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson, { + HOME: testDir + }); + assert.strictEqual(result.code, 0, 'Should exit 0'); + + const claudeDir = path.join(testDir, '.claude', 'sessions'); + if (fs.existsSync(claudeDir)) { + const files = fs.readdirSync(claudeDir).filter(f => f.endsWith('.tmp')); + if (files.length > 0) { + const content = fs.readFileSync(path.join(claudeDir, files[0]), 'utf8'); + assert.ok(content.includes('### Tools Used'), 'Should include Tools Used section'); + assert.ok(content.includes('Read'), 'Should list Read tool'); + assert.ok(content.includes('Grep'), 'Should list Grep tool'); + assert.ok(!content.includes('### Files Modified'), + 'Should NOT include Files Modified section (Read/Grep do not modify files)'); + } + } + cleanupTestDir(testDir); + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index 2aca8cfc..c321c5ec 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -1412,6 +1412,49 @@ function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + // ── Round 94: detectFromPackageJson with scoped package name ── + console.log('\nRound 94: detectFromPackageJson (scoped package name @scope/pkg@version):'); + + if (test('detectFromPackageJson returns null for scoped package name (@scope/pkg@version)', () => { + // package-manager.js line 116: pmName = pkg.packageManager.split('@')[0] + // For "@pnpm/exe@8.0.0", split('@') → ['', 'pnpm/exe', '8.0.0'], so [0] = '' + // PACKAGE_MANAGERS[''] is undefined → returns null. + // Scoped npm packages like @pnpm/exe are a real-world pattern but the + // packageManager field spec uses unscoped names (e.g., "pnpm@8"), so returning + // null is the correct defensive behaviour for this edge case. + const testDir = createTestDir(); + fs.writeFileSync( + path.join(testDir, 'package.json'), + JSON.stringify({ name: 'test', packageManager: '@pnpm/exe@8.0.0' })); + const result = pm.detectFromPackageJson(testDir); + assert.strictEqual(result, null, + 'Scoped package name should return null (split("@")[0] is empty string)'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + // ── Round 94: getPackageManager with empty string CLAUDE_PACKAGE_MANAGER ── + console.log('\nRound 94: getPackageManager (empty string CLAUDE_PACKAGE_MANAGER env var):'); + + if (test('getPackageManager skips empty string CLAUDE_PACKAGE_MANAGER (falsy short-circuit)', () => { + // package-manager.js line 168: if (envPm && PACKAGE_MANAGERS[envPm]) + // Empty string '' is falsy — the && short-circuits before checking PACKAGE_MANAGERS. + // This is distinct from the 'totally-fake-pm' test (truthy but unknown PM). + const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; + try { + process.env.CLAUDE_PACKAGE_MANAGER = ''; + const result = pm.getPackageManager(); + assert.notStrictEqual(result.source, 'environment', + 'Empty string env var should NOT be treated as environment source'); + assert.ok(result.name, 'Should still return a valid package manager name'); + } finally { + if (originalEnv !== undefined) { + process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; + } else { + delete process.env.CLAUDE_PACKAGE_MANAGER; + } + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`);