
























@@ -166,12 +166,31 @@ const recordInboundSession = vi.hoisted(() =>
166166vi.fn<(params?: unknown) => Promise<void>>(async () => {}),
167167);
168168const configSessionsMocks = vi.hoisted(() => ({
169+loadSessionStore: vi.fn<(storePath: string, opts?: unknown) => Record<string, unknown>>(
170+() => ({}),
171+),
169172readSessionUpdatedAt: vi.fn<(params?: unknown) => number | undefined>(() => undefined),
173+readLatestAssistantTextFromSessionTranscript: vi.fn<
174+(sessionFile: string) => Promise<{ text: string; timestamp?: number } | undefined>
175+>(async () => undefined),
176+resolveAndPersistSessionFile: vi.fn<(params?: unknown) => Promise<{ sessionFile: string }>>(
177+async () => ({ sessionFile: "/tmp/openclaw-discord-process-test-session.jsonl" }),
178+),
179+resolveSessionStoreEntry: vi.fn<
180+(params: { store: Record<string, unknown>; sessionKey?: string }) => { existing?: unknown }
181+>((params) => ({
182+existing: params.sessionKey ? params.store[params.sessionKey] : undefined,
183+})),
170184resolveStorePath: vi.fn<(path?: unknown, opts?: unknown) => string>(
171185() => "/tmp/openclaw-discord-process-test-sessions.json",
172186),
173187}));
188+const loadSessionStore = configSessionsMocks.loadSessionStore;
174189const readSessionUpdatedAt = configSessionsMocks.readSessionUpdatedAt;
190+const readLatestAssistantTextFromSessionTranscript =
191+configSessionsMocks.readLatestAssistantTextFromSessionTranscript;
192+const resolveAndPersistSessionFile = configSessionsMocks.resolveAndPersistSessionFile;
193+const resolveSessionStoreEntry = configSessionsMocks.resolveSessionStoreEntry;
175194const resolveStorePath = configSessionsMocks.resolveStorePath;
176195const createDiscordRestClientSpy = vi.hoisted(() =>
177196vi.fn<
@@ -266,8 +285,17 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
266285}));
267286268287vi.mock("openclaw/plugin-sdk/session-store-runtime", () => ({
269-readSessionUpdatedAt: (...args: unknown[]) => configSessionsMocks.readSessionUpdatedAt(...args),
270-resolveStorePath: (...args: unknown[]) => configSessionsMocks.resolveStorePath(...args),
288+loadSessionStore: (storePath: string, opts?: unknown) =>
289+configSessionsMocks.loadSessionStore(storePath, opts),
290+readSessionUpdatedAt: (params?: unknown) => configSessionsMocks.readSessionUpdatedAt(params),
291+readLatestAssistantTextFromSessionTranscript: (sessionFile: string) =>
292+configSessionsMocks.readLatestAssistantTextFromSessionTranscript(sessionFile),
293+resolveAndPersistSessionFile: (params?: unknown) =>
294+configSessionsMocks.resolveAndPersistSessionFile(params),
295+resolveSessionStoreEntry: (params: { store: Record<string, unknown>; sessionKey?: string }) =>
296+configSessionsMocks.resolveSessionStoreEntry(params),
297+resolveStorePath: (path?: unknown, opts?: unknown) =>
298+configSessionsMocks.resolveStorePath(path, opts),
271299}));
272300273301vi.mock("../client.js", () => ({
@@ -358,12 +386,24 @@ beforeEach(() => {
358386createDiscordDraftStream.mockClear();
359387dispatchInboundMessage.mockClear();
360388recordInboundSession.mockClear();
389+loadSessionStore.mockClear();
361390readSessionUpdatedAt.mockClear();
391+readLatestAssistantTextFromSessionTranscript.mockClear();
392+resolveAndPersistSessionFile.mockClear();
393+resolveSessionStoreEntry.mockClear();
362394resolveStorePath.mockClear();
363395createDiscordRestClientSpy.mockClear();
364396dispatchInboundMessage.mockResolvedValue(createNoQueuedDispatchResult());
365397recordInboundSession.mockResolvedValue(undefined);
398+loadSessionStore.mockReturnValue({});
366399readSessionUpdatedAt.mockReturnValue(undefined);
400+readLatestAssistantTextFromSessionTranscript.mockResolvedValue(undefined);
401+resolveAndPersistSessionFile.mockResolvedValue({
402+sessionFile: "/tmp/openclaw-discord-process-test-session.jsonl",
403+});
404+resolveSessionStoreEntry.mockImplementation((params) => ({
405+existing: params.sessionKey ? params.store[params.sessionKey] : undefined,
406+}));
367407resolveStorePath.mockReturnValue("/tmp/openclaw-discord-process-test-sessions.json");
368408threadBindingTesting.resetThreadBindingsForTests();
369409});
@@ -1739,6 +1779,47 @@ describe("processDiscordMessage draft streaming", () => {
17391779expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
17401780});
174117811782+it("uses transcript-backed final text when progress final text is truncated", async () => {
1783+const draftStream = createMockDraftStreamForTest();
1784+const prefix =
1785+"Here is the complete Discord answer with enough stable prefix text before truncation";
1786+const truncatedFinal = `${prefix}...`;
1787+const fullAnswer = `${prefix} ${Array.from(
1788+ { length: 260 },
1789+ (_value, index) => `continuation${index}`,
1790+ ).join(" ")}`;
1791+1792+loadSessionStore.mockReturnValue({
1793+"agent:main:discord:channel:c1": { sessionId: "session-1" },
1794+});
1795+readLatestAssistantTextFromSessionTranscript.mockResolvedValue({
1796+text: fullAnswer,
1797+timestamp: Date.now() + 60_000,
1798+});
1799+dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
1800+await params?.replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1801+await params?.replyOptions?.onItemEvent?.({ progressText: "exec done" });
1802+await params?.dispatcher.sendFinalReply({ text: truncatedFinal });
1803+return { queuedFinal: true, counts: { final: 1, tool: 0, block: 0 } };
1804+});
1805+1806+const ctx = await createAutomaticSourceDeliveryContext({
1807+baseSessionKey: BASE_CHANNEL_ROUTE.sessionKey,
1808+discordConfig: { maxLinesPerMessage: 120 },
1809+route: BASE_CHANNEL_ROUTE,
1810+});
1811+1812+await runProcessDiscordMessage(ctx);
1813+1814+expect(draftStream.update).toHaveBeenCalledTimes(1);
1815+expect(editMessageDiscord).not.toHaveBeenCalled();
1816+expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
1817+const params = firstMockArg(deliverDiscordReply, "deliverDiscordReply");
1818+const replies = requireRecord(params, "deliverDiscordReply params").replies;
1819+expect(Array.isArray(replies)).toBe(true);
1820+expect((replies as Array<{ text?: string }>)[0]?.text).toBe(fullAnswer);
1821+});
1822+17421823it("clears partial drafts when fallback final delivery fails before completion", async () => {
17431824const draftStream = createMockDraftStreamForTest();
17441825deliverDiscordReply.mockRejectedValueOnce(new Error("send failed"));
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。