YeonGyu-Kim d5fd918bff fix: guard output.output in tool after-hooks for MCP tools (#1720)
MCP tool responses can have undefined output.output, causing TypeError
crashes in tool.execute.after hooks.

Changes:
- comment-checker/hook.ts: guard output.output with ?? '' before toLowerCase()
- edit-error-recovery/hook.ts: guard output.output with ?? '' before toLowerCase()
- task-resume-info/hook.ts: extract output.output ?? '' into outputText before all string operations
- Added tests for undefined output.output in edit-error-recovery and task-resume-info
2026-02-11 15:49:56 +09:00

144 lines
4.8 KiB
TypeScript

import { describe, it, expect, beforeEach } from "bun:test"
import { createEditErrorRecoveryHook, EDIT_ERROR_REMINDER, EDIT_ERROR_PATTERNS } from "./index"
describe("createEditErrorRecoveryHook", () => {
let hook: ReturnType<typeof createEditErrorRecoveryHook>
beforeEach(() => {
hook = createEditErrorRecoveryHook({} as any)
})
describe("tool.execute.after", () => {
const createInput = (tool: string) => ({
tool,
sessionID: "test-session",
callID: "test-call-id",
})
const createOutput = (outputText: string) => ({
title: "Edit",
output: outputText,
metadata: {},
})
describe("#given Edit tool with oldString/newString same error", () => {
describe("#when the error message is detected", () => {
it("#then should append the recovery reminder", async () => {
const input = createInput("Edit")
const output = createOutput("Error: oldString and newString must be different")
await hook["tool.execute.after"](input, output)
expect(output.output).toContain(EDIT_ERROR_REMINDER)
expect(output.output).toContain("oldString and newString must be different")
})
})
describe("#when the error appears without Error prefix", () => {
it("#then should still detect and append reminder", async () => {
const input = createInput("Edit")
const output = createOutput("oldString and newString must be different")
await hook["tool.execute.after"](input, output)
expect(output.output).toContain(EDIT_ERROR_REMINDER)
})
})
})
describe("#given Edit tool with oldString not found error", () => {
describe("#when oldString not found in content", () => {
it("#then should append the recovery reminder", async () => {
const input = createInput("Edit")
const output = createOutput("Error: oldString not found in content")
await hook["tool.execute.after"](input, output)
expect(output.output).toContain(EDIT_ERROR_REMINDER)
})
})
})
describe("#given Edit tool with multiple matches error", () => {
describe("#when oldString found multiple times", () => {
it("#then should append the recovery reminder", async () => {
const input = createInput("Edit")
const output = createOutput(
"Error: oldString found multiple times and requires more code context to uniquely identify the intended match"
)
await hook["tool.execute.after"](input, output)
expect(output.output).toContain(EDIT_ERROR_REMINDER)
})
})
})
describe("#given non-Edit tool", () => {
describe("#when tool is not Edit", () => {
it("#then should not modify output", async () => {
const input = createInput("Read")
const originalOutput = "some output"
const output = createOutput(originalOutput)
await hook["tool.execute.after"](input, output)
expect(output.output).toBe(originalOutput)
})
})
})
describe("#given Edit tool with successful output", () => {
describe("#when no error in output", () => {
it("#then should not modify output", async () => {
const input = createInput("Edit")
const originalOutput = "File edited successfully"
const output = createOutput(originalOutput)
await hook["tool.execute.after"](input, output)
expect(output.output).toBe(originalOutput)
})
})
})
describe("#given MCP tool with undefined output.output", () => {
describe("#when output.output is undefined", () => {
it("#then should not crash", async () => {
const input = createInput("Edit")
const output = {
title: "Edit",
output: undefined as unknown as string,
metadata: {},
}
await hook["tool.execute.after"](input, output)
expect(output.output).toBeUndefined()
})
})
})
describe("#given case insensitive tool name", () => {
describe("#when tool is 'edit' lowercase", () => {
it("#then should still detect and append reminder", async () => {
const input = createInput("edit")
const output = createOutput("oldString and newString must be different")
await hook["tool.execute.after"](input, output)
expect(output.output).toContain(EDIT_ERROR_REMINDER)
})
})
})
})
describe("EDIT_ERROR_PATTERNS", () => {
it("#then should contain all known Edit error patterns", () => {
expect(EDIT_ERROR_PATTERNS).toContain("oldString and newString must be different")
expect(EDIT_ERROR_PATTERNS).toContain("oldString not found")
expect(EDIT_ERROR_PATTERNS).toContain("oldString found multiple times")
})
})
})