From 5d1f1d93621bb54590ecbca47e2538864dfd045a Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:55:45 -0500 Subject: [PATCH] fix: preserve reset hook sender policy context --- extensions/acpx/src/runtime.test.ts | 16 ++++--- .../msteams/src/reply-dispatcher.test.ts | 2 +- .../reply/commands-reset-hooks.test.ts | 42 +++++++++++++++++++ src/auto-reply/reply/commands-reset-hooks.ts | 3 ++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 888935ffd80..4f598f54438 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -1,11 +1,15 @@ -import type { AcpSessionStore } from "acpx/runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { AcpRuntime } from "../runtime-api.js"; import { AcpxRuntime } from "./runtime.js"; -function makeRuntime(baseStore: AcpSessionStore): { +type TestSessionStore = { + load(sessionId: string): Promise | undefined>; + save(record: Record): Promise; +}; + +function makeRuntime(baseStore: TestSessionStore): { runtime: AcpxRuntime; - wrappedStore: AcpSessionStore & { markFresh: (sessionKey: string) => void }; + wrappedStore: TestSessionStore & { markFresh: (sessionKey: string) => void }; delegate: { close: AcpRuntime["close"] }; } { const runtime = new AcpxRuntime({ @@ -22,7 +26,7 @@ function makeRuntime(baseStore: AcpSessionStore): { runtime, wrappedStore: ( runtime as unknown as { - sessionStore: AcpSessionStore & { markFresh: (sessionKey: string) => void }; + sessionStore: TestSessionStore & { markFresh: (sessionKey: string) => void }; } ).sessionStore, delegate: (runtime as unknown as { delegate: { close: AcpRuntime["close"] } }).delegate, @@ -35,7 +39,7 @@ describe("AcpxRuntime fresh reset wrapper", () => { }); it("keeps stale persistent loads hidden until a fresh record is saved", async () => { - const baseStore: AcpSessionStore = { + const baseStore: TestSessionStore = { load: vi.fn(async () => ({ acpxRecordId: "stale" }) as never), save: vi.fn(async () => {}), }; @@ -68,7 +72,7 @@ describe("AcpxRuntime fresh reset wrapper", () => { }); it("marks the session fresh after discardPersistentState close", async () => { - const baseStore: AcpSessionStore = { + const baseStore: TestSessionStore = { load: vi.fn(async () => ({ acpxRecordId: "stale" }) as never), save: vi.fn(async () => {}), }; diff --git a/extensions/msteams/src/reply-dispatcher.test.ts b/extensions/msteams/src/reply-dispatcher.test.ts index 7409509d0ee..22f86d908ab 100644 --- a/extensions/msteams/src/reply-dispatcher.test.ts +++ b/extensions/msteams/src/reply-dispatcher.test.ts @@ -163,7 +163,7 @@ describe("createMSTeamsReplyDispatcher", () => { if (!lastCreatedDispatcher) { throw new Error("createDispatcher must be called first"); } - await lastCreatedDispatcher.replyOptions.onPartialReply?.({ text }); + lastCreatedDispatcher.replyOptions.onPartialReply?.({ text }); } it("sends an informative status update on reply start for personal chats", async () => { diff --git a/src/auto-reply/reply/commands-reset-hooks.test.ts b/src/auto-reply/reply/commands-reset-hooks.test.ts index efd4c858ded..00c9a9d2333 100644 --- a/src/auto-reply/reply/commands-reset-hooks.test.ts +++ b/src/auto-reply/reply/commands-reset-hooks.test.ts @@ -6,6 +6,9 @@ import type { HandleCommandsParams } from "./commands-types.js"; import { parseInlineDirectives } from "./directive-handling.parse.js"; const triggerInternalHookMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const routeReplyMock = vi.hoisted(() => + vi.fn<(params: unknown) => Promise<{ ok: boolean }>>(async () => ({ ok: true })), +); const resetMocks = vi.hoisted(() => ({ resetConfiguredBindingTargetInPlace: vi.fn().mockResolvedValue({ ok: true as const }), resolveBoundAcpThreadSessionKey: vi.fn(() => undefined as string | undefined), @@ -49,6 +52,10 @@ vi.mock("./commands-handlers.runtime.js", () => ({ loadCommandHandlers: () => [], })); +vi.mock("./route-reply.runtime.js", () => ({ + routeReply: (params: unknown) => routeReplyMock(params), +})); + function buildResetParams( commandBody: string, cfg: OpenClawConfig, @@ -102,6 +109,7 @@ describe("handleCommands reset hooks", () => { vi.clearAllMocks(); resetMocks.resetConfiguredBindingTargetInPlace.mockResolvedValue({ ok: true }); resetMocks.resolveBoundAcpThreadSessionKey.mockReturnValue(undefined); + triggerInternalHookMock.mockResolvedValue(undefined); }); it("triggers hooks for /new commands", async () => { @@ -213,4 +221,38 @@ describe("handleCommands reset hooks", () => { expect(params.ctx.CommandBody).toBe("who are you"); expect(params.ctx.AcpDispatchTailAfterReset).toBe(true); }); + + it("forwards non-id sender fields when reset hooks emit routed replies", async () => { + triggerInternalHookMock.mockImplementationOnce(async (event: { messages: string[] }) => { + event.messages.push("Reset hook says hi"); + }); + const params = buildResetParams( + "/new", + { + commands: { text: true }, + channels: { whatsapp: { allowFrom: ["*"] } }, + } as OpenClawConfig, + { + SenderId: "id:whatsapp:123", + SenderName: "Alice", + SenderUsername: "alice_u", + SenderE164: "+15551234567", + OriginatingChannel: "whatsapp", + OriginatingTo: "group:ops", + MessageThreadId: "thread-1", + }, + ); + + await maybeHandleResetCommand(params); + + expect(routeReplyMock).toHaveBeenCalledWith( + expect.objectContaining({ + requesterSenderId: "id:whatsapp:123", + requesterSenderName: "Alice", + requesterSenderUsername: "alice_u", + requesterSenderE164: "+15551234567", + threadId: "thread-1", + }), + ); + }); }); diff --git a/src/auto-reply/reply/commands-reset-hooks.ts b/src/auto-reply/reply/commands-reset-hooks.ts index 2f0c9fde55c..2e529dfb6ae 100644 --- a/src/auto-reply/reply/commands-reset-hooks.ts +++ b/src/auto-reply/reply/commands-reset-hooks.ts @@ -128,6 +128,9 @@ export async function emitResetCommandHooks(params: { sessionKey: params.sessionKey, accountId: params.ctx.AccountId, requesterSenderId: params.command.senderId, + requesterSenderName: params.ctx.SenderName, + requesterSenderUsername: params.ctx.SenderUsername, + requesterSenderE164: params.ctx.SenderE164, threadId: params.ctx.MessageThreadId, cfg: params.cfg, });