397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.5 KiB
JavaScript
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 });
|
|
});
|
|
});
|