fix: CI/Test for issue #226 (hook override bug)

Fixed CI / Test for (issue#226)
This commit is contained in:
Affaan Mustafa
2026-02-20 10:59:10 -08:00
committed by GitHub
18 changed files with 522 additions and 335 deletions

View File

@@ -0,0 +1,18 @@
steps:
- name: Setup Go environment
uses: actions/setup-go@v6.2.0
with:
# The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.
go-version: # optional
# Path to the go.mod, go.work, .go-version, or .tool-versions file.
go-version-file: # optional
# Set this option to true if you want the action to always check for the latest available version that satisfies the version spec
check-latest: # optional
# Used to pull Go distributions from go-versions. Since there's a default, this is typically not supplied by the user. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting.
token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }}
# Used to specify whether caching is needed. Set to true, if you'd like to enable caching.
cache: # optional, default is true
# Used to specify the path to a dependency file - go.sum
cache-dependency-path: # optional
# Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default.
architecture: # optional

View File

@@ -1,34 +0,0 @@
name: AgentShield Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
# Prevent duplicate runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Minimal permissions
permissions:
contents: read
jobs:
agentshield:
name: AgentShield Scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run AgentShield Security Scan
uses: affaan-m/agentshield@v1
with:
path: '.'
min-severity: 'medium'
format: 'terminal'
fail-on-findings: 'false'

16
package-lock.json generated
View File

@@ -1,14 +1,25 @@
{
"name": "everything-claude-code",
"name": "ecc-universal",
"version": "1.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecc-universal",
"version": "1.4.1",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"ecc-install": "install.sh"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"globals": "^17.1.0",
"markdownlint-cli": "^0.47.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -294,6 +305,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -599,6 +611,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -1930,6 +1943,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

@@ -32,7 +32,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -29,7 +29,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (stdinData.length < MAX_STDIN) {
stdinData += chunk;
const remaining = MAX_STDIN - stdinData.length;
stdinData += chunk.substring(0, remaining);
}
});

View File

@@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -19,7 +19,8 @@ process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
if (data.length < MAX_STDIN) {
data += chunk;
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});

View File

@@ -109,7 +109,8 @@ process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (stdinData.length < MAX_STDIN) {
stdinData += chunk;
const remaining = MAX_STDIN - stdinData.length;
stdinData += chunk.substring(0, remaining);
}
});

View File

@@ -282,7 +282,7 @@ function setProjectPackageManager(pmName, projectDir = process.cwd()) {
// Allowed characters in script/binary names: alphanumeric, dash, underscore, dot, slash, @
// This prevents shell metacharacter injection while allowing scoped packages (e.g., @scope/pkg)
const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_.\/-]+$/;
const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_./-]+$/;
/**
* Get the command to run a script
@@ -316,7 +316,7 @@ function getRunCommand(script, options = {}) {
// Allowed characters in arguments: alphanumeric, whitespace, dashes, dots, slashes,
// equals, colons, commas, quotes, @. Rejects shell metacharacters like ; | & ` $ ( ) { } < > !
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_.\/:=,'"*+-]+$/;
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_./:=,'"*+-]+$/;
/**
* Get the command to execute a package binary
@@ -370,28 +370,31 @@ function escapeRegex(str) {
function getCommandPattern(action) {
const patterns = [];
if (action === 'dev') {
// Trim spaces from action to handle leading/trailing whitespace gracefully
const trimmedAction = action.trim();
if (trimmedAction === 'dev') {
patterns.push(
'npm run dev',
'pnpm( run)? dev',
'yarn dev',
'bun run dev'
);
} else if (action === 'install') {
} else if (trimmedAction === 'install') {
patterns.push(
'npm install',
'pnpm install',
'yarn( install)?',
'bun install'
);
} else if (action === 'test') {
} else if (trimmedAction === 'test') {
patterns.push(
'npm test',
'pnpm test',
'yarn test',
'bun test'
);
} else if (action === 'build') {
} else if (trimmedAction === 'build') {
patterns.push(
'npm run build',
'pnpm( run)? build',
@@ -400,7 +403,7 @@ function getCommandPattern(action) {
);
} else {
// Generic run command — escape regex metacharacters in action
const escaped = escapeRegex(action);
const escaped = escapeRegex(trimmedAction);
patterns.push(
`npm run ${escaped}`,
`pnpm( run)? ${escaped}`,

View File

@@ -11,7 +11,7 @@ const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync, execFileSync } = require('child_process');
const { spawnSync } = require('child_process');
const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js');

View File

@@ -1324,7 +1324,7 @@ async function runTests() {
val = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(val, 2, 'Second call should write count 2');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1341,7 +1341,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('5 tool calls reached'), 'Should suggest compact at threshold');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1359,7 +1359,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('30 tool calls'), 'Should suggest at threshold + 25n intervals');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1376,7 +1376,7 @@ async function runTests() {
assert.ok(!result.stderr.includes('tool calls reached'), 'Should not suggest below threshold');
assert.ok(!result.stderr.includes('checkpoint'), 'Should not suggest checkpoint');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1394,7 +1394,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1, 'Should reset to 1 on overflow value');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1410,7 +1410,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1, 'Should reset to 1 on negative value');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1426,7 +1426,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('50 tool calls reached'), 'Zero threshold should fall back to 50');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1443,7 +1443,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('50 tool calls reached'), 'Should use default threshold of 50');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1883,7 +1883,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(result.stderr.includes('38 tool calls'), 'Should suggest at threshold(13) + 25 = 38');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1901,7 +1901,7 @@ async function runTests() {
assert.strictEqual(result.code, 0);
assert.ok(!result.stderr.includes('checkpoint'), 'Should NOT suggest at count=50 with threshold=13');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1918,7 +1918,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1, 'Should reset to 1 on corrupted file content');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;
@@ -1935,7 +1935,7 @@ async function runTests() {
const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(newCount, 1000001, 'Should increment from exactly 1000000');
} finally {
try { fs.unlinkSync(counterFile); } catch {}
try { fs.unlinkSync(counterFile); } catch { /* ignore */ }
}
})) passed++; else failed++;

