Files

375 lines
10 KiB
TypeScript

import { expect, test, describe } from 'bun:test';
import {
isCcsWebSearchHook,
deduplicateCcsHooks,
} from '../../../../src/utils/websearch/hook-utils';
describe('isCcsWebSearchHook', () => {
test('Returns true for CCS hook with forward slashes (Unix path)', () => {
const hook = {
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(true);
});
test('Returns true for CCS hook with backslashes (Windows path)', () => {
const hook = {
matcher: 'WebSearch',
hooks: [
{
command: 'node C:\\Users\\user\\.ccs\\hooks\\websearch-transformer\\index.js',
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(true);
});
test('Returns true for mixed path separators', () => {
const hook = {
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user\\.ccs/hooks\\websearch-transformer/index.js',
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(true);
});
test('Returns true for path with double slashes (normalization)', () => {
const hook = {
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user//.ccs//hooks/websearch-transformer/index.js',
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(true);
});
test('Returns false for non-WebSearch matcher', () => {
const hook = {
matcher: 'SomethingElse',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(false);
});
test('Returns false for WebSearch with non-CCS hook command', () => {
const hook = {
matcher: 'WebSearch',
hooks: [
{
command: 'node /some/other/path/custom-hook.js',
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(false);
});
test('Returns false when hooks array is missing', () => {
const hook = {
matcher: 'WebSearch',
};
expect(isCcsWebSearchHook(hook)).toBe(false);
});
test('Returns false when hooks array is empty', () => {
const hook = {
matcher: 'WebSearch',
hooks: [],
};
expect(isCcsWebSearchHook(hook)).toBe(false);
});
test('Returns false when command is missing', () => {
const hook = {
matcher: 'WebSearch',
hooks: [{}],
};
expect(isCcsWebSearchHook(hook)).toBe(false);
});
test('Returns false when command is not a string', () => {
const hook = {
matcher: 'WebSearch',
hooks: [
{
command: 123,
},
],
};
expect(isCcsWebSearchHook(hook)).toBe(false);
});
});
describe('deduplicateCcsHooks', () => {
test('No-op when 0 CCS hooks (returns false)', () => {
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'SomeOtherMatcher',
hooks: [{ command: 'other-command' }],
},
],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(false);
expect(settings.hooks.PreToolUse).toHaveLength(1);
});
test('No-op when 1 CCS hook (returns false)', () => {
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
},
],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(false);
expect(settings.hooks.PreToolUse).toHaveLength(1);
});
test('Removes duplicates when 2+ CCS hooks (returns true, keeps first)', () => {
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node C:\\Users\\user\\.ccs\\hooks\\websearch-transformer\\index.js',
},
],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /another/path/.ccs/hooks/websearch-transformer/index.js',
},
],
},
],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(true);
expect(settings.hooks.PreToolUse).toHaveLength(1);
expect(settings.hooks.PreToolUse[0]).toEqual({
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
});
});
test('Preserves non-CCS hooks in array', () => {
const nonCcsHook = {
matcher: 'SomeOtherMatcher',
hooks: [{ command: 'other-command' }],
};
const settings = {
hooks: {
PreToolUse: [
nonCcsHook,
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node C:\\Users\\user\\.ccs\\hooks\\websearch-transformer\\index.js',
},
],
},
],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(true);
expect(settings.hooks.PreToolUse).toHaveLength(2);
expect(settings.hooks.PreToolUse[0]).toEqual(nonCcsHook);
});
test('Returns false when hooks is undefined', () => {
const settings = {};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(false);
});
test('Returns false when PreToolUse is undefined', () => {
const settings = {
hooks: {},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(false);
});
test('Handles multiple non-CCS hooks with duplicates', () => {
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'OtherMatcher1',
hooks: [{ command: 'command1' }],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /path1/.ccs/hooks/websearch-transformer/index.js',
},
],
},
{
matcher: 'OtherMatcher2',
hooks: [{ command: 'command2' }],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /path2/.ccs/hooks/websearch-transformer/index.js',
},
],
},
],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(true);
expect(settings.hooks.PreToolUse).toHaveLength(3);
// First and third should be non-CCS hooks, second should be the first CCS hook
expect(settings.hooks.PreToolUse[0].matcher).toBe('OtherMatcher1');
expect(settings.hooks.PreToolUse[1].matcher).toBe('WebSearch');
expect(settings.hooks.PreToolUse[2].matcher).toBe('OtherMatcher2');
});
test('Edge case: Empty PreToolUse array', () => {
const settings = {
hooks: {
PreToolUse: [],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(false);
expect(settings.hooks.PreToolUse).toHaveLength(0);
});
test('Stress test: 15 duplicate CCS WebSearch hooks', () => {
const ccsHook = {
matcher: 'WebSearch',
hooks: [
{
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
},
],
};
const settings = {
hooks: {
PreToolUse: Array(15).fill(ccsHook),
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(true);
expect(settings.hooks.PreToolUse).toHaveLength(1);
expect(settings.hooks.PreToolUse[0]).toEqual(ccsHook);
});
test('Leaves PostToolUse and PreToolCall untouched', () => {
const postToolUseHook1 = {
matcher: 'CustomMatcher1',
hooks: [{ command: 'custom-command-1' }],
};
const postToolUseHook2 = {
matcher: 'CustomMatcher2',
hooks: [{ command: 'custom-command-2' }],
};
const preToolCallHook = {
matcher: 'CustomMatcher3',
hooks: [{ command: 'custom-command-3' }],
};
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /path1/.ccs/hooks/websearch-transformer/index.js',
},
],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /path2/.ccs/hooks/websearch-transformer/index.js',
},
],
},
{
matcher: 'WebSearch',
hooks: [
{
command: 'node /path3/.ccs/hooks/websearch-transformer/index.js',
},
],
},
],
PostToolUse: [postToolUseHook1, postToolUseHook2],
PreToolCall: [preToolCallHook],
},
};
const result = deduplicateCcsHooks(settings);
expect(result).toBe(true);
// PreToolUse should be deduplicated to 1 hook
expect(settings.hooks.PreToolUse).toHaveLength(1);
expect(settings.hooks.PreToolUse[0].matcher).toBe('WebSearch');
// PostToolUse should remain unchanged with 2 hooks
expect(settings.hooks.PostToolUse).toHaveLength(2);
expect(settings.hooks.PostToolUse[0]).toEqual(postToolUseHook1);
expect(settings.hooks.PostToolUse[1]).toEqual(postToolUseHook2);
// PreToolCall should remain unchanged with 1 hook
expect(settings.hooks.PreToolCall).toHaveLength(1);
expect(settings.hooks.PreToolCall[0]).toEqual(preToolCallHook);
});
});