mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-02 23:23:31 +08:00
fix: harden session hook guards and session ID handling
This commit is contained in:
@@ -35,7 +35,7 @@ function runHook(input, env = {}) {
|
||||
});
|
||||
|
||||
return {
|
||||
code: result.status ?? 0,
|
||||
code: Number.isInteger(result.status) ? result.status : 1,
|
||||
stdout: result.stdout || '',
|
||||
stderr: result.stderr || ''
|
||||
};
|
||||
@@ -98,4 +98,4 @@ function runTests() {
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
runTests();
|
||||
|
||||
@@ -91,10 +91,7 @@ function getLegacySessionsDir(homeDir) {
|
||||
}
|
||||
|
||||
function getSessionStartAdditionalContext(stdout) {
|
||||
if (!stdout.trim()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
assert.ok(stdout.trim(), 'Expected SessionStart hook to emit stdout payload');
|
||||
const payload = JSON.parse(stdout);
|
||||
assert.strictEqual(payload.hookSpecificOutput?.hookEventName, 'SessionStart', 'Should emit SessionStart hook payload');
|
||||
assert.strictEqual(typeof payload.hookSpecificOutput?.additionalContext, 'string', 'Should include additionalContext text');
|
||||
@@ -435,6 +432,42 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('prefers canonical session-data content over legacy duplicates', async () => {
|
||||
const isoHome = path.join(os.tmpdir(), `ecc-canonical-start-${Date.now()}`);
|
||||
const canonicalDir = getCanonicalSessionsDir(isoHome);
|
||||
const legacyDir = getLegacySessionsDir(isoHome);
|
||||
const filename = '2026-02-11-dupe1234-session.tmp';
|
||||
const canonicalFile = path.join(canonicalDir, filename);
|
||||
const legacyFile = path.join(legacyDir, filename);
|
||||
const sameTime = new Date('2026-02-11T12:00:00Z');
|
||||
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(canonicalFile, '# Canonical Session\n\nUse the canonical session-data copy.\n');
|
||||
fs.writeFileSync(legacyFile, '# Legacy Session\n\nDo not prefer the legacy duplicate.\n');
|
||||
fs.utimesSync(canonicalFile, sameTime, sameTime);
|
||||
fs.utimesSync(legacyFile, sameTime, sameTime);
|
||||
|
||||
try {
|
||||
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||
HOME: isoHome,
|
||||
USERPROFILE: isoHome
|
||||
});
|
||||
assert.strictEqual(result.code, 0);
|
||||
const additionalContext = getSessionStartAdditionalContext(result.stdout);
|
||||
assert.ok(additionalContext.includes('canonical session-data copy'));
|
||||
assert.ok(!additionalContext.includes('legacy duplicate'));
|
||||
} finally {
|
||||
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('strips ANSI escape codes from injected session content', async () => {
|
||||
const isoHome = path.join(os.tmpdir(), `ecc-ansi-start-${Date.now()}`);
|
||||
@@ -1924,6 +1957,8 @@ async function runTests() {
|
||||
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code@everything-claude-code'"), 'Should probe the namespaced legacy plugin root');
|
||||
assert.ok(sessionStartHook.command.includes("plugins','marketplace','everything-claude-code'"), 'Should probe the marketplace legacy plugin root');
|
||||
assert.ok(sessionStartHook.command.includes("plugins','cache','everything-claude-code'"), 'Should retain cache lookup fallback');
|
||||
assert.ok(sessionStartHook.command.includes('if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim())'), 'Should validate CLAUDE_PLUGIN_ROOT before trusting it');
|
||||
assert.ok(sessionStartHook.command.includes('else process.stdout.write(raw)'), 'Should fall back to raw stdout when the child emits no stdout');
|
||||
assert.ok(!sessionStartHook.command.includes('find '), 'Should not scan arbitrary plugin paths with find');
|
||||
assert.ok(!sessionStartHook.command.includes('head -n 1'), 'Should not pick the first matching plugin path');
|
||||
})
|
||||
|
||||
@@ -91,11 +91,10 @@ function runHookWithInput(scriptPath, input = {}, env = {}, timeoutMs = 10000) {
|
||||
}
|
||||
|
||||
function getSessionStartPayload(stdout) {
|
||||
if (!stdout.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(stdout);
|
||||
assert.ok(stdout.trim(), 'Expected SessionStart hook to emit stdout payload');
|
||||
const payload = JSON.parse(stdout);
|
||||
assert.strictEqual(payload.hookSpecificOutput?.hookEventName, 'SessionStart');
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,10 +261,9 @@ async function runTests() {
|
||||
// Session-start should write info to stderr
|
||||
assert.ok(result.stderr.length > 0, 'Should have stderr output');
|
||||
assert.ok(result.stderr.includes('[SessionStart]'), 'Should have [SessionStart] prefix');
|
||||
if (result.stdout.trim()) {
|
||||
const payload = getSessionStartPayload(result.stdout);
|
||||
assert.strictEqual(payload.hookSpecificOutput?.hookEventName, 'SessionStart');
|
||||
}
|
||||
const payload = getSessionStartPayload(result.stdout);
|
||||
assert.ok(payload.hookSpecificOutput, 'Should include hookSpecificOutput');
|
||||
assert.strictEqual(payload.hookSpecificOutput.hookEventName, 'SessionStart');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('PreCompact hook logs to stderr', async () => {
|
||||
|
||||
@@ -477,6 +477,12 @@ src/main.ts
|
||||
assert.strictEqual(result, null, 'Empty string should not match any session');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getSessionById returns null for non-string IDs', () => {
|
||||
assert.strictEqual(sessionManager.getSessionById(null), null);
|
||||
assert.strictEqual(sessionManager.getSessionById(undefined), null);
|
||||
assert.strictEqual(sessionManager.getSessionById(42), null);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getSessionById metadata and stats populated when includeContent=true', () => {
|
||||
const result = sessionManager.getSessionById('abcd1234', true);
|
||||
assert.ok(result, 'Should find session');
|
||||
@@ -1601,18 +1607,13 @@ src/main.ts
|
||||
'Null search should return sessions (confirming they exist but space filtered them)');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 98: getSessionById with null sessionId throws TypeError ──
|
||||
console.log('\nRound 98: getSessionById (null sessionId — crashes at line 297):');
|
||||
// ── Round 98: getSessionById with null sessionId returns null ──
|
||||
console.log('\nRound 98: getSessionById (null sessionId — guarded null return):');
|
||||
|
||||
if (test('getSessionById(null) throws TypeError when session files exist', () => {
|
||||
// session-manager.js line 297: `sessionId.length > 0` — calling .length on null
|
||||
// throws TypeError because there's no early guard for null/undefined input.
|
||||
// This only surfaces when valid .tmp files exist in the sessions directory.
|
||||
assert.throws(
|
||||
() => sessionManager.getSessionById(null),
|
||||
{ name: 'TypeError' },
|
||||
'null.length should throw TypeError (no input guard at function entry)'
|
||||
);
|
||||
if (test('getSessionById(null) returns null when session files exist', () => {
|
||||
// Keep a populated sessions directory so the early input guard is exercised even when
|
||||
// candidate files are present.
|
||||
assert.strictEqual(sessionManager.getSessionById(null), null);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup test environment for Rounds 95-98 that needed sessions
|
||||
|
||||
@@ -74,8 +74,8 @@ function runTests() {
|
||||
|
||||
if (test('getSessionSearchDirs includes canonical and legacy paths', () => {
|
||||
const searchDirs = utils.getSessionSearchDirs();
|
||||
assert.ok(searchDirs.includes(utils.getSessionsDir()), 'Should include canonical session dir');
|
||||
assert.ok(searchDirs.includes(utils.getLegacySessionsDir()), 'Should include legacy session dir');
|
||||
assert.strictEqual(searchDirs[0], utils.getSessionsDir(), 'Canonical session dir should be searched first');
|
||||
assert.strictEqual(searchDirs[1], utils.getLegacySessionsDir(), 'Legacy session dir should be searched second');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getTempDir returns valid temp directory', () => {
|
||||
@@ -184,6 +184,14 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId avoids Windows reserved device names', () => {
|
||||
const con = utils.sanitizeSessionId('CON');
|
||||
const aux = utils.sanitizeSessionId('aux');
|
||||
assert.ok(con.startsWith('CON-'), `Expected CON to get a suffix, got: ${con}`);
|
||||
assert.ok(aux.startsWith('aux-'), `Expected aux to get a suffix, got: ${aux}`);
|
||||
assert.notStrictEqual(utils.sanitizeSessionId('COM1'), 'COM1');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Session ID tests
|
||||
console.log('\nSession ID Functions:');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user