mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
fix: preserve Telegram forum topic last-route delivery (#53052) (thanks @VACInc)
* fix(telegram): preserve forum topic thread in last-route delivery * style(telegram): format last-route update * test(telegram): cover General topic last-route thread * test(telegram): align topic route helper * fix(telegram): skip bound-topic last-route writes --------- Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -2,16 +2,20 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createConfiguredBindingConversationRuntimeModuleMock } from "../../../test/helpers/extensions/configured-binding-runtime.js";
|
||||
|
||||
const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => vi.fn());
|
||||
const recordInboundSessionMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
||||
const resolveConfiguredBindingRouteMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
return await createConfiguredBindingConversationRuntimeModuleMock(
|
||||
{
|
||||
ensureConfiguredBindingRouteReadyMock,
|
||||
resolveConfiguredBindingRouteMock,
|
||||
},
|
||||
importOriginal,
|
||||
);
|
||||
return {
|
||||
...(await createConfiguredBindingConversationRuntimeModuleMock(
|
||||
{
|
||||
ensureConfiguredBindingRouteReadyMock,
|
||||
resolveConfiguredBindingRouteMock,
|
||||
},
|
||||
importOriginal,
|
||||
)),
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest;
|
||||
@@ -136,6 +140,7 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
ensureConfiguredBindingRouteReadyMock.mockReset();
|
||||
recordInboundSessionMock.mockClear();
|
||||
resolveConfiguredBindingRouteMock.mockReset();
|
||||
resolveConfiguredBindingRouteMock.mockReturnValue(createConfiguredTelegramRoute());
|
||||
ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true });
|
||||
@@ -155,6 +160,9 @@ describe("buildTelegramMessageContext ACP configured bindings", () => {
|
||||
expect(ctx?.route.accountId).toBe("work");
|
||||
expect(ctx?.route.matchedBy).toBe("binding.channel");
|
||||
expect(ctx?.route.sessionKey).toBe("agent:codex:acp:binding:telegram:work:abc123");
|
||||
expect(recordInboundSessionMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
updateLastRoute: undefined,
|
||||
});
|
||||
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -74,10 +74,10 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889
|
||||
expect(updateLastRoute?.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not set updateLastRoute for group messages", async () => {
|
||||
it("passes threadId to updateLastRoute for forum topic group messages", async () => {
|
||||
const ctx = await buildCtx({
|
||||
message: {
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "Test Group" },
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "Test Group", is_forum: true },
|
||||
text: "@bot hello",
|
||||
message_thread_id: 99,
|
||||
},
|
||||
@@ -88,7 +88,32 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(recordInboundSessionMock).toHaveBeenCalled();
|
||||
|
||||
// Check that updateLastRoute is undefined for groups
|
||||
expect(getRecordedUpdateLastRoute(0)).toBeUndefined();
|
||||
const updateLastRoute = getRecordedUpdateLastRoute(0) as
|
||||
| { threadId?: string; to?: string }
|
||||
| undefined;
|
||||
expect(updateLastRoute).toBeDefined();
|
||||
expect(updateLastRoute?.to).toBe("telegram:-1001234567890");
|
||||
expect(updateLastRoute?.threadId).toBe("99");
|
||||
});
|
||||
|
||||
it("passes threadId to updateLastRoute for the forum General topic", async () => {
|
||||
const ctx = await buildCtx({
|
||||
message: {
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "Test Group", is_forum: true },
|
||||
text: "@bot hello",
|
||||
},
|
||||
options: { forceWasMentioned: true },
|
||||
resolveGroupActivation: () => true,
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(recordInboundSessionMock).toHaveBeenCalled();
|
||||
|
||||
const updateLastRoute = getRecordedUpdateLastRoute(0) as
|
||||
| { threadId?: string; to?: string }
|
||||
| undefined;
|
||||
expect(updateLastRoute).toBeDefined();
|
||||
expect(updateLastRoute?.to).toBe("telegram:-1001234567890");
|
||||
expect(updateLastRoute?.threadId).toBe("1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,32 +261,44 @@ export async function buildTelegramInboundContextPayload(params: {
|
||||
route,
|
||||
sessionKey: route.sessionKey,
|
||||
});
|
||||
const shouldPersistGroupLastRouteThread = isGroup && route.matchedBy !== "binding.channel";
|
||||
const updateLastRouteThreadId = isGroup
|
||||
? shouldPersistGroupLastRouteThread && resolvedThreadId != null
|
||||
? String(resolvedThreadId)
|
||||
: undefined
|
||||
: dmThreadId != null
|
||||
? String(dmThreadId)
|
||||
: undefined;
|
||||
|
||||
await recordInboundSession({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
updateLastRoute: !isGroup
|
||||
? {
|
||||
sessionKey: updateLastRouteSessionKey,
|
||||
channel: "telegram",
|
||||
to: `telegram:${chatId}`,
|
||||
accountId: route.accountId,
|
||||
threadId: dmThreadId != null ? String(dmThreadId) : undefined,
|
||||
mainDmOwnerPin:
|
||||
updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && senderId
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: senderId,
|
||||
onSkip: ({ ownerRecipient, senderRecipient }) => {
|
||||
logVerbose(
|
||||
`telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
updateLastRoute:
|
||||
!isGroup || updateLastRouteThreadId != null
|
||||
? {
|
||||
sessionKey: updateLastRouteSessionKey,
|
||||
channel: "telegram",
|
||||
to: `telegram:${chatId}`,
|
||||
accountId: route.accountId,
|
||||
threadId: updateLastRouteThreadId,
|
||||
mainDmOwnerPin:
|
||||
!isGroup &&
|
||||
updateLastRouteSessionKey === route.mainSessionKey &&
|
||||
pinnedMainDmOwner &&
|
||||
senderId
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: senderId,
|
||||
onSkip: ({ ownerRecipient, senderRecipient }) => {
|
||||
logVerbose(
|
||||
`telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err) => {
|
||||
logVerbose(`telegram: failed updating session meta: ${String(err)}`);
|
||||
},
|
||||
|
||||
@@ -2,8 +2,10 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const resolveByConversationMock = vi.fn();
|
||||
const recordInboundSessionMock = vi.fn().mockResolvedValue(undefined);
|
||||
const touchMock = vi.fn();
|
||||
return {
|
||||
recordInboundSessionMock,
|
||||
resolveByConversationMock,
|
||||
touchMock,
|
||||
};
|
||||
@@ -13,6 +15,7 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
recordInboundSession: (...args: unknown[]) => hoisted.recordInboundSessionMock(...args),
|
||||
getSessionBindingService: () => ({
|
||||
bind: vi.fn(),
|
||||
getCapabilities: vi.fn(),
|
||||
@@ -34,6 +37,7 @@ describe("buildTelegramMessageContext bound conversation override", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
hoisted.recordInboundSessionMock.mockClear();
|
||||
hoisted.resolveByConversationMock.mockReset().mockReturnValue(null);
|
||||
hoisted.touchMock.mockReset();
|
||||
});
|
||||
@@ -63,6 +67,9 @@ describe("buildTelegramMessageContext bound conversation override", () => {
|
||||
conversationId: "-100200300:topic:77",
|
||||
});
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:codex-acp:session-1");
|
||||
expect(hoisted.recordInboundSessionMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
updateLastRoute: undefined,
|
||||
});
|
||||
expect(hoisted.touchMock).toHaveBeenCalledWith("default:-100200300:topic:77", undefined);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user