test(claude-code-hooks): batch 30 (3 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:05 +09:00
parent 08ea320b54
commit de174b06e6
3 changed files with 131 additions and 0 deletions
+19
View File
@@ -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
}
@@ -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>): 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")
})
})
@@ -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>): 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"])
})
})