diff --git a/src/gateway/hooks-mapping.test.ts b/src/gateway/hooks-mapping.test.ts index 8900ffd07..696d3ba51 100644 --- a/src/gateway/hooks-mapping.test.ts +++ b/src/gateway/hooks-mapping.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import type { HookAction, HookMappingResolved } from "./hooks-mapping.js"; import { applyHookMappings, resolveHookMappings } from "./hooks-mapping.js"; const baseUrl = new URL("http://127.0.0.1:18789/hooks/gmail"); @@ -32,8 +33,7 @@ describe("hooks mapping", () => { path: "gmail", }); expect(result?.ok).toBe(true); - if (result?.ok) { - expect(result.action.kind).toBe("agent"); + if (result?.ok && result.action?.kind === "agent") { expect(result.action.message).toBe("Subject: Hello"); } }); @@ -57,7 +57,7 @@ describe("hooks mapping", () => { path: "gmail", }); expect(result?.ok).toBe(true); - if (result?.ok && result.action.kind === "agent") { + if (result?.ok && result.action?.kind === "agent") { expect(result.action.model).toBe("openai/gpt-4.1-mini"); } }); @@ -90,11 +90,8 @@ describe("hooks mapping", () => { }); expect(result?.ok).toBe(true); - if (result?.ok) { - expect(result.action.kind).toBe("wake"); - if (result.action.kind === "wake") { - expect(result.action.text).toBe("Ping Ada"); - } + if (result?.ok && result.action?.kind === "wake") { + expect(result.action.text).toBe("Ping Ada"); } }); @@ -147,8 +144,7 @@ describe("hooks mapping", () => { path: "gmail", }); expect(result?.ok).toBe(true); - if (result?.ok) { - expect(result.action.kind).toBe("agent"); + if (result?.ok && result.action?.kind === "agent") { expect(result.action.message).toBe("Override subject: Hello"); } }); @@ -166,3 +162,89 @@ describe("hooks mapping", () => { expect(result?.ok).toBe(false); }); }); + +describe("HookMappingResolved", () => { + it("includes cleanup fields", () => { + const resolved: HookMappingResolved = { + id: "test", + matchPath: "test", + action: "agent", + cleanup: "delete", + cleanupDelayMinutes: 5, + }; + expect(resolved.cleanup).toBe("delete"); + expect(resolved.cleanupDelayMinutes).toBe(5); + }); +}); + +describe("HookAction", () => { + it("agent action includes cleanup fields", () => { + const action: HookAction = { + kind: "agent", + message: "test", + wakeMode: "now", + cleanup: "delete", + cleanupDelayMinutes: 5, + }; + expect(action.kind).toBe("agent"); + if (action.kind === "agent") { + expect(action.cleanup).toBe("delete"); + expect(action.cleanupDelayMinutes).toBe(5); + } + }); +}); + +describe("applyHookMappings cleanup propagation", () => { + it("propagates cleanup option from config to action", async () => { + const mappings = resolveHookMappings({ + mappings: [ + { + id: "test", + match: { path: "test" }, + action: "agent", + messageTemplate: "hello", + cleanup: "delete", + cleanupDelayMinutes: 5, + }, + ], + }); + + const result = await applyHookMappings(mappings, { + payload: {}, + headers: {}, + url: new URL("http://localhost/hooks/test"), + path: "test", + }); + + expect(result?.ok).toBe(true); + if (result?.ok && result.action?.kind === "agent") { + expect(result.action.cleanup).toBe("delete"); + expect(result.action.cleanupDelayMinutes).toBe(5); + } + }); + + it("defaults cleanup to undefined when not specified", async () => { + const mappings = resolveHookMappings({ + mappings: [ + { + id: "test", + match: { path: "test" }, + action: "agent", + messageTemplate: "hello", + }, + ], + }); + + const result = await applyHookMappings(mappings, { + payload: {}, + headers: {}, + url: new URL("http://localhost/hooks/test"), + path: "test", + }); + + expect(result?.ok).toBe(true); + if (result?.ok && result.action?.kind === "agent") { + expect(result.action.cleanup).toBeUndefined(); + } + }); +}); diff --git a/src/gateway/hooks-mapping.ts b/src/gateway/hooks-mapping.ts index 2ebf9b136..9b4538380 100644 --- a/src/gateway/hooks-mapping.ts +++ b/src/gateway/hooks-mapping.ts @@ -21,6 +21,8 @@ export type HookMappingResolved = { model?: string; thinking?: string; timeoutSeconds?: number; + cleanup?: "delete" | "keep"; + cleanupDelayMinutes?: number; transform?: HookMappingTransformResolved; }; @@ -55,6 +57,8 @@ export type HookAction = model?: string; thinking?: string; timeoutSeconds?: number; + cleanup?: "delete" | "keep"; + cleanupDelayMinutes?: number; }; export type HookMappingResult = @@ -94,6 +98,8 @@ type HookTransformResult = Partial<{ model: string; thinking: string; timeoutSeconds: number; + cleanup: "delete" | "keep"; + cleanupDelayMinutes: number; }> | null; type HookTransformFn = ( @@ -191,6 +197,8 @@ function normalizeHookMapping( model: mapping.model, thinking: mapping.thinking, timeoutSeconds: mapping.timeoutSeconds, + cleanup: mapping.cleanup, + cleanupDelayMinutes: mapping.cleanupDelayMinutes, transform, }; } @@ -237,6 +245,8 @@ function buildActionFromMapping( model: renderOptional(mapping.model, ctx), thinking: renderOptional(mapping.thinking, ctx), timeoutSeconds: mapping.timeoutSeconds, + cleanup: mapping.cleanup, + cleanupDelayMinutes: mapping.cleanupDelayMinutes, }, }; } @@ -277,6 +287,8 @@ function mergeAction( model: override.model ?? baseAgent?.model, thinking: override.thinking ?? baseAgent?.thinking, timeoutSeconds: override.timeoutSeconds ?? baseAgent?.timeoutSeconds, + cleanup: override.cleanup ?? baseAgent?.cleanup, + cleanupDelayMinutes: override.cleanupDelayMinutes ?? baseAgent?.cleanupDelayMinutes, }); }