

















@@ -17,6 +17,8 @@ const startSlackStreamMock = vi.fn(async () => ({
1717pendingText: "",
1818}));
1919const stopSlackStreamMock = vi.fn(async () => {});
20+const reactSlackMessageMock = vi.fn(async () => {});
21+const removeSlackReactionMock = vi.fn(async () => {});
2022class TestSlackStreamNotDeliveredError extends Error {
2123readonly pendingText: string;
2224readonly slackCode: string;
@@ -32,6 +34,14 @@ let mockedBlockStreamingEnabled: boolean | undefined = false;
3234let capturedReplyOptions: { disableBlockStreaming?: boolean } | undefined;
3335let mockedReplyThreadTs: string | undefined = THREAD_TS;
3436let mockedReplyThreadTsSequence: Array<string | undefined> | undefined;
37+let capturedTyping:
38+| {
39+start: () => Promise<void>;
40+stop?: () => Promise<void>;
41+onStartError: (err: unknown) => void;
42+onStopError?: (err: unknown) => void;
43+}
44+| undefined;
3545let mockedDispatchSequence: Array<{
3646kind: "tool" | "block" | "final";
3747payload: {
@@ -62,28 +72,36 @@ function createDraftStreamStub() {
6272}
63736474function createPreparedSlackMessage(params?: {
75+cfg?: Record<string, unknown>;
76+ctxPayload?: Record<string, unknown>;
6577message?: Partial<{
6678channel: string;
6779ts: string;
6880thread_ts?: string;
6981user: string;
7082}>;
7183replyToMode?: "off" | "first" | "all" | "batched";
84+setSlackThreadStatus?: (params: {
85+channelId: string;
86+threadTs?: string;
87+status: string;
88+}) => Promise<void>;
89+typingReaction?: string;
7290}) {
7391return {
7492ctx: {
75-cfg: {},
93+cfg: params?.cfg ?? {},
7694runtime: {},
7795botToken: "xoxb-test",
7896app: { client: { chat: { postMessage: postMessageMock } } },
7997teamId: "T1",
8098textLimit: 4000,
81-typingReaction: "",
99+typingReaction: params?.typingReaction ?? "",
82100removeAckAfterReply: false,
83101historyLimit: 0,
84102channelHistories: new Map(),
85103allowFrom: [],
86-setSlackThreadStatus: async () => undefined,
104+setSlackThreadStatus: params?.setSlackThreadStatus ?? (async () => undefined),
87105},
88106account: {
89107accountId: "default",
@@ -106,6 +124,7 @@ function createPreparedSlackMessage(params?: {
106124replyTarget: "channel:C123",
107125ctxPayload: {
108126MessageThreadId: THREAD_TS,
127+ ...params?.ctxPayload,
109128},
110129turn: {
111130storePath: "/tmp/slack-sessions.json",
@@ -149,12 +168,29 @@ vi.mock("../conversation.runtime.js", () => ({
149168}));
150169151170vi.mock("openclaw/plugin-sdk/channel-reply-pipeline", () => ({
152-createChannelReplyPipeline: () => ({
153-typingCallbacks: {
154-onIdle: vi.fn(),
155-},
156-onModelSelected: undefined,
157-}),
171+createChannelReplyPipeline: (params: {
172+typing?: {
173+start: () => Promise<void>;
174+stop?: () => Promise<void>;
175+onStartError: (err: unknown) => void;
176+onStopError?: (err: unknown) => void;
177+};
178+}) => {
179+capturedTyping = params.typing;
180+return {
181+ ...(params.typing
182+ ? {
183+typingCallbacks: {
184+onReplyStart: params.typing.start,
185+onIdle: () => {
186+void params.typing?.stop?.();
187+},
188+},
189+}
190+ : {}),
191+onModelSelected: undefined,
192+};
193+},
158194resolveChannelSourceReplyDeliveryMode: (params: {
159195cfg?: { messages?: { groupChat?: { visibleReplies?: string } } };
160196ctx?: { ChatType?: string };
@@ -220,8 +256,8 @@ vi.mock("openclaw/plugin-sdk/text-runtime", () => ({
220256}));
221257222258vi.mock("../../actions.js", () => ({
223-reactSlackMessage: async () => {},
224-removeSlackReaction: async () => {},
259+reactSlackMessage: reactSlackMessageMock,
260+removeSlackReaction: removeSlackReactionMock,
225261}));
226262227263vi.mock("../../draft-stream.js", () => ({
@@ -370,9 +406,12 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
370406appendSlackStreamMock.mockReset();
371407startSlackStreamMock.mockReset();
372408stopSlackStreamMock.mockReset();
409+reactSlackMessageMock.mockReset();
410+removeSlackReactionMock.mockReset();
373411mockedNativeStreaming = false;
374412mockedBlockStreamingEnabled = false;
375413capturedReplyOptions = undefined;
414+capturedTyping = undefined;
376415mockedReplyThreadTs = THREAD_TS;
377416mockedReplyThreadTsSequence = undefined;
378417mockedDispatchSequence = [{ kind: "final", payload: { text: FINAL_REPLY_TEXT } }];
@@ -439,6 +478,48 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
439478expect(capturedReplyOptions?.disableBlockStreaming).toBe(true);
440479});
441480481+it("keeps Slack typing callbacks when channel replies are message-tool-only", async () => {
482+const setSlackThreadStatus = vi.fn(async () => undefined);
483+484+await dispatchPreparedSlackMessage(
485+createPreparedSlackMessage({
486+cfg: { messages: { groupChat: { visibleReplies: "message_tool" } } },
487+ctxPayload: { ChatType: "channel" },
488+ setSlackThreadStatus,
489+typingReaction: "hourglass_flowing_sand",
490+}),
491+);
492+493+expect(capturedTyping).toBeDefined();
494+expect(capturedReplyOptions?.disableBlockStreaming).toBe(true);
495+496+await capturedTyping?.start();
497+await capturedTyping?.stop?.();
498+499+expect(setSlackThreadStatus).toHaveBeenCalledWith({
500+channelId: "C123",
501+threadTs: THREAD_TS,
502+status: "is typing...",
503+});
504+expect(setSlackThreadStatus).toHaveBeenCalledWith({
505+channelId: "C123",
506+threadTs: THREAD_TS,
507+status: "",
508+});
509+expect(reactSlackMessageMock).toHaveBeenCalledWith(
510+"C123",
511+"171234.111",
512+"hourglass_flowing_sand",
513+expect.objectContaining({ token: "xoxb-test" }),
514+);
515+expect(removeSlackReactionMock).toHaveBeenCalledWith(
516+"C123",
517+"171234.111",
518+"hourglass_flowing_sand",
519+expect.objectContaining({ token: "xoxb-test" }),
520+);
521+});
522+442523it("escapes Slack mrkdwn in tool progress preview labels", async () => {
443524const draftStream = createDraftStreamStub();
444525createSlackDraftStreamMock.mockReturnValueOnce(draftStream);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。