diff --git a/src/plugin/tool-execute-after-metadata-recovery.test.ts b/src/plugin/tool-execute-after-metadata-recovery.test.ts new file mode 100644 index 000000000..b86015d91 --- /dev/null +++ b/src/plugin/tool-execute-after-metadata-recovery.test.ts @@ -0,0 +1,111 @@ +/// + +import { beforeEach, describe, expect, it } from "bun:test" +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +import { clearPendingStore } from "../features/tool-metadata-store" +import { _flushForTesting, _resetLoggerForTesting, _setLoggerForTesting } from "../shared/logger" +import { createToolExecuteAfterHandler } from "./tool-execute-after" + +function readLogIfPresent(filePath: string): string { + return existsSync(filePath) ? readFileSync(filePath, "utf8") : "" +} + +describe("createToolExecuteAfterHandler metadata recovery", () => { + beforeEach(() => { + clearPendingStore() + _resetLoggerForTesting() + }) + + it("#given builtin tool has no recoverable metadata #when tool.execute.after runs #then it fails open without warning spam", async () => { + // given + const logDir = mkdtempSync(join(tmpdir(), "omo-tool-after-")) + const logPath = join(logDir, "omo.log") + _setLoggerForTesting({ filePath: logPath }) + const handler = createToolExecuteAfterHandler({ + ctx: { directory: "/repo" } as never, + hooks: {} as never, + }) + const output = { title: "result", output: "read output", metadata: {} } + + try { + // when + await handler( + { tool: "read", sessionID: "ses_parent", callID: "call_read" }, + output, + ) + _flushForTesting() + + // then + expect(output).toEqual({ title: "result", output: "read output", metadata: {} }) + expect(readLogIfPresent(logPath)).not.toContain("Unable to recover stored metadata") + } finally { + _resetLoggerForTesting() + rmSync(logDir, { force: true, recursive: true }) + } + }) + + it("#given call_omo_agent has no recoverable store entry #when tool.execute.after runs #then it fails open without warning spam", async () => { + // given + const logDir = mkdtempSync(join(tmpdir(), "omo-tool-after-")) + const logPath = join(logDir, "omo.log") + _setLoggerForTesting({ filePath: logPath }) + const handler = createToolExecuteAfterHandler({ + ctx: { directory: "/repo" } as never, + hooks: {} as never, + }) + const output = { title: "result", output: "agent output", metadata: {} } + + try { + // when + await handler( + { tool: "call_omo_agent", sessionID: "ses_parent", callID: "call_agent" }, + output, + ) + _flushForTesting() + + // then + expect(output).toEqual({ title: "result", output: "agent output", metadata: {} }) + expect(readLogIfPresent(logPath)).not.toContain("Unable to recover stored metadata") + } finally { + _resetLoggerForTesting() + rmSync(logDir, { force: true, recursive: true }) + } + }) + + it("#given metadata-linked tool has stale metadata #when tool.execute.after runs #then it warns and still completes hooks", async () => { + // given + const logDir = mkdtempSync(join(tmpdir(), "omo-tool-after-")) + const logPath = join(logDir, "omo.log") + _setLoggerForTesting({ filePath: logPath }) + let hookRan = false + const handler = createToolExecuteAfterHandler({ + ctx: { directory: "/repo" } as never, + hooks: { + categorySkillReminder: { + "tool.execute.after": async () => { + hookRan = true + }, + }, + } as never, + }) + + try { + // when + await handler( + { tool: "task", sessionID: "ses_parent", callID: "call_missing" }, + { title: "result", output: "task output", metadata: {} }, + ) + _flushForTesting() + + // then + expect(hookRan).toBe(true) + expect(readLogIfPresent(logPath)).toContain("Unable to recover stored metadata") + } finally { + _resetLoggerForTesting() + rmSync(logDir, { force: true, recursive: true }) + } + }) +}) diff --git a/src/plugin/tool-execute-after.ts b/src/plugin/tool-execute-after.ts index 859bbb065..75bd1c0ee 100644 --- a/src/plugin/tool-execute-after.ts +++ b/src/plugin/tool-execute-after.ts @@ -6,6 +6,12 @@ import type { PluginContext } from "./types" const VERIFICATION_ATTEMPT_PATTERN = /(.*?)<\/ulw_verification_attempt_id>/i +const METADATA_LINKED_TOOLS = new Set([ + "background_output", + "edit", + "task", +]) + type ToolExecuteAfterInput = { readonly tool: string readonly sessionID: string @@ -40,6 +46,10 @@ function getPluginDirectory(ctx: PluginContext): string | null { return null } +function expectsRecoverableMetadata(tool: string): boolean { + return METADATA_LINKED_TOOLS.has(tool) +} + export function createToolExecuteAfterHandler(args: { ctx: PluginContext hooks: CreatedHooks @@ -84,7 +94,7 @@ export function createToolExecuteAfterHandler(args: { output.metadata = { ...output.metadata, ...stored.metadata } } } - } else if (!nativeSessionId) { + } else if (!nativeSessionId && expectsRecoverableMetadata(input.tool)) { log("[tool-execute-after] Unable to recover stored metadata and no native session linkage was present", { tool: input.tool, sessionID: input.sessionID,