fix(matrix): suppress previews for modifying hooks

This commit is contained in:
joshavant
2026-07-28 09:20:47 -05:00
committed by Josh Avant
parent 2b8e6395b9
commit ba85271dbe
2 changed files with 67 additions and 2 deletions
@@ -53,6 +53,15 @@ const resolveMatrixMentionsForBodyMock = vi.hoisted(() =>
};
}),
);
const getGlobalHookRunnerMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/plugin-runtime")>();
return {
...actual,
getGlobalHookRunner: getGlobalHookRunnerMock,
};
});
vi.mock("../send.js", () => ({
editMessageMatrix: editMessageMatrixMock,
@@ -116,6 +125,7 @@ async function writeMatrixSessionMeta(
beforeEach(() => {
sessionBindingTesting.resetSessionBindingAdaptersForTests();
installMatrixMonitorTestRuntime();
getGlobalHookRunnerMock.mockReset().mockReturnValue(null);
prepareMatrixSingleTextMock.mockReset().mockImplementation((text: string) => {
const trimmedText = text.trim();
return {
@@ -2988,6 +2998,53 @@ describe("matrix monitor handler draft streaming", () => {
await finish();
});
it("preserves provider previews for observer-only hooks", async () => {
getGlobalHookRunnerMock.mockReturnValue({
hasHooks: vi.fn((hookName: string) => hookName === "message_sent"),
});
const { dispatch } = createStreamingHarness({ streaming: "partial" });
const { deliver, opts, finish } = await dispatch();
opts.onPartialReply?.({ text: "Visible preview" });
await waitForMatrixState(() => {
expect(sendSingleTextMessageMatrixMock).toHaveBeenCalledTimes(1);
});
await deliver({ text: "Visible preview" }, { kind: "final" });
expectEditLiveFlag("$draft1", "Visible preview", false);
expect(deliverMatrixRepliesMock).not.toHaveBeenCalled();
await finish();
});
it.each([
{ label: "reply_payload_sending", hooks: ["reply_payload_sending"] },
{ label: "message_sending", hooks: ["message_sending"] },
{
label: "both modifying hooks",
hooks: ["reply_payload_sending", "message_sending"],
},
])("suppresses provider previews when $label is registered", async ({ hooks }) => {
const registered = new Set(hooks);
getGlobalHookRunnerMock.mockReturnValue({
hasHooks: vi.fn((hookName: string) => registered.has(hookName)),
});
const { dispatch } = createStreamingHarness({
previewToolProgressEnabled: true,
streaming: "progress",
});
const { deliver, opts, finish } = await dispatch();
expect(opts.onPartialReply).toBeUndefined();
expect(opts.onToolStart).toBeUndefined();
expect(opts.suppressDefaultToolProgressMessages).toBeUndefined();
await deliver({ text: "Durable final" }, { kind: "final" });
expect(sendSingleTextMessageMatrixMock).not.toHaveBeenCalled();
expect(editMessageMatrixMock).not.toHaveBeenCalled();
expect(deliverMatrixRepliesMock).toHaveBeenCalledTimes(1);
await finish();
});
it("streams tool progress into the Matrix draft preview when enabled", async () => {
const { dispatch, redactEventMock } = createStreamingHarness({
previewToolProgressEnabled: true,
@@ -5,6 +5,7 @@ import {
} from "openclaw/plugin-sdk/channel-inbound";
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
@@ -388,9 +389,16 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
});
},
});
// Matrix drafts are provider-visible before outbound modifiers run. Keep them off when a
// hook can rewrite or cancel so the original payload cannot escape the delivery gate.
const hookRunner = getGlobalHookRunner();
const allowProviderPreview = !(
(hookRunner?.hasHooks("reply_payload_sending") ?? false) ||
(hookRunner?.hasHooks("message_sending") ?? false)
);
const draftController = await createMatrixDraftController({
streaming,
previewToolProgressEnabled,
streaming: allowProviderPreview ? streaming : "off",
previewToolProgressEnabled: allowProviderPreview && previewToolProgressEnabled,
replyToMode,
messageId,
threadTarget,