diff --git a/src/gateway/hooks-mapping.test.ts b/src/gateway/hooks-mapping.test.ts index 0d8e56b81..dc8045d82 100644 --- a/src/gateway/hooks-mapping.test.ts +++ b/src/gateway/hooks-mapping.test.ts @@ -74,6 +74,61 @@ describe("hooks mapping", () => { } }); + it("treats null transform as a handled skip", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdis-hooks-skip-")); + const modPath = path.join(dir, "transform.mjs"); + fs.writeFileSync(modPath, "export default () => null;"); + + const mappings = resolveHookMappings({ + transformsDir: dir, + mappings: [ + { + match: { path: "skip" }, + action: "agent", + transform: { module: "transform.mjs" }, + }, + ], + }); + + const result = await applyHookMappings(mappings, { + payload: {}, + headers: {}, + url: new URL("http://127.0.0.1:18789/hooks/skip"), + path: "skip", + }); + + expect(result?.ok).toBe(true); + if (result?.ok) { + expect(result.action).toBeNull(); + expect("skipped" in result).toBe(true); + } + }); + + it("prefers explicit mappings over presets", async () => { + const mappings = resolveHookMappings({ + presets: ["gmail"], + mappings: [ + { + id: "override", + match: { path: "gmail" }, + action: "agent", + messageTemplate: "Override subject: {{messages[0].subject}}", + }, + ], + }); + const result = await applyHookMappings(mappings, { + payload: { messages: [{ subject: "Hello" }] }, + headers: {}, + url: baseUrl, + path: "gmail", + }); + expect(result?.ok).toBe(true); + if (result?.ok) { + expect(result.action.kind).toBe("agent"); + expect(result.action.message).toBe("Override subject: Hello"); + } + }); + it("rejects missing message", async () => { const mappings = resolveHookMappings({ mappings: [{ match: { path: "noop" }, action: "agent" }], diff --git a/src/gateway/hooks-mapping.ts b/src/gateway/hooks-mapping.ts index 0b2ee8132..0824b9a02 100644 --- a/src/gateway/hooks-mapping.ts +++ b/src/gateway/hooks-mapping.ts @@ -70,6 +70,7 @@ export type HookAction = export type HookMappingResult = | { ok: true; action: HookAction } + | { ok: true; action: null; skipped: true } | { ok: false; error: string }; const hookPresetMappings: Record = { @@ -145,7 +146,9 @@ export async function applyHookMappings( if (mapping.transform) { const transform = await loadTransform(mapping.transform); override = await transform(ctx); - if (override === null) return null; + if (override === null) { + return { ok: true, action: null, skipped: true }; + } } const merged = mergeAction(base.action, override, mapping.action); diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 56e090504..7fb2f95f5 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -1706,6 +1706,11 @@ export async function startGatewayServer( sendJson(res, 400, { ok: false, error: mapped.error }); return true; } + if (mapped.action === null) { + res.statusCode = 204; + res.end(); + return true; + } if (mapped.action.kind === "wake") { dispatchWakeHook({ text: mapped.action.text,