test(websearch): add unit tests for hook-utils module

Add 17 tests for isCcsWebSearchHook and deduplicateCcsHooks functions
covering Unix/Windows paths, edge cases, and duplicate cleanup.
Also add invariant comment in hook-config.ts.

Addresses code review feedback from PR #424.
This commit is contained in:
kaitranntt
2026-02-02 15:08:21 -05:00
parent cc55999628
commit deb62490db
2 changed files with 284 additions and 0 deletions
@@ -0,0 +1,280 @@
import { expect, test, describe } from "bun:test";
import { isCcsWebSearchHook, deduplicateCcsHooks } from "../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 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);
});
});
+4
View File
@@ -134,6 +134,10 @@ export function ensureHookConfig(): boolean {
return normalized.includes('.ccs/hooks/websearch-transformer');
});
// INVARIANT: webSearchHookIndex remains valid after deduplication because:
// - findIndex() returns the FIRST matching CCS hook
// - deduplicateCcsHooks() keeps the FIRST CCS hook and removes subsequent duplicates
// This means the index always points to the preserved hook.
if (webSearchHookIndex !== -1) {
// Hook exists - first clean up any duplicates
const hadDuplicates = deduplicateCcsHooks(settings);