test: add 3 tests for TOCTOU catch paths and NaN date sort fallback (round 84)

- getSessionById returns null for broken symlink (session-manager.js:307-310)
- findFiles skips broken symlinks matching the pattern (utils.js:170-173)
- listAliases sorts entries with invalid/missing dates via getTime() || 0 fallback
This commit is contained in:
Affaan Mustafa
2026-02-13 11:35:22 -08:00
parent 15717d6d04
commit cedcf9a701
3 changed files with 118 additions and 0 deletions

View File

@@ -1154,6 +1154,54 @@ function runTests() {
}
})) passed++; else failed++;
// ── Round 84: listAliases sort NaN date fallback (getTime() || 0) ──
console.log('\nRound 84: listAliases (NaN date fallback in sort comparator):');
if (test('listAliases sorts entries with invalid/missing dates to the end via || 0 fallback', () => {
// session-aliases.js line 257:
// (new Date(b.updatedAt || b.createdAt || 0).getTime() || 0) - ...
// When updatedAt and createdAt are both invalid strings, getTime() returns NaN.
// The outer || 0 converts NaN to 0 (epoch time), pushing the entry to the end.
resetAliases();
const data = aliases.loadAliases();
// Entry with valid dates — should sort first (newest)
data.aliases['valid-alias'] = {
sessionPath: '/sessions/valid',
createdAt: '2026-02-10T12:00:00.000Z',
updatedAt: '2026-02-10T12:00:00.000Z',
title: 'Valid'
};
// Entry with invalid date strings — getTime() → NaN → || 0 → epoch (oldest)
data.aliases['nan-alias'] = {
sessionPath: '/sessions/nan',
createdAt: 'not-a-date',
updatedAt: 'also-invalid',
title: 'NaN dates'
};
// Entry with missing date fields — undefined || undefined || 0 → new Date(0) → epoch
data.aliases['missing-alias'] = {
sessionPath: '/sessions/missing',
title: 'Missing dates'
// No createdAt or updatedAt
};
aliases.saveAliases(data);
const list = aliases.listAliases();
assert.strictEqual(list.length, 3, 'Should list all 3 aliases');
// Valid-dated entry should be first (newest by updatedAt)
assert.strictEqual(list[0].name, 'valid-alias',
'Entry with valid dates should sort first');
// The two invalid-dated entries sort to epoch (0), so they come after
assert.ok(
(list[1].name === 'nan-alias' || list[1].name === 'missing-alias') &&
(list[2].name === 'nan-alias' || list[2].name === 'missing-alias'),
'Entries with invalid/missing dates should sort to the end');
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -1349,6 +1349,42 @@ src/main.ts
}
})) passed++; else failed++;
// ── Round 84: getSessionById TOCTOU — statSync catch returns null for broken symlink ──
console.log('\nRound 84: getSessionById (broken symlink — statSync catch):');
if (test('getSessionById returns null when matching session is a broken symlink', () => {
// getSessionById at line 307-310: statSync throws for broken symlinks,
// the catch returns null (file deleted between readdir and stat).
const isoHome = path.join(os.tmpdir(), `ecc-r84-getbyid-toctou-${Date.now()}`);
const sessionsDir = path.join(isoHome, '.claude', 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
// Create a broken symlink that matches a session ID pattern
const brokenFile = '2026-02-11-deadbeef-session.tmp';
fs.symlinkSync('/nonexistent/target/that/does/not/exist', path.join(sessionsDir, brokenFile));
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
try {
process.env.HOME = isoHome;
process.env.USERPROFILE = isoHome;
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
const freshSM = require('../../scripts/lib/session-manager');
// Search by the short ID "deadbeef" — should match the broken symlink
const result = freshSM.getSessionById('deadbeef');
assert.strictEqual(result, null,
'Should return null when matching session file is a broken symlink');
} finally {
process.env.HOME = origHome;
process.env.USERPROFILE = origUserProfile;
delete require.cache[require.resolve('../../scripts/lib/session-manager')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
fs.rmSync(isoHome, { recursive: true, force: true });
}
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -1131,6 +1131,40 @@ function runTests() {
}
})) passed++; else failed++;
// ── Round 84: findFiles inner statSync catch (TOCTOU — broken symlink) ──
console.log('\nRound 84: findFiles (inner statSync catch — broken symlink):');
if (test('findFiles skips broken symlinks that match the pattern', () => {
// findFiles at utils.js:170-173: readdirSync returns entries including broken
// symlinks (entry.isFile() returns false for broken symlinks, but the test also
// verifies the overall robustness). On some systems, broken symlinks can be
// returned by readdirSync and pass through isFile() depending on the driver.
// More importantly: if statSync throws inside the inner loop, catch continues.
//
// To reliably trigger the statSync catch: create a real file, list it, then
// simulate the race. Since we can't truly race, we use a broken symlink which
// will at minimum verify the function doesn't crash on unusual dir entries.
const tmpDir = path.join(utils.getTempDir(), `ecc-r84-findfiles-toctou-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
// Create a real file and a broken symlink, both matching *.txt
const realFile = path.join(tmpDir, 'real.txt');
fs.writeFileSync(realFile, 'content');
const brokenLink = path.join(tmpDir, 'broken.txt');
fs.symlinkSync('/nonexistent/path/does/not/exist', brokenLink);
try {
const results = utils.findFiles(tmpDir, '*.txt');
// The real file should be found; the broken symlink should be skipped
const paths = results.map(r => r.path);
assert.ok(paths.some(p => p.includes('real.txt')), 'Should find the real file');
assert.ok(!paths.some(p => p.includes('broken.txt')),
'Should not include broken symlink in results');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// Summary
console.log('\n=== Test Results ===');
console.log(`Passed: ${passed}`);