From de174b06e6107222eb412097412a453446298d78 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 30 May 2026 19:12:05 +0900 Subject: [PATCH] test(claude-code-hooks): batch 30 (3 files) --- src/hooks/claude-code-hooks/hook-text.ts | 19 ++++++ .../claude-code-hooks/post-tool-use.test.ts | 67 +++++++++++++++++++ .../claude-code-hooks/pre-compact.test.ts | 45 +++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/hooks/claude-code-hooks/hook-text.ts create mode 100644 src/hooks/claude-code-hooks/post-tool-use.test.ts create mode 100644 src/hooks/claude-code-hooks/pre-compact.test.ts diff --git a/src/hooks/claude-code-hooks/hook-text.ts b/src/hooks/claude-code-hooks/hook-text.ts new file mode 100644 index 000000000..db77c9078 --- /dev/null +++ b/src/hooks/claude-code-hooks/hook-text.ts @@ -0,0 +1,19 @@ +export function normalizeHookText(value: string | undefined): string | undefined { + if (value === undefined) { + return undefined + } + + const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim() + return normalized.length > 0 ? normalized : undefined +} + +export function normalizeHookTextList(values: readonly (string | undefined)[]): string[] { + const normalizedValues: string[] = [] + for (const value of values) { + const normalized = normalizeHookText(value) + if (normalized !== undefined) { + normalizedValues.push(normalized) + } + } + return normalizedValues +} diff --git a/src/hooks/claude-code-hooks/post-tool-use.test.ts b/src/hooks/claude-code-hooks/post-tool-use.test.ts new file mode 100644 index 000000000..dd5325b6e --- /dev/null +++ b/src/hooks/claude-code-hooks/post-tool-use.test.ts @@ -0,0 +1,67 @@ +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as dispatchHookModule from "./dispatch-hook" +import { executePostToolUseHooks, type PostToolUseContext } from "./post-tool-use" +import type { ClaudeHooksConfig } from "./types" + +function createContext(overrides?: Partial): PostToolUseContext { + return { + sessionId: "test-session", + toolName: "write", + toolInput: { file_path: "/tmp/test.md", content: "hello" }, + toolOutput: { output: "wrote file" }, + cwd: "/tmp", + ...overrides, + } +} + +function createConfig(matchers: ClaudeHooksConfig["PostToolUse"]): ClaudeHooksConfig { + return { PostToolUse: matchers } +} + +describe("executePostToolUseHooks", () => { + afterEach(() => { + mock.restore() + }) + + it("#given hook JSON with multiline additional context #when PostToolUse runs #then it exposes readable context without raw JSON", async () => { + // given + spyOn(dispatchHookModule, "dispatchHook").mockResolvedValue({ + exitCode: 0, + stdout: JSON.stringify({ + hookSpecificOutput: { + hookEventName: "PostToolUse", + additionalContext: "\r\nFirst line\r\n indented second line\rThird line\r\n", + }, + }), + stderr: "", + }) + const config = createConfig([ + { matcher: "Write", hooks: [{ type: "command", command: "hook-context" }] }, + ]) + + // when + const result = await executePostToolUseHooks(createContext(), config) + + // then + expect(result.additionalContext).toBe("First line\n indented second line\nThird line") + expect(result.message).toBeUndefined() + }) + + it("#given non-json hook stdout with CRLF #when PostToolUse runs #then it normalizes the user-visible message", async () => { + // given + spyOn(dispatchHookModule, "dispatchHook").mockResolvedValue({ + exitCode: 0, + stdout: "\r\nFirst line\r\nSecond line\rThird line\r\n", + stderr: "", + }) + const config = createConfig([ + { matcher: "Write", hooks: [{ type: "command", command: "hook-text" }] }, + ]) + + // when + const result = await executePostToolUseHooks(createContext(), config) + + // then + expect(result.message).toBe("First line\nSecond line\nThird line") + }) +}) diff --git a/src/hooks/claude-code-hooks/pre-compact.test.ts b/src/hooks/claude-code-hooks/pre-compact.test.ts new file mode 100644 index 000000000..8d19ea017 --- /dev/null +++ b/src/hooks/claude-code-hooks/pre-compact.test.ts @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test" +import * as dispatchHookModule from "./dispatch-hook" +import { executePreCompactHooks, type PreCompactContext } from "./pre-compact" +import type { ClaudeHooksConfig } from "./types" + +function createContext(overrides?: Partial): PreCompactContext { + return { + sessionId: "test-session", + cwd: "/tmp", + ...overrides, + } +} + +function createConfig(matchers: ClaudeHooksConfig["PreCompact"]): ClaudeHooksConfig { + return { PreCompact: matchers } +} + +describe("executePreCompactHooks", () => { + afterEach(() => { + mock.restore() + }) + + it("#given hook context with CRLF and bare CR #when PreCompact runs #then it returns normalized context entries", async () => { + // given + spyOn(dispatchHookModule, "dispatchHook").mockResolvedValue({ + exitCode: 0, + stdout: JSON.stringify({ + hookSpecificOutput: { + hookEventName: "PreCompact", + additionalContext: ["\r\nfirst context\r\n detail\rsecond context\r\n"], + }, + }), + stderr: "", + }) + const config = createConfig([ + { matcher: "*", hooks: [{ type: "command", command: "pre-compact-hook" }] }, + ]) + + // when + const result = await executePreCompactHooks(createContext(), config) + + // then + expect(result.context).toEqual(["first context\n detail\nsecond context"]) + }) +})