Files
brain/tools/observer-hook-resolver.test.mjs

155 lines
5.5 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { buildHookMap, resolveScriptCounts, extractScriptName } from './observer-hook-resolver.mjs';
describe('extractScriptName', () => {
it('extracts tools/X.mjs from "node tools/observer-stop-hook.mjs"', () => {
expect(extractScriptName('node tools/observer-stop-hook.mjs')).toBe('tools/observer-stop-hook.mjs');
});
it('extracts tools/X.mjs from quoted path with cwd', () => {
expect(extractScriptName('node "C:/моя/проекты/портал crm/Документация/tools/subagent-prompt-prefix.mjs"'))
.toBe('tools/subagent-prompt-prefix.mjs');
});
it('extracts npx package name', () => {
expect(extractScriptName('npx -y markdownlint-cli2 --fix file.md')).toBe('markdownlint-cli2');
});
it('falls back to inline:<sha-16> for node -e inline scripts', () => {
const result = extractScriptName('node -e "const f=process.env.X; if(f) process.stderr.write(\'warn\');"');
expect(result).toMatch(/^inline:[0-9a-f]{16}$/);
});
it('inline fallback is stable across whitespace formatting', () => {
const a = extractScriptName('node -e "const f = 1;\n\nif(f) process.exit(0);"');
const b = extractScriptName('node -e "const f = 1; if(f) process.exit(0);"');
expect(a).toBe(b);
});
it('inline fallback differs for different commands', () => {
const a = extractScriptName('node -e "process.exit(0);"');
const b = extractScriptName('node -e "process.exit(1);"');
expect(a).not.toBe(b);
});
it('extracts tools/X.mjs from Windows backslash path', () => {
expect(extractScriptName('node tools\\observer-stop-hook.mjs')).toBe('tools/observer-stop-hook.mjs');
});
it('extracts tools/X.mjs from full Windows abs path with backslashes', () => {
expect(extractScriptName('node C:\\path\\tools\\foo.mjs')).toBe('tools/foo.mjs');
});
});
describe('buildHookMap', () => {
it('returns empty Map for empty settings', () => {
expect(buildHookMap({}).size).toBe(0);
});
it('handles missing hooks key', () => {
expect(buildHookMap({ permissions: {} }).size).toBe(0);
});
it('builds matcher → [scripts] for single-matcher single-script', () => {
const settings = {
hooks: {
PreToolUse: [
{ matcher: 'Bash', hooks: [{ type: 'command', command: 'node tools/foo.mjs' }] },
],
},
};
const map = buildHookMap(settings);
expect(map.get('PreToolUse:Bash')).toEqual(['tools/foo.mjs']);
});
it('aggregates multiple scripts per matcher', () => {
const settings = {
hooks: {
PreToolUse: [
{ matcher: 'Bash', hooks: [
{ type: 'command', command: 'node tools/foo.mjs' },
{ type: 'command', command: 'node tools/bar.mjs' },
]},
],
},
};
expect(buildHookMap(settings).get('PreToolUse:Bash')).toEqual(['tools/foo.mjs', 'tools/bar.mjs']);
});
it('uses event name without matcher for UserPromptSubmit-style hooks', () => {
const settings = {
hooks: {
UserPromptSubmit: [
{ hooks: [{ type: 'command', command: 'node tools/economy.mjs' }] },
],
},
};
expect(buildHookMap(settings).get('UserPromptSubmit')).toEqual(['tools/economy.mjs']);
});
it('merges project + user settings (project takes precedence on dup matcher)', () => {
const project = {
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'node tools/a.mjs' }] }] },
};
const user = {
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'node tools/b.mjs' }] }] },
};
const map = buildHookMap(project, user);
expect(map.get('PreToolUse:Bash')).toEqual(['tools/a.mjs', 'tools/b.mjs']);
});
it('splits combined matcher "Edit|Write" into two map entries', () => {
const settings = {
hooks: {
PreToolUse: [
{ matcher: 'Edit|Write', hooks: [{ type: 'command', command: 'node tools/guard.mjs' }] },
],
},
};
const map = buildHookMap(settings);
expect(map.get('PreToolUse:Edit')).toEqual(['tools/guard.mjs']);
expect(map.get('PreToolUse:Write')).toEqual(['tools/guard.mjs']);
expect(map.get('PreToolUse:Edit|Write')).toBeUndefined();
});
it('trims whitespace around matchers split on |', () => {
const settings = {
hooks: {
PreToolUse: [
{ matcher: 'Edit | Write', hooks: [{ type: 'command', command: 'node tools/g.mjs' }] },
],
},
};
const map = buildHookMap(settings);
expect(map.get('PreToolUse:Edit')).toEqual(['tools/g.mjs']);
expect(map.get('PreToolUse:Write')).toEqual(['tools/g.mjs']);
});
});
describe('resolveScriptCounts', () => {
it('returns {} for empty matcherCounts', () => {
expect(resolveScriptCounts({}, new Map())).toEqual({});
});
it('returns {} when matcher not in map', () => {
expect(resolveScriptCounts({ 'PreToolUse:Bash': 5 }, new Map())).toEqual({});
});
it('duplicates count for each script on the matcher', () => {
const map = new Map([['PreToolUse:Bash', ['tools/a.mjs', 'tools/b.mjs']]]);
expect(resolveScriptCounts({ 'PreToolUse:Bash': 5 }, map)).toEqual({
'tools/a.mjs': 5,
'tools/b.mjs': 5,
});
});
it('sums across multiple matchers that share a script', () => {
const map = new Map([
['PreToolUse:Bash', ['tools/x.mjs']],
['PostToolUse:Bash', ['tools/x.mjs']],
]);
expect(resolveScriptCounts({ 'PreToolUse:Bash': 3, 'PostToolUse:Bash': 2 }, map))
.toEqual({ 'tools/x.mjs': 5 });
});
});