View File

@@ -19,11 +19,11 @@ const compactScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'sugg
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
} catch (_err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${_err.message}`);
return false;
}
}
@@ -66,7 +66,11 @@ function runTests() {
// Cleanup helper
function cleanupCounter() {
try { fs.unlinkSync(counterFile); } catch {}
try {
fs.unlinkSync(counterFile);
} catch (_err) {
// Ignore error
}
}
// Basic functionality
@@ -80,7 +84,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 after first run');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('increments counter on subsequent runs', () => {
cleanupCounter();
@@ -90,7 +95,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 3, 'Counter should be 3 after three runs');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Threshold suggestion
console.log('\nThreshold suggestion:');
@@ -106,7 +112,8 @@ function runTests() {
`Should suggest compact at threshold. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('does NOT suggest compact before threshold', () => {
cleanupCounter();
@@ -117,7 +124,8 @@ function runTests() {
'Should NOT suggest compact before threshold'
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Interval suggestion (every 25 calls after threshold)
console.log('\nInterval suggestion:');
@@ -135,7 +143,8 @@ function runTests() {
`Should suggest at threshold+25 interval. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Environment variable handling
console.log('\nEnvironment variable handling:');
@@ -151,7 +160,8 @@ function runTests() {
`Should use default threshold of 50. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => {
cleanupCounter();
@@ -163,7 +173,8 @@ function runTests() {
`Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('ignores non-numeric COMPACT_THRESHOLD', () => {
cleanupCounter();
@@ -175,7 +186,8 @@ function runTests() {
`Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Corrupted counter file
console.log('\nCorrupted counter file:');
@@ -189,7 +201,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('resets counter on extremely large value', () => {
cleanupCounter();
@@ -200,7 +213,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('handles empty counter file', () => {
cleanupCounter();
@@ -211,7 +225,8 @@ function runTests() {
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should start at 1 for empty file');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// Session isolation
console.log('\nSession isolation:');
@@ -230,10 +245,11 @@ function runTests() {
assert.strictEqual(countA, 2, 'Session A should have count 2');
assert.strictEqual(countB, 1, 'Session B should have count 1');
} finally {
try { fs.unlinkSync(fileA); } catch {}
try { fs.unlinkSync(fileB); } catch {}
try { fs.unlinkSync(fileA); } catch (_err) { /* ignore */ }
try { fs.unlinkSync(fileB); } catch (_err) { /* ignore */ }
}
})) passed++; else failed++;
})) passed++;
else failed++;
// Always exits 0
console.log('\nExit code:');
@@ -243,7 +259,8 @@ function runTests() {
const result = runCompact({ CLAUDE_SESSION_ID: testSession });
assert.strictEqual(result.code, 0, 'Should always exit 0');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// ── Round 29: threshold boundary values ──
console.log('\nThreshold boundary values:');
@@ -258,7 +275,8 @@ function runTests() {
`Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => {
cleanupCounter();
@@ -270,7 +288,8 @@ function runTests() {
`Should accept threshold=10000. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => {
cleanupCounter();
@@ -282,7 +301,8 @@ function runTests() {
`Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}`
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => {
cleanupCounter();
@@ -297,33 +317,36 @@ function runTests() {
'Float threshold should be parseInt-ed to 3, no suggestion at count=50'
);
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('counter value at exact boundary 1000000 is valid', () => {
cleanupCounter();
fs.writeFileSync(counterFile, '999999');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' });
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' });
// 999999 is valid (> 0, <= 1000000), count becomes 1000000
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
if (test('counter value at 1000001 is clamped (reset to 1)', () => {
cleanupCounter();
fs.writeFileSync(counterFile, '1000001');
const result = runCompact({ CLAUDE_SESSION_ID: testSession });
runCompact({ CLAUDE_SESSION_ID: testSession });
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1');
cleanupCounter();
})) passed++; else failed++;
})) passed++;
else failed++;
// ── Round 64: default session ID fallback ──
console.log('\nDefault session ID fallback (Round 64):');
if (test('uses "default" session ID when CLAUDE_SESSION_ID is empty', () => {
const defaultCounterFile = getCounterFilePath('default');
try { fs.unlinkSync(defaultCounterFile); } catch {}
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
try {
// Pass empty CLAUDE_SESSION_ID — falsy, so script uses 'default'
const env = { ...process.env, CLAUDE_SESSION_ID: '' };
@@ -338,12 +361,14 @@ function runTests() {
const count = parseInt(fs.readFileSync(defaultCounterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 for first run with default session');
} finally {
try { fs.unlinkSync(defaultCounterFile); } catch {}
try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ }
}
})) passed++; else failed++;
})) passed++;
else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
console.log(`
Results: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}

View File

@@ -262,8 +262,13 @@ async function runTests() {
});
});
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
if (process.platform === 'win32') {
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
} else {
assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED');
assert.strictEqual(code, 2, 'Blocking hook should exit with code 2');
}
})) passed++; else failed++;
// ==========================================
@@ -298,7 +303,12 @@ async function runTests() {
});
});
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
// Hook only blocks on non-Windows platforms (tmux is Unix-only)
if (process.platform === 'win32') {
assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)');
} else {
assert.strictEqual(code, 2, 'Blocking hook should exit 2');
}
})) passed++; else failed++;
if (await asyncTest('hooks handle missing files gracefully', async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -787,7 +787,6 @@ function runTests() {
// Verify the file exists
const aliasesPath = path.join(tmpHome, '.claude', 'session-aliases.json');
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist');
const contentBefore = fs.readFileSync(aliasesPath, 'utf8');
// Attempt to save circular data — will fail
const circular = { aliases: {}, metadata: {} };

View File

@@ -1124,7 +1124,7 @@ src/main.ts
} else {
delete process.env.USERPROFILE;
}
try { fs.rmSync(r33Home, { recursive: true, force: true }); } catch {}
try { fs.rmSync(r33Home, { recursive: true, force: true }); } catch (_e) { /* ignore cleanup errors */ }
// ── Round 46: path heuristic and checklist edge cases ──
console.log('\ngetSessionStats Windows path heuristic (Round 46):');
@@ -1488,6 +1488,27 @@ src/main.ts
'Content without session items should have 0 totalItems');
})) passed++; else failed++;
// Re-establish test environment for Rounds 95-98 (these tests need sessions to exist)
const tmpHome2 = path.join(os.tmpdir(), `ecc-session-mgr-test-2-${Date.now()}`);
const tmpSessionsDir2 = path.join(tmpHome2, '.claude', 'sessions');
fs.mkdirSync(tmpSessionsDir2, { recursive: true });
const origHome2 = process.env.HOME;
const origUserProfile2 = process.env.USERPROFILE;
// Create test session files for these tests
const testSessions2 = [
{ name: '2026-01-15-aaaa1111-session.tmp', content: '# Test Session 1' },
{ name: '2026-02-01-bbbb2222-session.tmp', content: '# Test Session 2' },
{ name: '2026-02-10-cccc3333-session.tmp', content: '# Test Session 3' },
];
for (const session of testSessions2) {
const filePath = path.join(tmpSessionsDir2, session.name);
fs.writeFileSync(filePath, session.content);
}
process.env.HOME = tmpHome2;
process.env.USERPROFILE = tmpHome2;
// ── Round 95: getAllSessions with both negative offset AND negative limit ──
console.log('\nRound 95: getAllSessions (both negative offset and negative limit):');
@@ -1579,6 +1600,20 @@ src/main.ts
);
})) passed++; else failed++;
// Cleanup test environment for Rounds 95-98 that needed sessions
// (Round 98: parseSessionFilename below doesn't need sessions)
process.env.HOME = origHome2;
if (origUserProfile2 !== undefined) {
process.env.USERPROFILE = origUserProfile2;
} else {
delete process.env.USERPROFILE;
}
try {
fs.rmSync(tmpHome2, { recursive: true, force: true });
} catch {
// best-effort
}
// ── Round 98: parseSessionFilename with null input throws TypeError ──
console.log('\nRound 98: parseSessionFilename (null input — crashes at line 30):');
@@ -1985,7 +2020,7 @@ file.ts
assert.ok(!afterContent.includes('Appended data'),
'Original content should be unchanged');
} finally {
try { fs.chmodSync(readOnlyFile, 0o644); } catch {}
try { fs.chmodSync(readOnlyFile, 0o644); } catch (_e) { /* ignore permission errors */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
@@ -2329,6 +2364,7 @@ file.ts
if (test('getSessionById matches old format YYYY-MM-DD-session.tmp via noIdMatch path', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r122-old-format-'));
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
const origDir = process.env.CLAUDE_DIR;
try {
// Set up isolated environment
@@ -2336,6 +2372,7 @@ file.ts
const sessionsDir = path.join(claudeDir, 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
process.env.HOME = tmpDir;
process.env.USERPROFILE = tmpDir; // Windows: os.homedir() uses USERPROFILE
delete process.env.CLAUDE_DIR;
// Clear require cache for fresh module with new HOME
@@ -2361,6 +2398,8 @@ file.ts
'Non-matching date should return null');
} finally {
process.env.HOME = origHome;
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
else delete process.env.USERPROFILE;
if (origDir) process.env.CLAUDE_DIR = origDir;
delete require.cache[require.resolve('../../scripts/lib/utils')];
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
@@ -2450,6 +2489,7 @@ file.ts
// "2026/01/15" or "Jan 15 2026" will never match, silently returning empty.
// No validation or normalization occurs on the date parameter.
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
const origDir = process.env.CLAUDE_DIR;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'r124-date-format-'));
const homeDir = path.join(tmpDir, 'home');
@@ -2457,6 +2497,7 @@ file.ts
try {
process.env.HOME = homeDir;
process.env.USERPROFILE = homeDir; // Windows: os.homedir() uses USERPROFILE
delete process.env.CLAUDE_DIR;
delete require.cache[require.resolve('../../scripts/lib/utils')];
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
@@ -2495,6 +2536,8 @@ file.ts
'null date skips filter and returns all sessions');
} finally {
process.env.HOME = origHome;
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
else delete process.env.USERPROFILE;
if (origDir) process.env.CLAUDE_DIR = origDir;
delete require.cache[require.resolve('../../scripts/lib/utils')];
delete require.cache[require.resolve('../../scripts/lib/session-manager')];

View File

@@ -836,7 +836,8 @@ function runTests() {
console.log('\nrunCommand Edge Cases:');
if (test('runCommand returns trimmed output', () => {
const result = utils.runCommand('echo " hello "');
// Windows echo includes quotes in output, use node to ensure consistent behavior
const result = utils.runCommand('node -e "process.stdout.write(\' hello \')"');
assert.strictEqual(result.success, true);
assert.strictEqual(result.output, 'hello', 'Should trim leading/trailing whitespace');
})) passed++; else failed++;
@@ -884,6 +885,10 @@ function runTests() {
console.log('\nreadStdinJson maxSize truncation:');
if (test('readStdinJson maxSize stops accumulating after threshold (chunk-level guard)', () => {
if (process.platform === 'win32') {
console.log(' (skipped — stdin chunking behavior differs on Windows)');
return true;
}
const { execFileSync } = require('child_process');
// maxSize is a chunk-level guard: once data.length >= maxSize, no MORE chunks are added.
// A single small chunk that arrives when data.length < maxSize is added in full.
@@ -1678,6 +1683,10 @@ function runTests() {
// ── Round 110: findFiles root directory unreadable — silent empty return (not throw) ──
console.log('\nRound 110: findFiles (root directory unreadable — EACCES on readdirSync caught silently):');
if (test('findFiles returns empty array when root directory exists but is unreadable', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return true;
}
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r110-unreadable-root-'));
const unreadableDir = path.join(tmpDir, 'no-read');
fs.mkdirSync(unreadableDir);
@@ -1697,7 +1706,7 @@ function runTests() {
'Recursive search on unreadable root should also return empty array');
} finally {
// Restore permissions before cleanup
try { fs.chmodSync(unreadableDir, 0o755); } catch {}
try { fs.chmodSync(unreadableDir, 0o755); } catch (_e) { /* ignore permission errors */ }
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;