mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 03:43:30 +08:00
fix: fold session manager blockers into one candidate
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
* Covers the ECC root resolution fallback chain:
|
||||
* 1. CLAUDE_PLUGIN_ROOT env var
|
||||
* 2. Standard install (~/.claude/)
|
||||
* 3. Plugin cache auto-detection
|
||||
* 4. Fallback to ~/.claude/
|
||||
* 3. Exact legacy plugin roots under ~/.claude/plugins/
|
||||
* 4. Plugin cache auto-detection
|
||||
* 5. Fallback to ~/.claude/
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
@@ -39,6 +40,13 @@ function setupStandardInstall(homeDir) {
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
function setupLegacyPluginInstall(homeDir, segments) {
|
||||
const legacyDir = path.join(homeDir, '.claude', 'plugins', ...segments);
|
||||
const scriptDir = path.join(legacyDir, 'scripts', 'lib');
|
||||
fs.mkdirSync(scriptDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
|
||||
return legacyDir;
|
||||
}
|
||||
function setupPluginCache(homeDir, orgName, version) {
|
||||
const cacheDir = path.join(
|
||||
homeDir, '.claude', 'plugins', 'cache',
|
||||
@@ -103,6 +111,50 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds exact legacy plugin install at ~/.claude/plugins/everything-claude-code', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupLegacyPluginInstall(homeDir, ['everything-claude-code']);
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds exact legacy plugin install at ~/.claude/plugins/everything-claude-code@everything-claude-code', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupLegacyPluginInstall(homeDir, ['everything-claude-code@everything-claude-code']);
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds marketplace legacy plugin install at ~/.claude/plugins/marketplace/everything-claude-code', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'everything-claude-code']);
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('prefers exact legacy plugin install over plugin cache', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'everything-claude-code']);
|
||||
setupPluginCache(homeDir, 'everything-claude-code', '1.8.0');
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
// ─── Plugin Cache Auto-Detection ───
|
||||
|
||||
if (test('discovers plugin root from cache directory', () => {
|
||||
@@ -207,6 +259,22 @@ function runTests() {
|
||||
assert.strictEqual(result, '/inline/test/root');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('INLINE_RESOLVE discovers exact legacy plugin root when env var is unset', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupLegacyPluginInstall(homeDir, ['marketplace', 'everything-claude-code']);
|
||||
const { execFileSync } = require('child_process');
|
||||
const result = execFileSync('node', [
|
||||
'-e', `console.log(${INLINE_RESOLVE})`,
|
||||
], {
|
||||
env: { PATH: process.env.PATH, HOME: homeDir, USERPROFILE: homeDir },
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
if (test('INLINE_RESOLVE discovers plugin cache when env var is unset', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
|
||||
@@ -990,7 +990,7 @@ src/main.ts
|
||||
assert.ok(result.endsWith(filename), `Path should end with filename, got: ${result}`);
|
||||
// Since HOME is overridden, sessions dir should be under tmpHome
|
||||
assert.ok(result.includes('.claude'), 'Path should include .claude directory');
|
||||
assert.ok(result.includes('sessions'), 'Path should include sessions directory');
|
||||
assert.ok(result.includes('session-data'), 'Path should use canonical session-data directory');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 66: getSessionById noIdMatch path (date-only string for old format) ──
|
||||
@@ -1629,18 +1629,13 @@ src/main.ts
|
||||
// best-effort
|
||||
}
|
||||
|
||||
// ── Round 98: parseSessionFilename with null input throws TypeError ──
|
||||
console.log('\nRound 98: parseSessionFilename (null input — crashes at line 30):');
|
||||
// ── Round 98: parseSessionFilename with null input returns null ──
|
||||
console.log('\nRound 98: parseSessionFilename (null input is safely rejected):');
|
||||
|
||||
if (test('parseSessionFilename(null) throws TypeError because null has no .match()', () => {
|
||||
// session-manager.js line 30: `filename.match(SESSION_FILENAME_REGEX)`
|
||||
// When filename is null, null.match() throws TypeError.
|
||||
// Function lacks a type guard like `if (!filename || typeof filename !== 'string')`.
|
||||
assert.throws(
|
||||
() => sessionManager.parseSessionFilename(null),
|
||||
{ name: 'TypeError' },
|
||||
'null.match() should throw TypeError (no type guard on filename parameter)'
|
||||
);
|
||||
if (test('parseSessionFilename(null) returns null instead of throwing', () => {
|
||||
assert.strictEqual(sessionManager.parseSessionFilename(null), null);
|
||||
assert.strictEqual(sessionManager.parseSessionFilename(undefined), null);
|
||||
assert.strictEqual(sessionManager.parseSessionFilename(123), null);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 99: writeSessionContent with null path returns false (error caught) ──
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
// Import the module
|
||||
const utils = require('../../scripts/lib/utils');
|
||||
@@ -68,7 +69,13 @@ function runTests() {
|
||||
const sessionsDir = utils.getSessionsDir();
|
||||
const claudeDir = utils.getClaudeDir();
|
||||
assert.ok(sessionsDir.startsWith(claudeDir), 'Sessions should be under Claude dir');
|
||||
assert.ok(sessionsDir.includes('sessions'), 'Should contain sessions');
|
||||
assert.ok(sessionsDir.endsWith(path.join('.claude', 'session-data')) || sessionsDir.endsWith('/.claude/session-data'), 'Should use canonical session-data directory');
|
||||
})) passed++; else failed++;
|
||||
|
||||
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');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getTempDir returns valid temp directory', () => {
|
||||
@@ -118,17 +125,77 @@ function runTests() {
|
||||
assert.ok(name && name.length > 0);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// sanitizeSessionId tests
|
||||
console.log('\nsanitizeSessionId:');
|
||||
|
||||
if (test('sanitizeSessionId strips leading dots', () => {
|
||||
assert.strictEqual(utils.sanitizeSessionId('.claude'), 'claude');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId replaces dots and spaces', () => {
|
||||
assert.strictEqual(utils.sanitizeSessionId('my.project'), 'my-project');
|
||||
assert.strictEqual(utils.sanitizeSessionId('my project'), 'my-project');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId replaces special chars and collapses runs', () => {
|
||||
assert.strictEqual(utils.sanitizeSessionId('project@v2'), 'project-v2');
|
||||
assert.strictEqual(utils.sanitizeSessionId('a...b'), 'a-b');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId preserves valid chars', () => {
|
||||
assert.strictEqual(utils.sanitizeSessionId('my-project_123'), 'my-project_123');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId returns null for empty or punctuation-only values', () => {
|
||||
assert.strictEqual(utils.sanitizeSessionId(''), null);
|
||||
assert.strictEqual(utils.sanitizeSessionId(null), null);
|
||||
assert.strictEqual(utils.sanitizeSessionId(undefined), null);
|
||||
assert.strictEqual(utils.sanitizeSessionId('...'), null);
|
||||
assert.strictEqual(utils.sanitizeSessionId('…'), null);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId returns stable hashes for non-ASCII values', () => {
|
||||
const chinese = utils.sanitizeSessionId('我的项目');
|
||||
const cyrillic = utils.sanitizeSessionId('проект');
|
||||
const emoji = utils.sanitizeSessionId('🚀🎉');
|
||||
assert.ok(/^[a-f0-9]{8}$/.test(chinese), `Expected 8-char hash, got: ${chinese}`);
|
||||
assert.ok(/^[a-f0-9]{8}$/.test(cyrillic), `Expected 8-char hash, got: ${cyrillic}`);
|
||||
assert.ok(/^[a-f0-9]{8}$/.test(emoji), `Expected 8-char hash, got: ${emoji}`);
|
||||
assert.notStrictEqual(chinese, cyrillic);
|
||||
assert.notStrictEqual(chinese, emoji);
|
||||
assert.strictEqual(utils.sanitizeSessionId('日本語プロジェクト'), utils.sanitizeSessionId('日本語プロジェクト'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId disambiguates mixed-script names from pure ASCII', () => {
|
||||
const mixed = utils.sanitizeSessionId('我的app');
|
||||
const mixedTwo = utils.sanitizeSessionId('他的app');
|
||||
const pure = utils.sanitizeSessionId('app');
|
||||
assert.strictEqual(pure, 'app');
|
||||
assert.ok(mixed.startsWith('app-'), `Expected mixed-script prefix, got: ${mixed}`);
|
||||
assert.notStrictEqual(mixed, pure);
|
||||
assert.notStrictEqual(mixed, mixedTwo);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId is idempotent', () => {
|
||||
for (const input of ['.claude', 'my.project', 'project@v2', 'a...b', 'my-project_123']) {
|
||||
const once = utils.sanitizeSessionId(input);
|
||||
const twice = utils.sanitizeSessionId(once);
|
||||
assert.strictEqual(once, twice, `Expected idempotent result for ${input}`);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Session ID tests
|
||||
console.log('\nSession ID Functions:');
|
||||
|
||||
if (test('getSessionIdShort falls back to project name', () => {
|
||||
if (test('getSessionIdShort falls back to sanitized project name', () => {
|
||||
const original = process.env.CLAUDE_SESSION_ID;
|
||||
delete process.env.CLAUDE_SESSION_ID;
|
||||
try {
|
||||
const shortId = utils.getSessionIdShort();
|
||||
assert.strictEqual(shortId, utils.getProjectName());
|
||||
assert.strictEqual(shortId, utils.sanitizeSessionId(utils.getProjectName()));
|
||||
} finally {
|
||||
if (original) process.env.CLAUDE_SESSION_ID = original;
|
||||
if (original !== undefined) process.env.CLAUDE_SESSION_ID = original;
|
||||
else delete process.env.CLAUDE_SESSION_ID;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -154,6 +221,28 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getSessionIdShort sanitizes explicit fallback parameter', () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — root CWD differs on Windows)');
|
||||
return true;
|
||||
}
|
||||
|
||||
const utilsPath = path.join(__dirname, '..', '..', 'scripts', 'lib', 'utils.js');
|
||||
const script = `
|
||||
const utils = require('${utilsPath.replace(/'/g, "\\'")}');
|
||||
process.stdout.write(utils.getSessionIdShort('my.fallback'));
|
||||
`;
|
||||
const result = spawnSync('node', ['-e', script], {
|
||||
encoding: 'utf8',
|
||||
cwd: '/',
|
||||
env: { ...process.env, CLAUDE_SESSION_ID: '' },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
assert.strictEqual(result.status, 0, `Expected exit 0, got ${result.status}. stderr: ${result.stderr}`);
|
||||
assert.strictEqual(result.stdout, 'my-fallback');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// File operations tests
|
||||
console.log('\nFile Operations:');
|
||||
|
||||
@@ -1415,25 +1504,26 @@ function runTests() {
|
||||
// ── Round 97: getSessionIdShort with whitespace-only CLAUDE_SESSION_ID ──
|
||||
console.log('\nRound 97: getSessionIdShort (whitespace-only session ID):');
|
||||
|
||||
if (test('getSessionIdShort returns whitespace when CLAUDE_SESSION_ID is all spaces', () => {
|
||||
// utils.js line 116: if (sessionId && sessionId.length > 0) — ' ' is truthy
|
||||
// and has length > 0, so it passes the check instead of falling back.
|
||||
const original = process.env.CLAUDE_SESSION_ID;
|
||||
try {
|
||||
process.env.CLAUDE_SESSION_ID = ' '; // 10 spaces
|
||||
const result = utils.getSessionIdShort('fallback');
|
||||
// slice(-8) on 10 spaces returns 8 spaces — not the expected fallback
|
||||
assert.strictEqual(result, ' ',
|
||||
'Whitespace-only ID should return 8 trailing spaces (no trim check)');
|
||||
assert.strictEqual(result.trim().length, 0,
|
||||
'Result should be entirely whitespace (demonstrating the missing trim)');
|
||||
} finally {
|
||||
if (original !== undefined) {
|
||||
process.env.CLAUDE_SESSION_ID = original;
|
||||
} else {
|
||||
delete process.env.CLAUDE_SESSION_ID;
|
||||
}
|
||||
if (test('getSessionIdShort sanitizes whitespace-only CLAUDE_SESSION_ID to fallback', () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log(' (skipped — root CWD differs on Windows)');
|
||||
return true;
|
||||
}
|
||||
|
||||
const utilsPath = path.join(__dirname, '..', '..', 'scripts', 'lib', 'utils.js');
|
||||
const script = `
|
||||
const utils = require('${utilsPath.replace(/'/g, "\\'")}');
|
||||
process.stdout.write(utils.getSessionIdShort('fallback'));
|
||||
`;
|
||||
const result = spawnSync('node', ['-e', script], {
|
||||
encoding: 'utf8',
|
||||
cwd: '/',
|
||||
env: { ...process.env, CLAUDE_SESSION_ID: ' ' },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
assert.strictEqual(result.status, 0, `Expected exit 0, got ${result.status}. stderr: ${result.stderr}`);
|
||||
assert.strictEqual(result.stdout, 'fallback');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 97: countInFile with same RegExp object called twice (lastIndex reuse) ──
|
||||
|
||||
Reference in New Issue
Block a user