fix: preserve reset hook sender policy context

This commit is contained in:
Tak Hoffman
2026-04-10 17:55:45 -05:00
parent f3abc0c076
commit 5d1f1d9362
4 changed files with 56 additions and 7 deletions

View File

@@ -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<Record<string, unknown> | undefined>;
save(record: Record<string, unknown>): Promise<void>;
};
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 () => {}),
};

View File

@@ -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 () => {

View File

@@ -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",
}),
);
});
});

View File

@@ -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,
});