mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:18:28 +00:00
fix: emit message_sent hook for all successful outbound paths (#15104)
This commit is contained in:
@@ -14,6 +14,12 @@ import {
|
|||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })),
|
appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })),
|
||||||
}));
|
}));
|
||||||
|
const hookMocks = vi.hoisted(() => ({
|
||||||
|
runner: {
|
||||||
|
hasHooks: vi.fn(() => false),
|
||||||
|
runMessageSent: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../../config/sessions.js", async () => {
|
vi.mock("../../config/sessions.js", async () => {
|
||||||
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
|
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
|
||||||
@@ -24,12 +30,19 @@ vi.mock("../../config/sessions.js", async () => {
|
|||||||
appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript,
|
appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
vi.mock("../../plugins/hook-runner-global.js", () => ({
|
||||||
|
getGlobalHookRunner: () => hookMocks.runner,
|
||||||
|
}));
|
||||||
|
|
||||||
const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js");
|
const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js");
|
||||||
|
|
||||||
describe("deliverOutboundPayloads", () => {
|
describe("deliverOutboundPayloads", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePluginRegistry(defaultRegistry);
|
setActivePluginRegistry(defaultRegistry);
|
||||||
|
hookMocks.runner.hasHooks.mockReset();
|
||||||
|
hookMocks.runner.hasHooks.mockReturnValue(false);
|
||||||
|
hookMocks.runner.runMessageSent.mockReset();
|
||||||
|
hookMocks.runner.runMessageSent.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -422,6 +435,86 @@ describe("deliverOutboundPayloads", () => {
|
|||||||
expect.objectContaining({ text: "report.pdf" }),
|
expect.objectContaining({ text: "report.pdf" }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits message_sent success for text-only deliveries", async () => {
|
||||||
|
hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "message_sent");
|
||||||
|
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
||||||
|
|
||||||
|
await deliverOutboundPayloads({
|
||||||
|
cfg: {},
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: "+1555",
|
||||||
|
payloads: [{ text: "hello" }],
|
||||||
|
deps: { sendWhatsApp },
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ to: "+1555", content: "hello", success: true }),
|
||||||
|
expect.objectContaining({ channelId: "whatsapp" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits message_sent success for sendPayload deliveries", async () => {
|
||||||
|
hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "message_sent");
|
||||||
|
const sendPayload = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "mx-1" });
|
||||||
|
const sendText = vi.fn();
|
||||||
|
const sendMedia = vi.fn();
|
||||||
|
setActivePluginRegistry(
|
||||||
|
createTestRegistry([
|
||||||
|
{
|
||||||
|
pluginId: "matrix",
|
||||||
|
source: "test",
|
||||||
|
plugin: createOutboundTestPlugin({
|
||||||
|
id: "matrix",
|
||||||
|
outbound: { deliveryMode: "direct", sendPayload, sendText, sendMedia },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await deliverOutboundPayloads({
|
||||||
|
cfg: {},
|
||||||
|
channel: "matrix",
|
||||||
|
to: "!room:1",
|
||||||
|
payloads: [{ text: "payload text", channelData: { mode: "custom" } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ to: "!room:1", content: "payload text", success: true }),
|
||||||
|
expect.objectContaining({ channelId: "matrix" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits message_sent failure when delivery errors", async () => {
|
||||||
|
hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "message_sent");
|
||||||
|
const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
deliverOutboundPayloads({
|
||||||
|
cfg: {},
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: "+1555",
|
||||||
|
payloads: [{ text: "hi" }],
|
||||||
|
deps: { sendWhatsApp },
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("downstream failed");
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
to: "+1555",
|
||||||
|
content: "hi",
|
||||||
|
success: false,
|
||||||
|
error: "downstream failed",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({ channelId: "whatsapp" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptyRegistry = createTestRegistry([]);
|
const emptyRegistry = createTestRegistry([]);
|
||||||
|
|||||||
@@ -345,6 +345,25 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []),
|
mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []),
|
||||||
channelData: payload.channelData,
|
channelData: payload.channelData,
|
||||||
};
|
};
|
||||||
|
const emitMessageSent = (success: boolean, error?: string) => {
|
||||||
|
if (!hookRunner?.hasHooks("message_sent")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void hookRunner
|
||||||
|
.runMessageSent(
|
||||||
|
{
|
||||||
|
to,
|
||||||
|
content: payloadSummary.text,
|
||||||
|
success,
|
||||||
|
...(error ? { error } : {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channelId: channel,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
throwIfAborted(abortSignal);
|
throwIfAborted(abortSignal);
|
||||||
|
|
||||||
@@ -378,6 +397,7 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
params.onPayload?.(payloadSummary);
|
params.onPayload?.(payloadSummary);
|
||||||
if (handler.sendPayload && effectivePayload.channelData) {
|
if (handler.sendPayload && effectivePayload.channelData) {
|
||||||
results.push(await handler.sendPayload(effectivePayload));
|
results.push(await handler.sendPayload(effectivePayload));
|
||||||
|
emitMessageSent(true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (payloadSummary.mediaUrls.length === 0) {
|
if (payloadSummary.mediaUrls.length === 0) {
|
||||||
@@ -386,6 +406,7 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
} else {
|
} else {
|
||||||
await sendTextChunks(payloadSummary.text);
|
await sendTextChunks(payloadSummary.text);
|
||||||
}
|
}
|
||||||
|
emitMessageSent(true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,40 +421,9 @@ export async function deliverOutboundPayloads(params: {
|
|||||||
results.push(await handler.sendMedia(caption, url));
|
results.push(await handler.sendMedia(caption, url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Run message_sent plugin hook (fire-and-forget) on success
|
emitMessageSent(true);
|
||||||
if (hookRunner?.hasHooks("message_sent")) {
|
|
||||||
void hookRunner
|
|
||||||
.runMessageSent(
|
|
||||||
{
|
|
||||||
to,
|
|
||||||
content: payloadSummary.text,
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
channelId: channel,
|
|
||||||
accountId: accountId ?? undefined,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Run message_sent plugin hook on failure (fire-and-forget)
|
emitMessageSent(false, err instanceof Error ? err.message : String(err));
|
||||||
if (hookRunner?.hasHooks("message_sent")) {
|
|
||||||
void hookRunner
|
|
||||||
.runMessageSent(
|
|
||||||
{
|
|
||||||
to,
|
|
||||||
content: payloadSummary.text,
|
|
||||||
success: false,
|
|
||||||
error: err instanceof Error ? err.message : String(err),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
channelId: channel,
|
|
||||||
accountId: accountId ?? undefined,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
if (!params.bestEffort) {
|
if (!params.bestEffort) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user