test(claude-code-hooks): batch 45 (4 files)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { normalizeSessionId } from "../../features/boulder-state"
|
||||
import { log } from "../../shared/logger"
|
||||
import { HOOK_NAME } from "./hook-name"
|
||||
|
||||
@@ -7,6 +8,7 @@ export async function isSessionInBoulderLineage(input: {
|
||||
sessionID: string
|
||||
boulderSessionIDs: string[]
|
||||
}): Promise<boolean> {
|
||||
const normalizedBoulderSessionIDs = input.boulderSessionIDs.map((sessionID) => normalizeSessionId(sessionID))
|
||||
const visitedSessionIDs = new Set<string>()
|
||||
let currentSessionID = input.sessionID
|
||||
|
||||
@@ -33,7 +35,7 @@ export async function isSessionInBoulderLineage(input: {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.boulderSessionIDs.includes(parentSessionID)) {
|
||||
if (normalizedBoulderSessionIDs.includes(normalizeSessionId(parentSessionID))) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getPlanProgress,
|
||||
getWorkForSession,
|
||||
getTaskSessionState,
|
||||
normalizeSessionId,
|
||||
readBoulderState,
|
||||
readCurrentTopLevelTask,
|
||||
resolveBoulderPlanPath,
|
||||
@@ -77,6 +78,7 @@ async function injectContinuation(input: {
|
||||
|
||||
try {
|
||||
const currentBoulder = readBoulderState(input.ctx.directory)
|
||||
const normalizedSessionID = normalizeSessionId(input.sessionID)
|
||||
const currentPlanPath = currentBoulder
|
||||
? resolveBoulderPlanPath(input.ctx.directory, currentBoulder)
|
||||
: null
|
||||
@@ -95,7 +97,7 @@ async function injectContinuation(input: {
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
client: input.ctx.client,
|
||||
sessionID: input.sessionID,
|
||||
sessionOrigin: currentBoulder.session_origins?.[input.sessionID],
|
||||
sessionOrigin: currentBoulder.session_origins?.[normalizedSessionID],
|
||||
boulderSessionIDs: currentBoulder.session_ids,
|
||||
requiredAgent: currentBoulder.agent,
|
||||
})
|
||||
@@ -192,7 +194,8 @@ function scheduleRetry(input: {
|
||||
|
||||
const currentBoulder = readBoulderState(ctx.directory)
|
||||
if (!currentBoulder) return
|
||||
if (!currentBoulder.session_ids?.includes(sessionID)) return
|
||||
const normalizedSessionID = normalizeSessionId(sessionID)
|
||||
if (!currentBoulder.session_ids?.includes(normalizedSessionID)) return
|
||||
|
||||
const currentProgress = getPlanProgress(resolveBoulderPlanPath(ctx.directory, currentBoulder))
|
||||
if (currentProgress.isComplete) return
|
||||
@@ -200,7 +203,7 @@ function scheduleRetry(input: {
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
client: ctx.client,
|
||||
sessionID,
|
||||
sessionOrigin: currentBoulder.session_origins?.[sessionID],
|
||||
sessionOrigin: currentBoulder.session_origins?.[normalizedSessionID],
|
||||
boulderSessionIDs: currentBoulder.session_ids,
|
||||
requiredAgent: currentBoulder.agent,
|
||||
})
|
||||
@@ -230,6 +233,7 @@ export async function handleAtlasSessionIdle(input: {
|
||||
sessionID: string
|
||||
}): Promise<void> {
|
||||
const { ctx, options, getState, sessionID } = input
|
||||
const normalizedSessionID = normalizeSessionId(sessionID)
|
||||
const sessionState = getState(sessionID)
|
||||
|
||||
log(`[${HOOK_NAME}] session.idle`, { sessionID })
|
||||
@@ -358,7 +362,7 @@ export async function handleAtlasSessionIdle(input: {
|
||||
const canContinueSession = await canContinueTrackedBoulderSession({
|
||||
client: ctx.client,
|
||||
sessionID,
|
||||
sessionOrigin: boulderState.session_origins?.[sessionID],
|
||||
sessionOrigin: boulderState.session_origins?.[normalizedSessionID],
|
||||
boulderSessionIDs: boulderState.session_ids,
|
||||
requiredAgent: boulderState.agent,
|
||||
})
|
||||
@@ -477,7 +481,14 @@ async function canContinueTrackedBoulderSession(input: {
|
||||
boulderSessionIDs: string[]
|
||||
requiredAgent?: string
|
||||
}): Promise<boolean> {
|
||||
const ancestorSessionIDs = input.boulderSessionIDs.filter((trackedSessionID) => trackedSessionID !== input.sessionID)
|
||||
const normalizedSessionID = normalizeSessionId(input.sessionID)
|
||||
if (input.sessionOrigin === "direct") {
|
||||
return true
|
||||
}
|
||||
|
||||
const ancestorSessionIDs = input.boulderSessionIDs
|
||||
.map((sessionID) => normalizeSessionId(sessionID))
|
||||
.filter((trackedSessionID) => trackedSessionID !== normalizedSessionID)
|
||||
if (ancestorSessionIDs.length === 0) {
|
||||
return true
|
||||
}
|
||||
@@ -487,10 +498,6 @@ async function canContinueTrackedBoulderSession(input: {
|
||||
sessionID: input.sessionID,
|
||||
boulderSessionIDs: ancestorSessionIDs,
|
||||
})
|
||||
if (input.sessionOrigin === "direct") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isTrackedDescendant) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { beforeEach, describe, expect, it, mock, afterAll } from "bun:test"
|
||||
|
||||
type PostToolUseMockResult = {
|
||||
block?: boolean
|
||||
reason?: string
|
||||
message?: string
|
||||
warnings?: string[]
|
||||
additionalContext?: string
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -8,6 +16,7 @@ const transcriptCalls: Array<[string, unknown]> = []
|
||||
const appendTranscriptEntry = mock((sessionId: string, entry: unknown) => {
|
||||
transcriptCalls.push([sessionId, entry])
|
||||
})
|
||||
let postToolUseResult: PostToolUseMockResult = { warnings: [] }
|
||||
|
||||
mock.module("../config", () => ({
|
||||
loadClaudeHooksConfig: async () => ({}),
|
||||
@@ -18,7 +27,7 @@ mock.module("../config-loader", () => ({
|
||||
}))
|
||||
|
||||
mock.module("../post-tool-use", () => ({
|
||||
executePostToolUseHooks: async () => ({ warnings: [] }),
|
||||
executePostToolUseHooks: async () => postToolUseResult,
|
||||
}))
|
||||
|
||||
mock.module("../transcript", () => ({
|
||||
@@ -34,6 +43,7 @@ describe("createToolExecuteAfterHandler", () => {
|
||||
beforeEach(() => {
|
||||
appendTranscriptEntry.mockClear()
|
||||
transcriptCalls.length = 0
|
||||
postToolUseResult = { warnings: [] }
|
||||
})
|
||||
|
||||
it("#given diff-heavy metadata #when transcript entry is appended #then it keeps concise output with compact metadata", async () => {
|
||||
@@ -129,4 +139,42 @@ describe("createToolExecuteAfterHandler", () => {
|
||||
expect(filediff).not.toHaveProperty("before")
|
||||
expect(filediff).not.toHaveProperty("after")
|
||||
})
|
||||
|
||||
it("#given multiline PostToolUse context on empty tool output #when output is appended #then it renders clean normalized sections", async () => {
|
||||
// given
|
||||
postToolUseResult = {
|
||||
warnings: ["\r\nWarning line\r\n warning detail\r\n"],
|
||||
additionalContext: "\r\nContext line\r\n context detail\r",
|
||||
message: "\r\nMessage line\r\nmessage detail\r\n",
|
||||
}
|
||||
const handler = createToolExecuteAfterHandler(
|
||||
{
|
||||
client: {
|
||||
tui: {
|
||||
showToast: async () => ({}),
|
||||
},
|
||||
},
|
||||
directory: "/repo",
|
||||
} as never,
|
||||
{ disabledHooks: [] }
|
||||
)
|
||||
const output = {
|
||||
title: "tool",
|
||||
output: "",
|
||||
metadata: {},
|
||||
}
|
||||
|
||||
// when
|
||||
await handler({ tool: "write", sessionID: "ses_test", callID: "call_test" }, output)
|
||||
|
||||
// then
|
||||
expect(output.output).toBe(
|
||||
[
|
||||
"Warning line\n warning detail",
|
||||
"Context line\n context detail",
|
||||
"Message line\nmessage detail",
|
||||
].join("\n\n")
|
||||
)
|
||||
expect(output.output).not.toContain("\r")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getToolInput } from "../tool-input-cache"
|
||||
import { appendTranscriptEntry, getTranscriptPath } from "../transcript"
|
||||
import type { PluginConfig } from "../types"
|
||||
import { isHookDisabled } from "../../../shared"
|
||||
import { normalizeHookText, normalizeHookTextList } from "../hook-text"
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
@@ -70,6 +71,17 @@ function buildTranscriptToolOutput(outputText: string, metadata: unknown): Recor
|
||||
return compactOutput
|
||||
}
|
||||
|
||||
function appendHookSections(outputText: string, sections: readonly (string | undefined)[]): string {
|
||||
const normalizedSections = normalizeHookTextList(sections)
|
||||
if (normalizedSections.length === 0) {
|
||||
return outputText
|
||||
}
|
||||
if (outputText.length === 0) {
|
||||
return normalizedSections.join("\n\n")
|
||||
}
|
||||
return [outputText, ...normalizedSections].join("\n\n")
|
||||
}
|
||||
|
||||
export function createToolExecuteAfterHandler(ctx: PluginInput, config: PluginConfig) {
|
||||
return async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
@@ -134,13 +146,11 @@ export function createToolExecuteAfterHandler(ctx: PluginInput, config: PluginCo
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
output.output = `${output.output}\n\n${result.warnings.join("\n")}`
|
||||
}
|
||||
|
||||
if (result.message) {
|
||||
output.output = `${output.output}\n\n${result.message}`
|
||||
}
|
||||
output.output = appendHookSections(output.output, [
|
||||
...(result.warnings ?? []),
|
||||
...(normalizeHookText(result.additionalContext) === undefined ? [] : [result.additionalContext]),
|
||||
...(result.message === undefined ? [] : [result.message]),
|
||||
])
|
||||
|
||||
if (result.hookName) {
|
||||
ctx.client.tui
|
||||
|
||||
Reference in New Issue
Block a user