test: isolate suggest-compact counter fixtures

This commit is contained in:
Affaan Mustafa
2026-03-25 03:51:15 -04:00
parent 9c5ca92e6e
commit 2d1e384eef

View File

@@ -54,47 +54,56 @@ function getCounterFilePath(sessionId) {
return path.join(os.tmpdir(), `claude-tool-count-${sessionId}`); return path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
} }
let counterContextSeq = 0;
function createCounterContext(prefix = 'test-compact') {
counterContextSeq += 1;
const sessionId = `${prefix}-${Date.now()}-${counterContextSeq}`;
const counterFile = getCounterFilePath(sessionId);
return {
sessionId,
counterFile,
cleanup() {
try {
fs.unlinkSync(counterFile);
} catch (_err) {
// Ignore missing temp files between runs
}
}
};
}
function runTests() { function runTests() {
console.log('\n=== Testing suggest-compact.js ===\n'); console.log('\n=== Testing suggest-compact.js ===\n');
let passed = 0; let passed = 0;
let failed = 0; let failed = 0;
// Use a unique session ID per test run to avoid collisions
const testSession = `test-compact-${Date.now()}`;
const counterFile = getCounterFilePath(testSession);
// Cleanup helper
function cleanupCounter() {
try {
fs.unlinkSync(counterFile);
} catch (_err) {
// Ignore error
}
}
// Basic functionality // Basic functionality
console.log('Basic counter functionality:'); console.log('Basic counter functionality:');
if (test('creates counter file on first run', () => { if (test('creates counter file on first run', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
const result = runCompact({ CLAUDE_SESSION_ID: testSession }); cleanup();
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0, 'Should exit 0'); assert.strictEqual(result.code, 0, 'Should exit 0');
assert.ok(fs.existsSync(counterFile), 'Counter file should be created'); assert.ok(fs.existsSync(counterFile), 'Counter file should be created');
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter should be 1 after first run'); assert.strictEqual(count, 1, 'Counter should be 1 after first run');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('increments counter on subsequent runs', () => { if (test('increments counter on subsequent runs', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
runCompact({ CLAUDE_SESSION_ID: testSession }); cleanup();
runCompact({ CLAUDE_SESSION_ID: testSession }); runCompact({ CLAUDE_SESSION_ID: sessionId });
runCompact({ CLAUDE_SESSION_ID: testSession }); runCompact({ CLAUDE_SESSION_ID: sessionId });
runCompact({ CLAUDE_SESSION_ID: sessionId });
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 3, 'Counter should be 3 after three runs'); assert.strictEqual(count, 3, 'Counter should be 3 after three runs');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
@@ -102,28 +111,30 @@ function runTests() {
console.log('\nThreshold suggestion:'); console.log('\nThreshold suggestion:');
if (test('suggests compact at threshold (COMPACT_THRESHOLD=3)', () => { if (test('suggests compact at threshold (COMPACT_THRESHOLD=3)', () => {
cleanupCounter(); const { sessionId, cleanup } = createCounterContext();
cleanup();
// Run 3 times with threshold=3 // Run 3 times with threshold=3
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
assert.ok( assert.ok(
result.stderr.includes('3 tool calls reached') || result.stderr.includes('consider /compact'), result.stderr.includes('3 tool calls reached') || result.stderr.includes('consider /compact'),
`Should suggest compact at threshold. Got stderr: ${result.stderr}` `Should suggest compact at threshold. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('does NOT suggest compact before threshold', () => { if (test('does NOT suggest compact before threshold', () => {
cleanupCounter(); const { sessionId, cleanup } = createCounterContext();
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '5' }); cleanup();
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '5' }); runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '5' });
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '5' });
assert.ok( assert.ok(
!result.stderr.includes('StrategicCompact'), !result.stderr.includes('StrategicCompact'),
'Should NOT suggest compact before threshold' 'Should NOT suggest compact before threshold'
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
@@ -131,18 +142,19 @@ function runTests() {
console.log('\nInterval suggestion:'); console.log('\nInterval suggestion:');
if (test('suggests at threshold + 25 interval', () => { if (test('suggests at threshold + 25 interval', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
// Set counter to threshold+24 (so next run = threshold+25) // Set counter to threshold+24 (so next run = threshold+25)
// threshold=3, so we need count=28 → 25 calls past threshold // threshold=3, so we need count=28 → 25 calls past threshold
// Write 27 to the counter file, next run will be 28 = 3 + 25 // Write 27 to the counter file, next run will be 28 = 3 + 25
fs.writeFileSync(counterFile, '27'); fs.writeFileSync(counterFile, '27');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
// count=28, threshold=3, 28-3=25, 25 % 25 === 0 → should suggest // count=28, threshold=3, 28-3=25, 25 % 25 === 0 → should suggest
assert.ok( assert.ok(
result.stderr.includes('28 tool calls') || result.stderr.includes('checkpoint'), result.stderr.includes('28 tool calls') || result.stderr.includes('checkpoint'),
`Should suggest at threshold+25 interval. Got stderr: ${result.stderr}` `Should suggest at threshold+25 interval. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
@@ -150,42 +162,45 @@ function runTests() {
console.log('\nEnvironment variable handling:'); console.log('\nEnvironment variable handling:');
if (test('uses default threshold (50) when COMPACT_THRESHOLD is not set', () => { if (test('uses default threshold (50) when COMPACT_THRESHOLD is not set', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
// Write counter to 49, next run will be 50 = default threshold // Write counter to 49, next run will be 50 = default threshold
fs.writeFileSync(counterFile, '49'); fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: testSession }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
// Remove COMPACT_THRESHOLD from env // Remove COMPACT_THRESHOLD from env
assert.ok( assert.ok(
result.stderr.includes('50 tool calls reached'), result.stderr.includes('50 tool calls reached'),
`Should use default threshold of 50. Got stderr: ${result.stderr}` `Should use default threshold of 50. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => { if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49'); fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '-5' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '-5' });
// Invalid threshold falls back to 50 // Invalid threshold falls back to 50
assert.ok( assert.ok(
result.stderr.includes('50 tool calls reached'), result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}` `Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('ignores non-numeric COMPACT_THRESHOLD', () => { if (test('ignores non-numeric COMPACT_THRESHOLD', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49'); fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: 'abc' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: 'abc' });
// NaN falls back to 50 // NaN falls back to 50
assert.ok( assert.ok(
result.stderr.includes('50 tool calls reached'), result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}` `Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
@@ -193,38 +208,41 @@ function runTests() {
console.log('\nCorrupted counter file:'); console.log('\nCorrupted counter file:');
if (test('resets counter on corrupted file content', () => { if (test('resets counter on corrupted file content', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, 'not-a-number'); fs.writeFileSync(counterFile, 'not-a-number');
const result = runCompact({ CLAUDE_SESSION_ID: testSession }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0); assert.strictEqual(result.code, 0);
// Corrupted file → parsed is NaN → falls back to count=1 // Corrupted file → parsed is NaN → falls back to count=1
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file'); assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('resets counter on extremely large value', () => { if (test('resets counter on extremely large value', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
// Value > 1000000 should be clamped // Value > 1000000 should be clamped
fs.writeFileSync(counterFile, '9999999'); fs.writeFileSync(counterFile, '9999999');
const result = runCompact({ CLAUDE_SESSION_ID: testSession }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0); assert.strictEqual(result.code, 0);
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000'); assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('handles empty counter file', () => { if (test('handles empty counter file', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, ''); fs.writeFileSync(counterFile, '');
const result = runCompact({ CLAUDE_SESSION_ID: testSession }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0); assert.strictEqual(result.code, 0);
// Empty file → bytesRead=0 → count starts at 1 // Empty file → bytesRead=0 → count starts at 1
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Should start at 1 for empty file'); assert.strictEqual(count, 1, 'Should start at 1 for empty file');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
@@ -255,10 +273,11 @@ function runTests() {
console.log('\nExit code:'); console.log('\nExit code:');
if (test('always exits 0 (never blocks Claude)', () => { if (test('always exits 0 (never blocks Claude)', () => {
cleanupCounter(); const { sessionId, cleanup } = createCounterContext();
const result = runCompact({ CLAUDE_SESSION_ID: testSession }); cleanup();
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
assert.strictEqual(result.code, 0, 'Should always exit 0'); assert.strictEqual(result.code, 0, 'Should always exit 0');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
@@ -266,48 +285,52 @@ function runTests() {
console.log('\nThreshold boundary values:'); console.log('\nThreshold boundary values:');
if (test('rejects COMPACT_THRESHOLD=0 (falls back to 50)', () => { if (test('rejects COMPACT_THRESHOLD=0 (falls back to 50)', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49'); fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '0' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '0' });
// 0 is invalid (must be > 0), falls back to 50, count becomes 50 → should suggest // 0 is invalid (must be > 0), falls back to 50, count becomes 50 → should suggest
assert.ok( assert.ok(
result.stderr.includes('50 tool calls reached'), result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}` `Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => { if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '9999'); fs.writeFileSync(counterFile, '9999');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '10000' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '10000' });
// count becomes 10000, threshold=10000 → should suggest // count becomes 10000, threshold=10000 → should suggest
assert.ok( assert.ok(
result.stderr.includes('10000 tool calls reached'), result.stderr.includes('10000 tool calls reached'),
`Should accept threshold=10000. Got stderr: ${result.stderr}` `Should accept threshold=10000. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => { if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49'); fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '10001' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '10001' });
// 10001 > 10000, invalid, falls back to 50, count becomes 50 → should suggest // 10001 > 10000, invalid, falls back to 50, count becomes 50 → should suggest
assert.ok( assert.ok(
result.stderr.includes('50 tool calls reached'), result.stderr.includes('50 tool calls reached'),
`Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}` `Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}`
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => { if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '49'); fs.writeFileSync(counterFile, '49');
const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3.5' }); const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3.5' });
// parseInt('3.5') = 3, which is valid (> 0 && <= 10000) // parseInt('3.5') = 3, which is valid (> 0 && <= 10000)
// count becomes 50, threshold=3, 50-3=47, 47%25≠0 and 50≠3 → no suggestion // count becomes 50, threshold=3, 50-3=47, 47%25≠0 and 50≠3 → no suggestion
assert.strictEqual(result.code, 0); assert.strictEqual(result.code, 0);
@@ -316,28 +339,30 @@ function runTests() {
!result.stderr.includes('StrategicCompact'), !result.stderr.includes('StrategicCompact'),
'Float threshold should be parseInt-ed to 3, no suggestion at count=50' 'Float threshold should be parseInt-ed to 3, no suggestion at count=50'
); );
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('counter value at exact boundary 1000000 is valid', () => { if (test('counter value at exact boundary 1000000 is valid', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '999999'); fs.writeFileSync(counterFile, '999999');
runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
// 999999 is valid (> 0, <= 1000000), count becomes 1000000 // 999999 is valid (> 0, <= 1000000), count becomes 1000000
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid'); assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;
if (test('counter value at 1000001 is clamped (reset to 1)', () => { if (test('counter value at 1000001 is clamped (reset to 1)', () => {
cleanupCounter(); const { sessionId, counterFile, cleanup } = createCounterContext();
cleanup();
fs.writeFileSync(counterFile, '1000001'); fs.writeFileSync(counterFile, '1000001');
runCompact({ CLAUDE_SESSION_ID: testSession }); runCompact({ CLAUDE_SESSION_ID: sessionId });
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1'); assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1');
cleanupCounter(); cleanup();
})) passed++; })) passed++;
else failed++; else failed++;