mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
test(agents): add missing announce delivery regressions
This commit is contained in:
@@ -825,6 +825,47 @@ describe("subagent announce formatting", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("routes manual completion direct-send for telegram forum topics", async () => {
|
||||
sendSpy.mockClear();
|
||||
agentSpy.mockClear();
|
||||
sessionStore = {
|
||||
"agent:main:subagent:test": {
|
||||
sessionId: "child-session-telegram-topic",
|
||||
},
|
||||
"agent:main:main": {
|
||||
sessionId: "requester-session-telegram-topic",
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123:topic:999",
|
||||
lastThreadId: 999,
|
||||
},
|
||||
};
|
||||
chatHistoryMock.mockResolvedValueOnce({
|
||||
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
|
||||
});
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-direct-telegram-topic",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
to: "123",
|
||||
threadId: 42,
|
||||
},
|
||||
...defaultOutcomeAnnounce,
|
||||
expectsCompletionMessage: true,
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
expect(sendSpy).toHaveBeenCalledTimes(1);
|
||||
expect(agentSpy).not.toHaveBeenCalled();
|
||||
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||
expect(call?.params?.channel).toBe("telegram");
|
||||
expect(call?.params?.to).toBe("123");
|
||||
expect(call?.params?.threadId).toBe("42");
|
||||
});
|
||||
|
||||
it("uses hook-provided thread target across requester thread variants", async () => {
|
||||
const cases = [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { sendHandlers } from "./send.js";
|
||||
import type { GatewayRequestContext } from "./types.js";
|
||||
|
||||
@@ -10,6 +12,8 @@ const mocks = vi.hoisted(() => ({
|
||||
resolveOutboundTarget: vi.fn(() => ({ ok: true, to: "resolved" })),
|
||||
resolveMessageChannelSelection: vi.fn(),
|
||||
sendPoll: vi.fn(async () => ({ messageId: "poll-1" })),
|
||||
getChannelPlugin: vi.fn(),
|
||||
loadOpenClawPlugins: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", async () => {
|
||||
@@ -22,10 +26,38 @@ vi.mock("../../config/config.js", async () => {
|
||||
});
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
getChannelPlugin: () => ({ outbound: { sendPoll: mocks.sendPoll } }),
|
||||
getChannelPlugin: mocks.getChannelPlugin,
|
||||
normalizeChannelId: (value: string) => (value === "webchat" ? null : value),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveSessionAgentId: ({
|
||||
sessionKey,
|
||||
}: {
|
||||
sessionKey?: string;
|
||||
config?: unknown;
|
||||
agentId?: string;
|
||||
}) => {
|
||||
if (typeof sessionKey === "string") {
|
||||
const match = sessionKey.match(/^agent:([^:]+)/i);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return "main";
|
||||
},
|
||||
resolveDefaultAgentId: () => "main",
|
||||
resolveAgentWorkspaceDir: () => "/tmp/openclaw-test-workspace",
|
||||
}));
|
||||
|
||||
vi.mock("../../config/plugin-auto-enable.js", () => ({
|
||||
applyPluginAutoEnable: ({ config }: { config: unknown }) => ({ config, changes: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: mocks.loadOpenClawPlugins,
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/outbound/targets.js", () => ({
|
||||
resolveOutboundTarget: mocks.resolveOutboundTarget,
|
||||
}));
|
||||
@@ -85,14 +117,19 @@ function mockDeliverySuccess(messageId: string) {
|
||||
}
|
||||
|
||||
describe("gateway send mirroring", () => {
|
||||
let registrySeq = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
registrySeq += 1;
|
||||
setActivePluginRegistry(createTestRegistry([]), `send-test-${registrySeq}`);
|
||||
mocks.resolveOutboundTarget.mockReturnValue({ ok: true, to: "resolved" });
|
||||
mocks.resolveMessageChannelSelection.mockResolvedValue({
|
||||
channel: "slack",
|
||||
configured: ["slack"],
|
||||
});
|
||||
mocks.sendPoll.mockResolvedValue({ messageId: "poll-1" });
|
||||
mocks.getChannelPlugin.mockReturnValue({ outbound: { sendPoll: mocks.sendPoll } });
|
||||
});
|
||||
|
||||
it("accepts media-only sends without message", async () => {
|
||||
@@ -475,4 +512,39 @@ describe("gateway send mirroring", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("recovers cold plugin resolution for telegram threaded sends", async () => {
|
||||
mocks.resolveOutboundTarget.mockReturnValue({ ok: true, to: "123" });
|
||||
mocks.deliverOutboundPayloads.mockResolvedValue([
|
||||
{ messageId: "m-telegram", channel: "telegram" },
|
||||
]);
|
||||
const telegramPlugin = { outbound: { sendPoll: mocks.sendPoll } };
|
||||
mocks.getChannelPlugin
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(telegramPlugin)
|
||||
.mockReturnValue(telegramPlugin);
|
||||
|
||||
const { respond } = await runSend({
|
||||
to: "123",
|
||||
message: "forum completion",
|
||||
channel: "telegram",
|
||||
threadId: "42",
|
||||
idempotencyKey: "idem-cold-telegram-thread",
|
||||
});
|
||||
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
to: "123",
|
||||
threadId: "42",
|
||||
}),
|
||||
);
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ messageId: "m-telegram" }),
|
||||
undefined,
|
||||
expect.objectContaining({ channel: "telegram" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,10 +47,15 @@ function maybeBootstrapChannelPlugin(params: {
|
||||
const autoEnabled = applyPluginAutoEnable({ config: cfg }).config;
|
||||
const defaultAgentId = resolveDefaultAgentId(autoEnabled);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(autoEnabled, defaultAgentId);
|
||||
loadOpenClawPlugins({
|
||||
config: autoEnabled,
|
||||
workspaceDir,
|
||||
});
|
||||
try {
|
||||
loadOpenClawPlugins({
|
||||
config: autoEnabled,
|
||||
workspaceDir,
|
||||
});
|
||||
} catch {
|
||||
// Allow a follow-up resolution attempt if bootstrap failed transiently.
|
||||
bootstrapAttempts.delete(attemptKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOutboundChannelPlugin(params: {
|
||||
|
||||
@@ -28,8 +28,11 @@ import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { resolveOutboundTarget } from "./targets.js";
|
||||
|
||||
describe("resolveOutboundTarget channel resolution", () => {
|
||||
let registrySeq = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
registrySeq += 1;
|
||||
setActivePluginRegistry(createTestRegistry([]), `targets-test-${registrySeq}`);
|
||||
mocks.getChannelPlugin.mockReset();
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
});
|
||||
@@ -58,4 +61,43 @@ describe("resolveOutboundTarget channel resolution", () => {
|
||||
expect(result).toEqual({ ok: true, to: "123456" });
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries bootstrap on subsequent resolve when the first bootstrap attempt fails", () => {
|
||||
const telegramPlugin = {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram" },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
};
|
||||
mocks.getChannelPlugin
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(telegramPlugin)
|
||||
.mockReturnValue(telegramPlugin);
|
||||
mocks.loadOpenClawPlugins
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("bootstrap failed");
|
||||
})
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
const first = resolveOutboundTarget({
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
cfg: { channels: { telegram: { botToken: "test-token" } } },
|
||||
mode: "explicit",
|
||||
});
|
||||
const second = resolveOutboundTarget({
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
cfg: { channels: { telegram: { botToken: "test-token" } } },
|
||||
mode: "explicit",
|
||||
});
|
||||
|
||||
expect(first.ok).toBe(false);
|
||||
expect(second).toEqual({ ok: true, to: "123456" });
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1280,6 +1280,23 @@ describe("sendStickerTelegram", () => {
|
||||
expect(sendSticker).toHaveBeenNthCalledWith(2, chatId, "fileId123", undefined);
|
||||
expect(res.messageId).toBe("109");
|
||||
});
|
||||
|
||||
it("fails when sticker send returns no message_id", async () => {
|
||||
const chatId = "123";
|
||||
const sendSticker = vi.fn().mockResolvedValue({
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const api = { sendSticker } as unknown as {
|
||||
sendSticker: typeof sendSticker;
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendStickerTelegram(chatId, "fileId123", {
|
||||
token: "tok",
|
||||
api,
|
||||
}),
|
||||
).rejects.toThrow(/returned no message_id/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared send behaviors", () => {
|
||||
@@ -1542,6 +1559,20 @@ describe("sendPollTelegram", () => {
|
||||
|
||||
expect(api.sendPoll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails when poll send returns no message_id", async () => {
|
||||
const api = {
|
||||
sendPoll: vi.fn(async () => ({ chat: { id: 555 }, poll: { id: "p1" } })),
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendPollTelegram(
|
||||
"123",
|
||||
{ question: "Q", options: ["A", "B"] },
|
||||
{ token: "t", api: api as unknown as Bot["api"] },
|
||||
),
|
||||
).rejects.toThrow(/returned no message_id/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createForumTopicTelegram", () => {
|
||||
|
||||
Reference in New Issue
Block a user