mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 16:45:48 +08:00
test: cover session activity tracker edge paths
This commit is contained in:
parent
f92dc544c4
commit
33edfd3bb3
@ -16,6 +16,13 @@ const script = path.join(
|
||||
'hooks',
|
||||
'session-activity-tracker.js'
|
||||
);
|
||||
const {
|
||||
buildActivityRow,
|
||||
extractFileEvents,
|
||||
extractFilePaths,
|
||||
summarizeOutput,
|
||||
run,
|
||||
} = require(script);
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@ -52,6 +59,15 @@ function runScript(input, envOverrides = {}, options = {}) {
|
||||
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
|
||||
}
|
||||
|
||||
function readMetricRows(homeDir) {
|
||||
const metricsFile = path.join(homeDir, '.claude', 'metrics', 'tool-usage.jsonl');
|
||||
return fs.readFileSync(metricsFile, 'utf8')
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map(line => JSON.parse(line));
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing session-activity-tracker.js ===\n');
|
||||
|
||||
@ -405,6 +421,246 @@ function runTests() {
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('skips non-PostToolUse events and rows without required identifiers', () => {
|
||||
assert.strictEqual(buildActivityRow(
|
||||
{ tool_name: 'Read', tool_input: { file_path: 'README.md' } },
|
||||
{ CLAUDE_HOOK_EVENT_NAME: 'PreToolUse', ECC_SESSION_ID: 'sess' }
|
||||
), null);
|
||||
assert.strictEqual(buildActivityRow(
|
||||
{ tool_name: 'Read', tool_input: { file_path: 'README.md' } },
|
||||
{ CLAUDE_HOOK_EVENT_NAME: 'PostToolUse' }
|
||||
), null);
|
||||
assert.strictEqual(buildActivityRow(
|
||||
{ tool_input: { file_path: 'README.md' } },
|
||||
{ CLAUDE_HOOK_EVENT_NAME: 'PostToolUse', ECC_SESSION_ID: 'sess' }
|
||||
), null);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('sanitizes nested params, long summaries, and output variants', () => {
|
||||
const longValue = `start ${'x'.repeat(260)} ghp_${'A'.repeat(20)}`;
|
||||
const row = buildActivityRow(
|
||||
{
|
||||
tool_name: 'Lookup',
|
||||
tool_input: {
|
||||
query: longValue,
|
||||
secret: `gho_${'B'.repeat(20)}`,
|
||||
count: 3,
|
||||
enabled: false,
|
||||
omitted: null,
|
||||
nested: { a: { b: { c: { d: 'too deep' } } } },
|
||||
list: [1, true, null, 4],
|
||||
},
|
||||
tool_output: `line one\nline two ${'y'.repeat(260)}`,
|
||||
},
|
||||
{ CLAUDE_HOOK_EVENT_NAME: 'PostToolUse', CLAUDE_SESSION_ID: 'claude-fallback' }
|
||||
);
|
||||
|
||||
assert.strictEqual(row.session_id, 'claude-fallback');
|
||||
assert.strictEqual(row.file_paths.length, 0);
|
||||
assert.ok(row.input_summary.endsWith('...'), 'Expected long shallow summary to be truncated');
|
||||
assert.ok(!row.input_summary.includes('ghp_'), 'Expected GitHub token redaction in input summary');
|
||||
assert.ok(row.output_summary.endsWith('...'), 'Expected long output summary to be truncated');
|
||||
assert.ok(!row.output_summary.includes('\n'), 'Expected output summary to normalize whitespace');
|
||||
|
||||
const params = JSON.parse(row.input_params_json);
|
||||
assert.strictEqual(params.count, 3);
|
||||
assert.strictEqual(params.enabled, false);
|
||||
assert.strictEqual(params.omitted, null);
|
||||
assert.strictEqual(params.secret, '<REDACTED>');
|
||||
assert.strictEqual(params.nested.a.b.c, '[Truncated]');
|
||||
assert.deepStrictEqual(params.list.slice(0, 3), [1, true, null]);
|
||||
assert.strictEqual(params.list[3], 4);
|
||||
assert.ok(params.query.endsWith('...'), 'Expected long param value to be truncated');
|
||||
|
||||
assert.strictEqual(summarizeOutput(null), '');
|
||||
assert.strictEqual(summarizeOutput(undefined), '');
|
||||
assert.strictEqual(summarizeOutput('hello\nworld'), 'hello world');
|
||||
assert.strictEqual(summarizeOutput({ ok: true }), '{"ok":true}');
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('extracts file paths from nested arrays while filtering duplicates and remote URIs', () => {
|
||||
const paths = extractFilePaths({
|
||||
file_paths: [
|
||||
'src/a.js',
|
||||
'src/a.js',
|
||||
'https://example.com/file.js',
|
||||
'',
|
||||
{ file_path: 'src/b.js' },
|
||||
],
|
||||
nested: {
|
||||
source_path: 'app://connector/item',
|
||||
deep: [
|
||||
{ new_file_path: 'src/c.js' },
|
||||
{ old_file_path: 'plugin://plugin/item' },
|
||||
42,
|
||||
],
|
||||
},
|
||||
ignored: 'not-a-path-field',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(paths, ['src/a.js', 'src/b.js', 'src/c.js']);
|
||||
assert.deepStrictEqual(extractFilePaths(null), []);
|
||||
assert.deepStrictEqual(extractFilePaths('src/not-collected.js'), []);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('extracts file event previews for create delete and one-sided edits', () => {
|
||||
const events = extractFileEvents('Write', {
|
||||
files: [
|
||||
{
|
||||
file_path: 'src/new.ts',
|
||||
content: 'first line\nsecond line',
|
||||
},
|
||||
{
|
||||
file_path: 'src/new.ts',
|
||||
content: 'first line\nsecond line',
|
||||
},
|
||||
{
|
||||
file_path: 'https://example.com/remote.ts',
|
||||
content: 'ignored',
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.deepStrictEqual(events, [
|
||||
{
|
||||
path: 'src/new.ts',
|
||||
action: 'create',
|
||||
diff_preview: '+ first line second line',
|
||||
patch_preview: '+ first line second line',
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(extractFileEvents('Remove', {
|
||||
file_path: 'src/old.ts',
|
||||
content: 'legacy line',
|
||||
}), [
|
||||
{
|
||||
path: 'src/old.ts',
|
||||
action: 'delete',
|
||||
patch_preview: '- legacy line',
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(extractFileEvents('Edit', {
|
||||
edits: [
|
||||
{ file_path: 'src/before.ts', old_string: 'legacy', new_string: '' },
|
||||
{ file_path: 'src/after.ts', old_string: '', new_string: 'modern' },
|
||||
{ file_path: 'src/no-preview.ts', old_string: '', new_string: '' },
|
||||
],
|
||||
}), [
|
||||
{
|
||||
path: 'src/before.ts',
|
||||
action: 'modify',
|
||||
diff_preview: 'legacy ->',
|
||||
patch_preview: '@@\n- legacy',
|
||||
},
|
||||
{
|
||||
path: 'src/after.ts',
|
||||
action: 'modify',
|
||||
diff_preview: '-> modern',
|
||||
patch_preview: '@@\n+ modern',
|
||||
},
|
||||
{ path: 'src/no-preview.ts', action: 'modify' },
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(extractFileEvents('Rename', {
|
||||
old_file_path: 'src/old-name.ts',
|
||||
new_file_path: 'src/new-name.ts',
|
||||
}), [
|
||||
{ path: 'src/old-name.ts', action: 'move' },
|
||||
{ path: 'src/new-name.ts', action: 'move' },
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(extractFileEvents('Read', null), []);
|
||||
assert.deepStrictEqual(extractFileEvents('Touch', { file_path: 'src/touched.ts' }), [
|
||||
{ path: 'src/touched.ts', action: 'touch' },
|
||||
]);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('records creation previews unchanged when running outside a git repository', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const tmpCwd = makeTempDir();
|
||||
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: 'created.txt',
|
||||
content: 'alpha\nbeta',
|
||||
},
|
||||
tool_output: 17,
|
||||
};
|
||||
const result = runScript(input, {
|
||||
...withTempHome(tmpHome),
|
||||
CLAUDE_HOOK_EVENT_NAME: 'PostToolUse',
|
||||
ECC_SESSION_ID: 'ecc-session-non-git-create',
|
||||
}, {
|
||||
cwd: tmpCwd,
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 0);
|
||||
const [row] = readMetricRows(tmpHome);
|
||||
assert.strictEqual(row.output_summary, '17');
|
||||
assert.deepStrictEqual(row.file_events, [
|
||||
{
|
||||
path: 'created.txt',
|
||||
action: 'create',
|
||||
diff_preview: '+ alpha beta',
|
||||
patch_preview: '+ alpha beta',
|
||||
},
|
||||
]);
|
||||
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('preserves absolute paths outside the repo without git enrichment', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const outsideDir = makeTempDir();
|
||||
const outsideFile = path.join(outsideDir, 'outside.txt');
|
||||
fs.writeFileSync(outsideFile, 'outside', 'utf8');
|
||||
|
||||
const input = {
|
||||
tool_name: 'Read',
|
||||
tool_input: {
|
||||
file_path: outsideFile,
|
||||
},
|
||||
tool_output: 'read outside',
|
||||
};
|
||||
const result = runScript(input, {
|
||||
...withTempHome(tmpHome),
|
||||
CLAUDE_HOOK_EVENT_NAME: 'PostToolUse',
|
||||
ECC_SESSION_ID: 'ecc-session-absolute-outside',
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 0);
|
||||
const [row] = readMetricRows(tmpHome);
|
||||
assert.deepStrictEqual(row.file_paths, [outsideFile]);
|
||||
assert.deepStrictEqual(row.file_events, [
|
||||
{ path: outsideFile, action: 'read' },
|
||||
]);
|
||||
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
fs.rmSync(outsideDir, { recursive: true, force: true });
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('passes empty stdin through without creating metrics', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const result = runScript('', {
|
||||
...withTempHome(tmpHome),
|
||||
CLAUDE_HOOK_EVENT_NAME: 'PostToolUse',
|
||||
ECC_SESSION_ID: 'sess-empty',
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
assert.strictEqual(run(''), '');
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(tmpHome, '.claude', 'metrics', 'tool-usage.jsonl')),
|
||||
false
|
||||
);
|
||||
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user