outputWithContext() emitted a hookSpecificOutput object without the
required hookEventName field, so Claude Code rejected the hook output
("hookSpecificOutput is missing required field hookEventName") and
dropped the context the SessionStart, UserPromptSubmit, and PostToolUse
hooks tried to inject. Thread the event name through the helper; each
caller now passes its own (SessionStart / UserPromptSubmit / PostToolUse).
Existing emission tests now assert hookEventName per event.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
95 lines
3.3 KiB
JavaScript
95 lines
3.3 KiB
JavaScript
import { describe, it, afterEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { join } from 'path';
|
|
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState, readJsonl } from './test-helper.mjs';
|
|
|
|
let dir;
|
|
|
|
function freshState(overrides = {}) {
|
|
return {
|
|
start_epoch: Math.floor(Date.now() / 1000) - 60,
|
|
start_iso: '2026-01-01T10:00:00Z',
|
|
tool_count: 0, edit_count: 0,
|
|
last_event_epoch: 0, burst_count: 0,
|
|
dep_flags: 0, esc_flags: 0, fatigue_flags: 0, val_flags: 0,
|
|
last_warning_epoch: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
afterEach(() => { if (dir) cleanupTestDir(dir); });
|
|
|
|
describe('tool-tracker', () => {
|
|
it('tracks tool call and increments tool_count', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't1', freshState());
|
|
runHook('tool-tracker.mjs', { session_id: 't1', tool_name: 'Read' }, dir);
|
|
const s = readState(dir, 't1');
|
|
assert.equal(s.tool_count, 1);
|
|
const events = readJsonl(join(dir, 'events.jsonl'));
|
|
assert.equal(events.length, 1);
|
|
assert.equal(events[0].tool_name, 'Read');
|
|
assert.equal(events[0].session_id, 't1');
|
|
});
|
|
|
|
it('increments edit_count for Edit tool', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't2', freshState());
|
|
runHook('tool-tracker.mjs', { session_id: 't2', tool_name: 'Edit' }, dir);
|
|
const s = readState(dir, 't2');
|
|
assert.equal(s.edit_count, 1);
|
|
});
|
|
|
|
it('does not increment edit_count for non-Edit tool', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't3', freshState());
|
|
runHook('tool-tracker.mjs', { session_id: 't3', tool_name: 'Bash' }, dir);
|
|
const s = readState(dir, 't3');
|
|
assert.equal(s.edit_count, 0);
|
|
});
|
|
|
|
it('detects burst when interval < 30s', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't4', freshState({
|
|
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
|
|
burst_count: 0,
|
|
}));
|
|
runHook('tool-tracker.mjs', { session_id: 't4', tool_name: 'Read' }, dir);
|
|
const s = readState(dir, 't4');
|
|
assert.equal(s.burst_count, 1);
|
|
});
|
|
|
|
it('resets burst when interval >= 30s', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't5', freshState({
|
|
last_event_epoch: Math.floor(Date.now() / 1000) - 60,
|
|
burst_count: 3,
|
|
}));
|
|
runHook('tool-tracker.mjs', { session_id: 't5', tool_name: 'Read' }, dir);
|
|
const s = readState(dir, 't5');
|
|
assert.equal(s.burst_count, 0);
|
|
});
|
|
|
|
it('emits periodic reminder at modulo 25', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't6', freshState({ tool_count: 24 }));
|
|
const out = runHook('tool-tracker.mjs', { session_id: 't6', tool_name: 'Read' }, dir);
|
|
assert.equal(out.hookSpecificOutput?.hookEventName, 'PostToolUse');
|
|
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('REMINDER'));
|
|
});
|
|
|
|
it('outputs continue between checkpoints', () => {
|
|
dir = setupTestDir();
|
|
createStateFile(dir, 't7', freshState({ tool_count: 5 }));
|
|
const out = runHook('tool-tracker.mjs', { session_id: 't7', tool_name: 'Read' }, dir);
|
|
assert.equal(out.continue, true);
|
|
assert.ok(!out.hookSpecificOutput);
|
|
});
|
|
|
|
it('handles missing state file gracefully', () => {
|
|
dir = setupTestDir();
|
|
// No state file created
|
|
const out = runHook('tool-tracker.mjs', { session_id: 'missing', tool_name: 'Read' }, dir);
|
|
assert.equal(out.continue, true);
|
|
});
|
|
});
|