@openclaw/feishu 2026.5.2 → 2026.5.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts-Ba3-WP1z.js +423 -0
- package/dist/api.js +2280 -0
- package/dist/app-registration-B8qc1MCM.js +184 -0
- package/dist/audio-preflight.runtime-BPlzkO3l.js +7 -0
- package/dist/card-interaction-BfRLgvw_.js +96 -0
- package/dist/channel-CSD_Jt8I.js +1668 -0
- package/dist/channel-entry.js +22 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-DYsXcD36.js +700 -0
- package/dist/client-DBVoQL5w.js +157 -0
- package/dist/contract-api.js +9 -0
- package/dist/conversation-id-DWS3Ep2A.js +139 -0
- package/dist/directory.static-f3EeoRJd.js +44 -0
- package/dist/drive-C5eJLJr7.js +883 -0
- package/dist/index.js +68 -0
- package/dist/monitor-CT189QfR.js +60 -0
- package/dist/monitor.account-dJV2jO8C.js +4990 -0
- package/dist/monitor.state-DYM02ipp.js +100 -0
- package/dist/policy-D6c-wMPl.js +118 -0
- package/dist/probe-BNzzU_uR.js +149 -0
- package/dist/rolldown-runtime-DUslC3ob.js +14 -0
- package/dist/runtime-CG0DuRCy.js +8 -0
- package/dist/runtime-api.js +14 -0
- package/dist/secret-contract-Dm4Z_zQN.js +119 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/security-audit-DqJdocrN.js +11 -0
- package/dist/security-audit-shared-ByuMx9cJ.js +38 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-DowxxbpH.js +1218 -0
- package/dist/session-conversation-B4nrW-vo.js +27 -0
- package/dist/session-key-api.js +2 -0
- package/dist/setup-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/subagent-hooks-C3UhPVLV.js +227 -0
- package/dist/subagent-hooks-api.js +23 -0
- package/dist/targets-JMFJRKSe.js +48 -0
- package/dist/thread-bindings-BmS6TLes.js +222 -0
- package/package.json +15 -6
- package/api.ts +0 -31
- package/channel-entry.ts +0 -20
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -16
- package/index.ts +0 -82
- package/runtime-api.ts +0 -55
- package/secret-contract-api.ts +0 -5
- package/security-contract-api.ts +0 -1
- package/session-key-api.ts +0 -1
- package/setup-api.ts +0 -3
- package/setup-entry.test.ts +0 -14
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -459
- package/src/accounts.ts +0 -326
- package/src/app-registration.ts +0 -331
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -25
- package/src/async.test.ts +0 -35
- package/src/async.ts +0 -104
- package/src/audio-preflight.runtime.ts +0 -9
- package/src/bitable.test.ts +0 -131
- package/src/bitable.ts +0 -762
- package/src/bot-content.ts +0 -474
- package/src/bot-group-name.test.ts +0 -108
- package/src/bot-runtime-api.ts +0 -12
- package/src/bot-sender-name.ts +0 -125
- package/src/bot.broadcast.test.ts +0 -463
- package/src/bot.card-action.test.ts +0 -577
- package/src/bot.checkBotMentioned.test.ts +0 -265
- package/src/bot.helpers.test.ts +0 -118
- package/src/bot.stripBotMention.test.ts +0 -126
- package/src/bot.test.ts +0 -3040
- package/src/bot.ts +0 -1559
- package/src/card-action.ts +0 -447
- package/src/card-interaction.test.ts +0 -129
- package/src/card-interaction.ts +0 -159
- package/src/card-test-helpers.ts +0 -47
- package/src/card-ux-approval.ts +0 -65
- package/src/card-ux-launcher.test.ts +0 -99
- package/src/card-ux-launcher.ts +0 -121
- package/src/card-ux-shared.ts +0 -33
- package/src/channel-runtime-api.ts +0 -16
- package/src/channel.runtime.ts +0 -47
- package/src/channel.test.ts +0 -959
- package/src/channel.ts +0 -1313
- package/src/chat-schema.ts +0 -25
- package/src/chat.test.ts +0 -196
- package/src/chat.ts +0 -188
- package/src/client.test.ts +0 -433
- package/src/client.ts +0 -290
- package/src/comment-dispatcher-runtime-api.ts +0 -6
- package/src/comment-dispatcher.test.ts +0 -169
- package/src/comment-dispatcher.ts +0 -107
- package/src/comment-handler-runtime-api.ts +0 -3
- package/src/comment-handler.test.ts +0 -486
- package/src/comment-handler.ts +0 -309
- package/src/comment-reaction.test.ts +0 -166
- package/src/comment-reaction.ts +0 -259
- package/src/comment-shared.test.ts +0 -182
- package/src/comment-shared.ts +0 -406
- package/src/comment-target.ts +0 -44
- package/src/config-schema.test.ts +0 -309
- package/src/config-schema.ts +0 -333
- package/src/conversation-id.test.ts +0 -18
- package/src/conversation-id.ts +0 -199
- package/src/dedup-runtime-api.ts +0 -1
- package/src/dedup.ts +0 -141
- package/src/directory.static.ts +0 -61
- package/src/directory.test.ts +0 -136
- package/src/directory.ts +0 -124
- package/src/doc-schema.ts +0 -182
- package/src/docx-batch-insert.test.ts +0 -91
- package/src/docx-batch-insert.ts +0 -223
- package/src/docx-color-text.ts +0 -154
- package/src/docx-table-ops.test.ts +0 -53
- package/src/docx-table-ops.ts +0 -316
- package/src/docx-types.ts +0 -38
- package/src/docx.account-selection.test.ts +0 -79
- package/src/docx.test.ts +0 -685
- package/src/docx.ts +0 -1616
- package/src/drive-schema.ts +0 -92
- package/src/drive.test.ts +0 -1219
- package/src/drive.ts +0 -829
- package/src/dynamic-agent.ts +0 -137
- package/src/event-types.ts +0 -45
- package/src/external-keys.test.ts +0 -20
- package/src/external-keys.ts +0 -19
- package/src/lifecycle.test-support.ts +0 -220
- package/src/media.test.ts +0 -900
- package/src/media.ts +0 -861
- package/src/mention-target.types.ts +0 -5
- package/src/mention.ts +0 -114
- package/src/message-action-contract.ts +0 -13
- package/src/monitor-state-runtime-api.ts +0 -7
- package/src/monitor-transport-runtime-api.ts +0 -7
- package/src/monitor.account.ts +0 -468
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
- package/src/monitor.bot-identity.ts +0 -86
- package/src/monitor.bot-menu-handler.ts +0 -165
- package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
- package/src/monitor.bot-menu.test.ts +0 -178
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
- package/src/monitor.card-action.lifecycle.test-support.ts +0 -373
- package/src/monitor.cleanup.test.ts +0 -376
- package/src/monitor.comment-notice-handler.ts +0 -105
- package/src/monitor.comment.test.ts +0 -937
- package/src/monitor.comment.ts +0 -1386
- package/src/monitor.lifecycle.test.ts +0 -4
- package/src/monitor.message-handler.ts +0 -339
- package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
- package/src/monitor.reaction.test.ts +0 -713
- package/src/monitor.startup.test.ts +0 -192
- package/src/monitor.startup.ts +0 -74
- package/src/monitor.state.defaults.test.ts +0 -46
- package/src/monitor.state.ts +0 -170
- package/src/monitor.synthetic-error.ts +0 -18
- package/src/monitor.test-mocks.ts +0 -45
- package/src/monitor.transport.ts +0 -424
- package/src/monitor.ts +0 -100
- package/src/monitor.webhook-e2e.test.ts +0 -272
- package/src/monitor.webhook-security.test.ts +0 -264
- package/src/monitor.webhook.test-helpers.ts +0 -116
- package/src/outbound-runtime-api.ts +0 -1
- package/src/outbound.test.ts +0 -935
- package/src/outbound.ts +0 -718
- package/src/perm-schema.ts +0 -52
- package/src/perm.ts +0 -170
- package/src/pins.ts +0 -108
- package/src/policy.test.ts +0 -334
- package/src/policy.ts +0 -236
- package/src/post.test.ts +0 -105
- package/src/post.ts +0 -275
- package/src/probe.test.ts +0 -275
- package/src/probe.ts +0 -166
- package/src/processing-claims.ts +0 -59
- package/src/qr-terminal.ts +0 -1
- package/src/reactions.ts +0 -123
- package/src/reasoning-preview.test.ts +0 -59
- package/src/reasoning-preview.ts +0 -20
- package/src/reply-dispatcher-runtime-api.ts +0 -7
- package/src/reply-dispatcher.test.ts +0 -1144
- package/src/reply-dispatcher.ts +0 -650
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -145
- package/src/secret-input.ts +0 -1
- package/src/security-audit-shared.ts +0 -69
- package/src/security-audit.test.ts +0 -61
- package/src/security-audit.ts +0 -1
- package/src/send-result.ts +0 -29
- package/src/send-target.test.ts +0 -80
- package/src/send-target.ts +0 -35
- package/src/send.reply-fallback.test.ts +0 -292
- package/src/send.test.ts +0 -550
- package/src/send.ts +0 -800
- package/src/sequential-key.test.ts +0 -72
- package/src/sequential-key.ts +0 -28
- package/src/sequential-queue.test.ts +0 -92
- package/src/sequential-queue.ts +0 -16
- package/src/session-conversation.ts +0 -42
- package/src/session-route.ts +0 -48
- package/src/setup-core.ts +0 -51
- package/src/setup-surface.test.ts +0 -174
- package/src/setup-surface.ts +0 -581
- package/src/streaming-card.test.ts +0 -190
- package/src/streaming-card.ts +0 -490
- package/src/subagent-hooks.test.ts +0 -603
- package/src/subagent-hooks.ts +0 -397
- package/src/targets.ts +0 -97
- package/src/test-support/lifecycle-test-support.ts +0 -453
- package/src/thread-bindings.test.ts +0 -143
- package/src/thread-bindings.ts +0 -330
- package/src/tool-account-routing.test.ts +0 -187
- package/src/tool-account.test.ts +0 -44
- package/src/tool-account.ts +0 -93
- package/src/tool-factory-test-harness.ts +0 -79
- package/src/tool-result.test.ts +0 -32
- package/src/tool-result.ts +0 -16
- package/src/tools-config.test.ts +0 -21
- package/src/tools-config.ts +0 -22
- package/src/types.ts +0 -104
- package/src/typing.test.ts +0 -144
- package/src/typing.ts +0 -214
- package/src/wiki-schema.ts +0 -55
- package/src/wiki.ts +0 -227
- package/subagent-hooks-api.ts +0 -31
- package/tsconfig.json +0 -16
|
@@ -1,1144 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
type StreamingSessionStub = {
|
|
4
|
-
active: boolean;
|
|
5
|
-
start: ReturnType<typeof vi.fn>;
|
|
6
|
-
update: ReturnType<typeof vi.fn>;
|
|
7
|
-
close: ReturnType<typeof vi.fn>;
|
|
8
|
-
isActive: ReturnType<typeof vi.fn>;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
|
|
12
|
-
const getFeishuRuntimeMock = vi.hoisted(() => vi.fn());
|
|
13
|
-
const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
|
|
14
|
-
const sendMarkdownCardFeishuMock = vi.hoisted(() => vi.fn());
|
|
15
|
-
const sendStructuredCardFeishuMock = vi.hoisted(() => vi.fn());
|
|
16
|
-
const sendMediaFeishuMock = vi.hoisted(() => vi.fn());
|
|
17
|
-
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
|
18
|
-
const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
|
|
19
|
-
const createReplyDispatcherWithTypingMock = vi.hoisted(() => vi.fn());
|
|
20
|
-
const addTypingIndicatorMock = vi.hoisted(() => vi.fn(async () => ({ messageId: "om_msg" })));
|
|
21
|
-
const removeTypingIndicatorMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
22
|
-
const streamingInstances = vi.hoisted((): StreamingSessionStub[] => []);
|
|
23
|
-
|
|
24
|
-
function mergeStreamingText(
|
|
25
|
-
previousText: string | undefined,
|
|
26
|
-
nextText: string | undefined,
|
|
27
|
-
): string {
|
|
28
|
-
const previous = typeof previousText === "string" ? previousText : "";
|
|
29
|
-
const next = typeof nextText === "string" ? nextText : "";
|
|
30
|
-
if (!next) {
|
|
31
|
-
return previous;
|
|
32
|
-
}
|
|
33
|
-
if (!previous || next === previous) {
|
|
34
|
-
return next;
|
|
35
|
-
}
|
|
36
|
-
if (next.startsWith(previous) || next.includes(previous)) {
|
|
37
|
-
return next;
|
|
38
|
-
}
|
|
39
|
-
if (previous.startsWith(next) || previous.includes(next)) {
|
|
40
|
-
return previous;
|
|
41
|
-
}
|
|
42
|
-
const maxOverlap = Math.min(previous.length, next.length);
|
|
43
|
-
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
|
|
44
|
-
if (previous.slice(-overlap) === next.slice(0, overlap)) {
|
|
45
|
-
return `${previous}${next.slice(overlap)}`;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return `${previous}${next}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
vi.mock("./accounts.js", () => ({
|
|
52
|
-
resolveFeishuAccount: resolveFeishuAccountMock,
|
|
53
|
-
resolveFeishuRuntimeAccount: resolveFeishuAccountMock,
|
|
54
|
-
}));
|
|
55
|
-
vi.mock("./runtime.js", () => ({ getFeishuRuntime: getFeishuRuntimeMock }));
|
|
56
|
-
vi.mock("./send.js", () => ({
|
|
57
|
-
sendMessageFeishu: sendMessageFeishuMock,
|
|
58
|
-
sendMarkdownCardFeishu: sendMarkdownCardFeishuMock,
|
|
59
|
-
sendStructuredCardFeishu: sendStructuredCardFeishuMock,
|
|
60
|
-
}));
|
|
61
|
-
vi.mock("./media.js", () => ({ sendMediaFeishu: sendMediaFeishuMock }));
|
|
62
|
-
vi.mock("./client.js", () => ({ createFeishuClient: createFeishuClientMock }));
|
|
63
|
-
vi.mock("./targets.js", () => ({ resolveReceiveIdType: resolveReceiveIdTypeMock }));
|
|
64
|
-
vi.mock("./typing.js", () => ({
|
|
65
|
-
addTypingIndicator: addTypingIndicatorMock,
|
|
66
|
-
removeTypingIndicator: removeTypingIndicatorMock,
|
|
67
|
-
}));
|
|
68
|
-
vi.mock("./streaming-card.js", () => {
|
|
69
|
-
return {
|
|
70
|
-
mergeStreamingText,
|
|
71
|
-
FeishuStreamingSession: class {
|
|
72
|
-
active = false;
|
|
73
|
-
start = vi.fn(async () => {
|
|
74
|
-
this.active = true;
|
|
75
|
-
});
|
|
76
|
-
update = vi.fn(async () => {});
|
|
77
|
-
close = vi.fn(async () => {
|
|
78
|
-
this.active = false;
|
|
79
|
-
});
|
|
80
|
-
isActive = vi.fn(() => this.active);
|
|
81
|
-
|
|
82
|
-
constructor() {
|
|
83
|
-
streamingInstances.push(this);
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
import {
|
|
90
|
-
clearFeishuStreamingStartBackoffForTests,
|
|
91
|
-
createFeishuReplyDispatcher,
|
|
92
|
-
} from "./reply-dispatcher.js";
|
|
93
|
-
|
|
94
|
-
describe("createFeishuReplyDispatcher streaming behavior", () => {
|
|
95
|
-
type ReplyDispatcherArgs = Parameters<typeof createFeishuReplyDispatcher>[0];
|
|
96
|
-
|
|
97
|
-
beforeEach(() => {
|
|
98
|
-
vi.clearAllMocks();
|
|
99
|
-
clearFeishuStreamingStartBackoffForTests();
|
|
100
|
-
streamingInstances.length = 0;
|
|
101
|
-
sendMediaFeishuMock.mockResolvedValue(undefined);
|
|
102
|
-
sendStructuredCardFeishuMock.mockResolvedValue(undefined);
|
|
103
|
-
|
|
104
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
105
|
-
accountId: "main",
|
|
106
|
-
appId: "app_id",
|
|
107
|
-
appSecret: "app_secret",
|
|
108
|
-
domain: "feishu",
|
|
109
|
-
config: {
|
|
110
|
-
renderMode: "auto",
|
|
111
|
-
streaming: true,
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
resolveReceiveIdTypeMock.mockReturnValue("chat_id");
|
|
116
|
-
createFeishuClientMock.mockReturnValue({});
|
|
117
|
-
|
|
118
|
-
createReplyDispatcherWithTypingMock.mockImplementation((opts) => ({
|
|
119
|
-
dispatcher: {},
|
|
120
|
-
replyOptions: {},
|
|
121
|
-
markDispatchIdle: vi.fn(),
|
|
122
|
-
_opts: opts,
|
|
123
|
-
}));
|
|
124
|
-
|
|
125
|
-
getFeishuRuntimeMock.mockReturnValue({
|
|
126
|
-
channel: {
|
|
127
|
-
text: {
|
|
128
|
-
resolveTextChunkLimit: vi.fn(() => 4000),
|
|
129
|
-
resolveChunkMode: vi.fn(() => "line"),
|
|
130
|
-
resolveMarkdownTableMode: vi.fn(() => "preserve"),
|
|
131
|
-
convertMarkdownTables: vi.fn((text) => text),
|
|
132
|
-
chunkTextWithMode: vi.fn((text) => [text]),
|
|
133
|
-
},
|
|
134
|
-
reply: {
|
|
135
|
-
createReplyDispatcherWithTyping: createReplyDispatcherWithTypingMock,
|
|
136
|
-
resolveHumanDelayConfig: vi.fn(() => undefined),
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
function setupNonStreamingAutoDispatcher() {
|
|
143
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
144
|
-
accountId: "main",
|
|
145
|
-
appId: "app_id",
|
|
146
|
-
appSecret: "app_secret",
|
|
147
|
-
domain: "feishu",
|
|
148
|
-
config: {
|
|
149
|
-
renderMode: "auto",
|
|
150
|
-
streaming: false,
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
createFeishuReplyDispatcher({
|
|
155
|
-
cfg: {} as never,
|
|
156
|
-
agentId: "agent",
|
|
157
|
-
runtime: { log: vi.fn(), error: vi.fn() } as never,
|
|
158
|
-
chatId: "oc_chat",
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function createRuntimeLogger() {
|
|
165
|
-
return { log: vi.fn(), error: vi.fn() } as never;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function createDispatcherHarness(overrides: Partial<ReplyDispatcherArgs> = {}) {
|
|
169
|
-
const result = createFeishuReplyDispatcher({
|
|
170
|
-
cfg: {} as never,
|
|
171
|
-
agentId: "agent",
|
|
172
|
-
runtime: {} as never,
|
|
173
|
-
chatId: "oc_chat",
|
|
174
|
-
...overrides,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
result,
|
|
179
|
-
options: createReplyDispatcherWithTypingMock.mock.calls.at(-1)?.[0],
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
it("skips typing indicator when account typingIndicator is disabled", async () => {
|
|
184
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
185
|
-
accountId: "main",
|
|
186
|
-
appId: "app_id",
|
|
187
|
-
appSecret: "app_secret",
|
|
188
|
-
domain: "feishu",
|
|
189
|
-
config: {
|
|
190
|
-
renderMode: "auto",
|
|
191
|
-
streaming: true,
|
|
192
|
-
typingIndicator: false,
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
createFeishuReplyDispatcher({
|
|
197
|
-
cfg: {} as never,
|
|
198
|
-
agentId: "agent",
|
|
199
|
-
runtime: {} as never,
|
|
200
|
-
chatId: "oc_chat",
|
|
201
|
-
replyToMessageId: "om_parent",
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
205
|
-
await options.onReplyStart?.();
|
|
206
|
-
|
|
207
|
-
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("skips typing indicator for stale replayed messages", async () => {
|
|
211
|
-
createFeishuReplyDispatcher({
|
|
212
|
-
cfg: {} as never,
|
|
213
|
-
agentId: "agent",
|
|
214
|
-
runtime: {} as never,
|
|
215
|
-
chatId: "oc_chat",
|
|
216
|
-
replyToMessageId: "om_parent",
|
|
217
|
-
messageCreateTimeMs: Date.now() - 3 * 60_000,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
221
|
-
await options.onReplyStart?.();
|
|
222
|
-
|
|
223
|
-
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("treats second-based timestamps as stale for typing suppression", async () => {
|
|
227
|
-
createFeishuReplyDispatcher({
|
|
228
|
-
cfg: {} as never,
|
|
229
|
-
agentId: "agent",
|
|
230
|
-
runtime: {} as never,
|
|
231
|
-
chatId: "oc_chat",
|
|
232
|
-
replyToMessageId: "om_parent",
|
|
233
|
-
messageCreateTimeMs: Math.floor((Date.now() - 3 * 60_000) / 1000),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
237
|
-
await options.onReplyStart?.();
|
|
238
|
-
|
|
239
|
-
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("keeps typing indicator for fresh messages", async () => {
|
|
243
|
-
createFeishuReplyDispatcher({
|
|
244
|
-
cfg: {} as never,
|
|
245
|
-
agentId: "agent",
|
|
246
|
-
runtime: {} as never,
|
|
247
|
-
chatId: "oc_chat",
|
|
248
|
-
replyToMessageId: "om_parent",
|
|
249
|
-
messageCreateTimeMs: Date.now() - 30_000,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
253
|
-
await options.onReplyStart?.();
|
|
254
|
-
|
|
255
|
-
expect(addTypingIndicatorMock).toHaveBeenCalledTimes(1);
|
|
256
|
-
expect(addTypingIndicatorMock).toHaveBeenCalledWith(
|
|
257
|
-
expect.objectContaining({
|
|
258
|
-
messageId: "om_parent",
|
|
259
|
-
}),
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("keeps auto mode plain text on non-streaming send path", async () => {
|
|
264
|
-
const { options } = createDispatcherHarness();
|
|
265
|
-
await options.deliver({ text: "plain text" }, { kind: "final" });
|
|
266
|
-
|
|
267
|
-
expect(streamingInstances).toHaveLength(0);
|
|
268
|
-
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
|
|
269
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("suppresses internal block payload delivery", async () => {
|
|
273
|
-
const { options } = createDispatcherHarness();
|
|
274
|
-
await options.deliver({ text: "internal reasoning chunk" }, { kind: "block" });
|
|
275
|
-
|
|
276
|
-
expect(streamingInstances).toHaveLength(0);
|
|
277
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
278
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
279
|
-
expect(sendMediaFeishuMock).not.toHaveBeenCalled();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("sets disableBlockStreaming in replyOptions to prevent silent reply drops", async () => {
|
|
283
|
-
const result = createFeishuReplyDispatcher({
|
|
284
|
-
cfg: {} as never,
|
|
285
|
-
agentId: "agent",
|
|
286
|
-
runtime: {} as never,
|
|
287
|
-
chatId: "oc_chat",
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
expect(result.replyOptions).toHaveProperty("disableBlockStreaming", true);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("uses streaming session for auto mode markdown payloads", async () => {
|
|
294
|
-
const { options } = createDispatcherHarness({
|
|
295
|
-
runtime: createRuntimeLogger(),
|
|
296
|
-
rootId: "om_root_topic",
|
|
297
|
-
});
|
|
298
|
-
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
299
|
-
await options.onIdle?.();
|
|
300
|
-
|
|
301
|
-
expect(streamingInstances).toHaveLength(1);
|
|
302
|
-
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
|
|
303
|
-
expect(streamingInstances[0].start).toHaveBeenCalledWith(
|
|
304
|
-
"oc_chat",
|
|
305
|
-
"chat_id",
|
|
306
|
-
expect.objectContaining({
|
|
307
|
-
replyToMessageId: undefined,
|
|
308
|
-
replyInThread: undefined,
|
|
309
|
-
rootId: "om_root_topic",
|
|
310
|
-
header: { title: "agent", template: "blue" },
|
|
311
|
-
note: "Agent: agent",
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
315
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
316
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("closes streaming with block text when final reply is missing", async () => {
|
|
320
|
-
const { options } = createDispatcherHarness({
|
|
321
|
-
runtime: createRuntimeLogger(),
|
|
322
|
-
});
|
|
323
|
-
await options.deliver({ text: "```md\npartial answer\n```" }, { kind: "block" });
|
|
324
|
-
await options.onIdle?.();
|
|
325
|
-
|
|
326
|
-
expect(streamingInstances).toHaveLength(1);
|
|
327
|
-
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
|
|
328
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
329
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\npartial answer\n```", {
|
|
330
|
-
note: "Agent: agent",
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("coalesces distinct final payloads into one streaming card until idle", async () => {
|
|
335
|
-
const { options } = createDispatcherHarness({
|
|
336
|
-
runtime: createRuntimeLogger(),
|
|
337
|
-
});
|
|
338
|
-
await options.deliver({ text: "```md\n完整回复第一段\n```" }, { kind: "final" });
|
|
339
|
-
await options.deliver({ text: "```md\n完整回复第一段 + 第二段\n```" }, { kind: "final" });
|
|
340
|
-
await options.onIdle?.();
|
|
341
|
-
|
|
342
|
-
expect(streamingInstances).toHaveLength(1);
|
|
343
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
344
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith(
|
|
345
|
-
"```md\n完整回复第一段 + 第二段\n```",
|
|
346
|
-
{
|
|
347
|
-
note: "Agent: agent",
|
|
348
|
-
},
|
|
349
|
-
);
|
|
350
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
351
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("skips exact duplicate final text after streaming close", async () => {
|
|
355
|
-
const { options } = createDispatcherHarness({
|
|
356
|
-
runtime: createRuntimeLogger(),
|
|
357
|
-
});
|
|
358
|
-
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
|
|
359
|
-
await options.onIdle?.();
|
|
360
|
-
await options.deliver({ text: "```md\n同一条回复\n```" }, { kind: "final" });
|
|
361
|
-
|
|
362
|
-
expect(streamingInstances).toHaveLength(1);
|
|
363
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
364
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\n同一条回复\n```", {
|
|
365
|
-
note: "Agent: agent",
|
|
366
|
-
});
|
|
367
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
368
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("skips final text already closed by idle streaming", async () => {
|
|
372
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
373
|
-
accountId: "main",
|
|
374
|
-
appId: "app_id",
|
|
375
|
-
appSecret: "app_secret",
|
|
376
|
-
domain: "feishu",
|
|
377
|
-
config: {
|
|
378
|
-
renderMode: "card",
|
|
379
|
-
streaming: true,
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
const { result, options } = createDispatcherHarness({
|
|
384
|
-
runtime: createRuntimeLogger(),
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
await options.onReplyStart?.();
|
|
388
|
-
result.replyOptions.onPartialReply?.({ text: "```md\nidle streamed reply\n```" });
|
|
389
|
-
await options.onIdle?.();
|
|
390
|
-
await options.deliver({ text: "```md\nidle streamed reply\n```" }, { kind: "final" });
|
|
391
|
-
|
|
392
|
-
expect(streamingInstances).toHaveLength(1);
|
|
393
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
394
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\nidle streamed reply\n```", {
|
|
395
|
-
note: "Agent: agent",
|
|
396
|
-
});
|
|
397
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
398
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
399
|
-
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("skips distinct late final text after streaming card close", async () => {
|
|
403
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
404
|
-
accountId: "main",
|
|
405
|
-
appId: "app_id",
|
|
406
|
-
appSecret: "app_secret",
|
|
407
|
-
domain: "feishu",
|
|
408
|
-
config: {
|
|
409
|
-
renderMode: "card",
|
|
410
|
-
streaming: true,
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
const { options } = createDispatcherHarness({
|
|
415
|
-
runtime: createRuntimeLogger(),
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
await options.deliver({ text: "First complete answer" }, { kind: "final" });
|
|
419
|
-
await options.onIdle?.();
|
|
420
|
-
await options.deliver(
|
|
421
|
-
{ text: "Late tool-result final", mediaUrl: "https://example.com/a.png" },
|
|
422
|
-
{ kind: "final" },
|
|
423
|
-
);
|
|
424
|
-
await options.onIdle?.();
|
|
425
|
-
|
|
426
|
-
expect(streamingInstances).toHaveLength(1);
|
|
427
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
428
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("First complete answer", {
|
|
429
|
-
note: "Agent: agent",
|
|
430
|
-
});
|
|
431
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
432
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
433
|
-
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
434
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
|
435
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
436
|
-
expect.objectContaining({
|
|
437
|
-
mediaUrl: "https://example.com/a.png",
|
|
438
|
-
}),
|
|
439
|
-
);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it("suppresses duplicate final text while still sending media", async () => {
|
|
443
|
-
const options = setupNonStreamingAutoDispatcher();
|
|
444
|
-
await options.deliver({ text: "plain final" }, { kind: "final" });
|
|
445
|
-
await options.deliver(
|
|
446
|
-
{ text: "plain final", mediaUrl: "https://example.com/a.png" },
|
|
447
|
-
{ kind: "final" },
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
|
|
451
|
-
expect(sendMessageFeishuMock).toHaveBeenLastCalledWith(
|
|
452
|
-
expect.objectContaining({
|
|
453
|
-
text: "plain final",
|
|
454
|
-
}),
|
|
455
|
-
);
|
|
456
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
|
457
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
458
|
-
expect.objectContaining({
|
|
459
|
-
mediaUrl: "https://example.com/a.png",
|
|
460
|
-
}),
|
|
461
|
-
);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it("keeps distinct non-streaming final payloads", async () => {
|
|
465
|
-
const options = setupNonStreamingAutoDispatcher();
|
|
466
|
-
await options.deliver({ text: "notice header" }, { kind: "final" });
|
|
467
|
-
await options.deliver({ text: "actual answer body" }, { kind: "final" });
|
|
468
|
-
|
|
469
|
-
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
|
|
470
|
-
expect(sendMessageFeishuMock).toHaveBeenNthCalledWith(
|
|
471
|
-
1,
|
|
472
|
-
expect.objectContaining({ text: "notice header" }),
|
|
473
|
-
);
|
|
474
|
-
expect(sendMessageFeishuMock).toHaveBeenNthCalledWith(
|
|
475
|
-
2,
|
|
476
|
-
expect.objectContaining({ text: "actual answer body" }),
|
|
477
|
-
);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it("treats block updates as delta chunks", async () => {
|
|
481
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
482
|
-
accountId: "main",
|
|
483
|
-
appId: "app_id",
|
|
484
|
-
appSecret: "app_secret",
|
|
485
|
-
domain: "feishu",
|
|
486
|
-
config: {
|
|
487
|
-
renderMode: "card",
|
|
488
|
-
streaming: true,
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
const { result, options } = createDispatcherHarness({
|
|
493
|
-
runtime: createRuntimeLogger(),
|
|
494
|
-
});
|
|
495
|
-
await options.onReplyStart?.();
|
|
496
|
-
result.replyOptions.onPartialReply?.({ text: "hello" });
|
|
497
|
-
await options.deliver({ text: "lo world" }, { kind: "block" });
|
|
498
|
-
await options.onIdle?.();
|
|
499
|
-
|
|
500
|
-
expect(streamingInstances).toHaveLength(1);
|
|
501
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
502
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("hellolo world", {
|
|
503
|
-
note: "Agent: agent",
|
|
504
|
-
});
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it("skips block payloads that exactly repeat the latest partial snapshot", async () => {
|
|
508
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
509
|
-
accountId: "main",
|
|
510
|
-
appId: "app_id",
|
|
511
|
-
appSecret: "app_secret",
|
|
512
|
-
domain: "feishu",
|
|
513
|
-
config: {
|
|
514
|
-
renderMode: "card",
|
|
515
|
-
streaming: true,
|
|
516
|
-
},
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
const { result, options } = createDispatcherHarness({
|
|
520
|
-
runtime: createRuntimeLogger(),
|
|
521
|
-
});
|
|
522
|
-
await options.onReplyStart?.();
|
|
523
|
-
result.replyOptions.onPartialReply?.({ text: "```md\npartial\n```" });
|
|
524
|
-
await options.deliver({ text: "```md\npartial\n```" }, { kind: "block" });
|
|
525
|
-
await options.onIdle?.();
|
|
526
|
-
|
|
527
|
-
expect(streamingInstances).toHaveLength(1);
|
|
528
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
529
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("```md\npartial\n```", {
|
|
530
|
-
note: "Agent: agent",
|
|
531
|
-
});
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it("preserves previous generation blocks when partial snapshots reset after tools", async () => {
|
|
535
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
536
|
-
accountId: "main",
|
|
537
|
-
appId: "app_id",
|
|
538
|
-
appSecret: "app_secret",
|
|
539
|
-
domain: "feishu",
|
|
540
|
-
config: {
|
|
541
|
-
renderMode: "card",
|
|
542
|
-
streaming: true,
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
const { result, options } = createDispatcherHarness({
|
|
547
|
-
runtime: createRuntimeLogger(),
|
|
548
|
-
});
|
|
549
|
-
await options.onReplyStart?.();
|
|
550
|
-
result.replyOptions.onPartialReply?.({
|
|
551
|
-
text: "Preparing the lookup plan with enough text to count as one block.",
|
|
552
|
-
});
|
|
553
|
-
result.replyOptions.onPartialReply?.({ text: "Found" });
|
|
554
|
-
result.replyOptions.onPartialReply?.({ text: "Found the answer." });
|
|
555
|
-
await options.onIdle?.();
|
|
556
|
-
|
|
557
|
-
expect(streamingInstances).toHaveLength(1);
|
|
558
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith(
|
|
559
|
-
"Preparing the lookup plan with enough text to count as one block.Found the answer.",
|
|
560
|
-
{
|
|
561
|
-
note: "Agent: agent",
|
|
562
|
-
},
|
|
563
|
-
);
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it("strips reasoning tags from streamed partial snapshots", async () => {
|
|
567
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
568
|
-
accountId: "main",
|
|
569
|
-
appId: "app_id",
|
|
570
|
-
appSecret: "app_secret",
|
|
571
|
-
domain: "feishu",
|
|
572
|
-
config: {
|
|
573
|
-
renderMode: "card",
|
|
574
|
-
streaming: true,
|
|
575
|
-
},
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
const { result, options } = createDispatcherHarness({
|
|
579
|
-
runtime: createRuntimeLogger(),
|
|
580
|
-
});
|
|
581
|
-
await options.onReplyStart?.();
|
|
582
|
-
result.replyOptions.onPartialReply?.({
|
|
583
|
-
text: "<thinking>private chain of thought</thinking>\nvisible answer",
|
|
584
|
-
});
|
|
585
|
-
await options.onIdle?.();
|
|
586
|
-
|
|
587
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("visible answer", {
|
|
588
|
-
note: "Agent: agent",
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
it("sends media-only payloads as attachments", async () => {
|
|
593
|
-
const { options } = createDispatcherHarness();
|
|
594
|
-
await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
|
|
595
|
-
|
|
596
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
|
597
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
598
|
-
expect.objectContaining({
|
|
599
|
-
to: "oc_chat",
|
|
600
|
-
mediaUrl: "https://example.com/a.png",
|
|
601
|
-
}),
|
|
602
|
-
);
|
|
603
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
604
|
-
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it("passes audioAsVoice to media attachments", async () => {
|
|
608
|
-
const { options } = createDispatcherHarness();
|
|
609
|
-
await options.deliver(
|
|
610
|
-
{ mediaUrl: "https://example.com/reply.mp3", audioAsVoice: true },
|
|
611
|
-
{ kind: "final" },
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
615
|
-
expect.objectContaining({
|
|
616
|
-
mediaUrl: "https://example.com/reply.mp3",
|
|
617
|
-
audioAsVoice: true,
|
|
618
|
-
}),
|
|
619
|
-
);
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
it("falls back to legacy mediaUrl when mediaUrls is an empty array", async () => {
|
|
623
|
-
const { options } = createDispatcherHarness();
|
|
624
|
-
await options.deliver(
|
|
625
|
-
{ text: "caption", mediaUrl: "https://example.com/a.png", mediaUrls: [] },
|
|
626
|
-
{ kind: "final" },
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
|
|
630
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
|
631
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
632
|
-
expect.objectContaining({
|
|
633
|
-
mediaUrl: "https://example.com/a.png",
|
|
634
|
-
}),
|
|
635
|
-
);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
it("sends attachments after streaming final markdown replies", async () => {
|
|
639
|
-
const { options } = createDispatcherHarness({
|
|
640
|
-
runtime: createRuntimeLogger(),
|
|
641
|
-
});
|
|
642
|
-
await options.deliver(
|
|
643
|
-
{ text: "```ts\nconst x = 1\n```", mediaUrls: ["https://example.com/a.png"] },
|
|
644
|
-
{ kind: "final" },
|
|
645
|
-
);
|
|
646
|
-
await options.onIdle?.();
|
|
647
|
-
|
|
648
|
-
expect(streamingInstances).toHaveLength(1);
|
|
649
|
-
expect(streamingInstances[0].start).toHaveBeenCalledTimes(1);
|
|
650
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
651
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
|
652
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
653
|
-
expect.objectContaining({
|
|
654
|
-
mediaUrl: "https://example.com/a.png",
|
|
655
|
-
}),
|
|
656
|
-
);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it("passes replyInThread to sendMessageFeishu for plain text", async () => {
|
|
660
|
-
const { options } = createDispatcherHarness({
|
|
661
|
-
replyToMessageId: "om_msg",
|
|
662
|
-
replyInThread: true,
|
|
663
|
-
});
|
|
664
|
-
await options.deliver({ text: "plain text" }, { kind: "final" });
|
|
665
|
-
|
|
666
|
-
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
667
|
-
expect.objectContaining({
|
|
668
|
-
replyToMessageId: "om_msg",
|
|
669
|
-
replyInThread: true,
|
|
670
|
-
}),
|
|
671
|
-
);
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
it("passes replyInThread to sendStructuredCardFeishu for card text", async () => {
|
|
675
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
676
|
-
accountId: "main",
|
|
677
|
-
appId: "app_id",
|
|
678
|
-
appSecret: "app_secret",
|
|
679
|
-
domain: "feishu",
|
|
680
|
-
config: {
|
|
681
|
-
renderMode: "card",
|
|
682
|
-
streaming: false,
|
|
683
|
-
},
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
const { options } = createDispatcherHarness({
|
|
687
|
-
replyToMessageId: "om_msg",
|
|
688
|
-
replyInThread: true,
|
|
689
|
-
});
|
|
690
|
-
await options.deliver({ text: "card text" }, { kind: "final" });
|
|
691
|
-
|
|
692
|
-
expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
|
|
693
|
-
expect.objectContaining({
|
|
694
|
-
replyToMessageId: "om_msg",
|
|
695
|
-
replyInThread: true,
|
|
696
|
-
}),
|
|
697
|
-
);
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
it("streams reasoning content as blockquote before answer", async () => {
|
|
701
|
-
const { result, options } = createDispatcherHarness({
|
|
702
|
-
runtime: createRuntimeLogger(),
|
|
703
|
-
allowReasoningPreview: true,
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
await options.onReplyStart?.();
|
|
707
|
-
// Core agent sends pre-formatted text from formatReasoningMessage
|
|
708
|
-
result.replyOptions.onReasoningStream?.({ text: "Reasoning:\n_thinking step 1_" });
|
|
709
|
-
result.replyOptions.onReasoningStream?.({
|
|
710
|
-
text: "Reasoning:\n_thinking step 1_\n_step 2_",
|
|
711
|
-
});
|
|
712
|
-
result.replyOptions.onPartialReply?.({ text: "answer part" });
|
|
713
|
-
result.replyOptions.onReasoningEnd?.();
|
|
714
|
-
await options.deliver({ text: "answer part final" }, { kind: "final" });
|
|
715
|
-
await options.onIdle?.();
|
|
716
|
-
|
|
717
|
-
expect(streamingInstances).toHaveLength(1);
|
|
718
|
-
const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) =>
|
|
719
|
-
typeof c[0] === "string" ? c[0] : "",
|
|
720
|
-
);
|
|
721
|
-
const reasoningUpdate = updateCalls.find((c) => c.includes("Thinking"));
|
|
722
|
-
expect(reasoningUpdate).toContain("> 💭 **Thinking**");
|
|
723
|
-
// formatReasoningPrefix strips "Reasoning:" prefix and italic markers
|
|
724
|
-
expect(reasoningUpdate).toContain("> thinking step");
|
|
725
|
-
expect(reasoningUpdate).not.toContain("Reasoning:");
|
|
726
|
-
expect(reasoningUpdate).not.toMatch(/> _.*_/);
|
|
727
|
-
|
|
728
|
-
const combinedUpdate = updateCalls.find((c) => c.includes("Thinking") && c.includes("---"));
|
|
729
|
-
expect(combinedUpdate).toBeDefined();
|
|
730
|
-
|
|
731
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
732
|
-
const closeArg = streamingInstances[0].close.mock.calls[0][0] as string;
|
|
733
|
-
expect(closeArg).toContain("> 💭 **Thinking**");
|
|
734
|
-
expect(closeArg).toContain("---");
|
|
735
|
-
expect(closeArg).toContain("answer part final");
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
it("provides onReasoningStream and onReasoningEnd when reasoning previews are allowed", () => {
|
|
739
|
-
const { result } = createDispatcherHarness({
|
|
740
|
-
runtime: createRuntimeLogger(),
|
|
741
|
-
allowReasoningPreview: true,
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
expect(result.replyOptions.onReasoningStream).toBeTypeOf("function");
|
|
745
|
-
expect(result.replyOptions.onReasoningEnd).toBeTypeOf("function");
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
it("omits reasoning callbacks unless reasoning previews are allowed", () => {
|
|
749
|
-
const { result } = createDispatcherHarness({
|
|
750
|
-
runtime: createRuntimeLogger(),
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
expect(result.replyOptions.onReasoningStream).toBeUndefined();
|
|
754
|
-
expect(result.replyOptions.onReasoningEnd).toBeUndefined();
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
it("omits reasoning callbacks when streaming is disabled", () => {
|
|
758
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
759
|
-
accountId: "main",
|
|
760
|
-
appId: "app_id",
|
|
761
|
-
appSecret: "app_secret",
|
|
762
|
-
domain: "feishu",
|
|
763
|
-
config: {
|
|
764
|
-
renderMode: "auto",
|
|
765
|
-
streaming: false,
|
|
766
|
-
},
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
const { result } = createDispatcherHarness({
|
|
770
|
-
runtime: createRuntimeLogger(),
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
expect(result.replyOptions.onReasoningStream).toBeUndefined();
|
|
774
|
-
expect(result.replyOptions.onReasoningEnd).toBeUndefined();
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
it("renders reasoning-only card when no answer text arrives", async () => {
|
|
778
|
-
const { result, options } = createDispatcherHarness({
|
|
779
|
-
runtime: createRuntimeLogger(),
|
|
780
|
-
allowReasoningPreview: true,
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
await options.onReplyStart?.();
|
|
784
|
-
result.replyOptions.onReasoningStream?.({ text: "Reasoning:\n_deep thought_" });
|
|
785
|
-
result.replyOptions.onReasoningEnd?.();
|
|
786
|
-
await options.onIdle?.();
|
|
787
|
-
|
|
788
|
-
expect(streamingInstances).toHaveLength(1);
|
|
789
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
790
|
-
const closeArg = streamingInstances[0].close.mock.calls[0][0] as string;
|
|
791
|
-
expect(closeArg).toContain("> 💭 **Thinking**");
|
|
792
|
-
expect(closeArg).toContain("> deep thought");
|
|
793
|
-
expect(closeArg).not.toContain("Reasoning:");
|
|
794
|
-
expect(closeArg).not.toContain("---");
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
it("ignores empty reasoning payloads", async () => {
|
|
798
|
-
const { result, options } = createDispatcherHarness({
|
|
799
|
-
runtime: createRuntimeLogger(),
|
|
800
|
-
allowReasoningPreview: true,
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
await options.onReplyStart?.();
|
|
804
|
-
result.replyOptions.onReasoningStream?.({ text: "" });
|
|
805
|
-
result.replyOptions.onPartialReply?.({ text: "```ts\ncode\n```" });
|
|
806
|
-
await options.deliver({ text: "```ts\ncode\n```" }, { kind: "final" });
|
|
807
|
-
await options.onIdle?.();
|
|
808
|
-
|
|
809
|
-
expect(streamingInstances).toHaveLength(1);
|
|
810
|
-
const closeArg = streamingInstances[0].close.mock.calls[0][0] as string;
|
|
811
|
-
expect(closeArg).not.toContain("Thinking");
|
|
812
|
-
expect(closeArg).toBe("```ts\ncode\n```");
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
it("deduplicates final text by raw answer payload, not combined card text", async () => {
|
|
816
|
-
const { result, options } = createDispatcherHarness({
|
|
817
|
-
runtime: createRuntimeLogger(),
|
|
818
|
-
allowReasoningPreview: true,
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
await options.onReplyStart?.();
|
|
822
|
-
result.replyOptions.onReasoningStream?.({ text: "Reasoning:\n_thought_" });
|
|
823
|
-
result.replyOptions.onReasoningEnd?.();
|
|
824
|
-
await options.deliver({ text: "```ts\nfinal answer\n```" }, { kind: "final" });
|
|
825
|
-
await options.onIdle?.();
|
|
826
|
-
|
|
827
|
-
expect(streamingInstances).toHaveLength(1);
|
|
828
|
-
expect(streamingInstances[0].close).toHaveBeenCalledTimes(1);
|
|
829
|
-
|
|
830
|
-
// Deliver the same raw answer text again — should be deduped
|
|
831
|
-
await options.deliver({ text: "```ts\nfinal answer\n```" }, { kind: "final" });
|
|
832
|
-
|
|
833
|
-
// No second streaming session since the raw answer text matches
|
|
834
|
-
expect(streamingInstances).toHaveLength(1);
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it("passes replyToMessageId and replyInThread to streaming.start()", async () => {
|
|
838
|
-
const { options } = createDispatcherHarness({
|
|
839
|
-
runtime: createRuntimeLogger(),
|
|
840
|
-
replyToMessageId: "om_msg",
|
|
841
|
-
replyInThread: true,
|
|
842
|
-
});
|
|
843
|
-
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
844
|
-
|
|
845
|
-
expect(streamingInstances).toHaveLength(1);
|
|
846
|
-
expect(streamingInstances[0].start).toHaveBeenCalledWith(
|
|
847
|
-
"oc_chat",
|
|
848
|
-
"chat_id",
|
|
849
|
-
expect.objectContaining({
|
|
850
|
-
replyToMessageId: "om_msg",
|
|
851
|
-
replyInThread: true,
|
|
852
|
-
header: { title: "agent", template: "blue" },
|
|
853
|
-
note: "Agent: agent",
|
|
854
|
-
}),
|
|
855
|
-
);
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
it("uses streaming cards for thread replies and keeps topic metadata", async () => {
|
|
859
|
-
const { options } = createDispatcherHarness({
|
|
860
|
-
runtime: createRuntimeLogger(),
|
|
861
|
-
replyToMessageId: "om_msg",
|
|
862
|
-
replyInThread: false,
|
|
863
|
-
threadReply: true,
|
|
864
|
-
rootId: "om_root_topic",
|
|
865
|
-
});
|
|
866
|
-
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
867
|
-
|
|
868
|
-
expect(streamingInstances).toHaveLength(1);
|
|
869
|
-
expect(streamingInstances[0].start).toHaveBeenCalledWith(
|
|
870
|
-
"oc_chat",
|
|
871
|
-
"chat_id",
|
|
872
|
-
expect.objectContaining({
|
|
873
|
-
replyToMessageId: "om_msg",
|
|
874
|
-
replyInThread: true,
|
|
875
|
-
rootId: "om_root_topic",
|
|
876
|
-
}),
|
|
877
|
-
);
|
|
878
|
-
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
it("omits the generic main header from streaming and static cards", async () => {
|
|
882
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
883
|
-
accountId: "main",
|
|
884
|
-
appId: "app_id",
|
|
885
|
-
appSecret: "app_secret",
|
|
886
|
-
domain: "feishu",
|
|
887
|
-
config: {
|
|
888
|
-
renderMode: "card",
|
|
889
|
-
streaming: true,
|
|
890
|
-
},
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
const { options } = createDispatcherHarness({
|
|
894
|
-
agentId: "main",
|
|
895
|
-
runtime: createRuntimeLogger(),
|
|
896
|
-
});
|
|
897
|
-
await options.deliver({ text: "streamed card" }, { kind: "final" });
|
|
898
|
-
await options.onIdle?.();
|
|
899
|
-
|
|
900
|
-
expect(streamingInstances[0].start).toHaveBeenCalledWith(
|
|
901
|
-
"oc_chat",
|
|
902
|
-
"chat_id",
|
|
903
|
-
expect.objectContaining({
|
|
904
|
-
header: undefined,
|
|
905
|
-
}),
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
909
|
-
accountId: "main",
|
|
910
|
-
appId: "app_id",
|
|
911
|
-
appSecret: "app_secret",
|
|
912
|
-
domain: "feishu",
|
|
913
|
-
config: {
|
|
914
|
-
renderMode: "card",
|
|
915
|
-
streaming: false,
|
|
916
|
-
},
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
const { options: staticOptions } = createDispatcherHarness({
|
|
920
|
-
agentId: "main",
|
|
921
|
-
runtime: createRuntimeLogger(),
|
|
922
|
-
});
|
|
923
|
-
await staticOptions.deliver({ text: "static card" }, { kind: "final" });
|
|
924
|
-
|
|
925
|
-
expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
|
|
926
|
-
expect.objectContaining({
|
|
927
|
-
header: undefined,
|
|
928
|
-
}),
|
|
929
|
-
);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
it("shows transient tool status on streaming cards but omits it from the final close", async () => {
|
|
933
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
934
|
-
accountId: "main",
|
|
935
|
-
appId: "app_id",
|
|
936
|
-
appSecret: "app_secret",
|
|
937
|
-
domain: "feishu",
|
|
938
|
-
config: {
|
|
939
|
-
renderMode: "card",
|
|
940
|
-
streaming: true,
|
|
941
|
-
},
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
const { result, options } = createDispatcherHarness({
|
|
945
|
-
runtime: createRuntimeLogger(),
|
|
946
|
-
});
|
|
947
|
-
await options.onReplyStart?.();
|
|
948
|
-
result.replyOptions.onToolStart?.({ name: "web_search" });
|
|
949
|
-
result.replyOptions.onPartialReply?.({ text: "final answer" });
|
|
950
|
-
await options.onIdle?.();
|
|
951
|
-
|
|
952
|
-
const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) =>
|
|
953
|
-
typeof call[0] === "string" ? call[0] : "",
|
|
954
|
-
);
|
|
955
|
-
expect(updateTexts.some((text) => text.includes("Using: web_search"))).toBe(true);
|
|
956
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("final answer", {
|
|
957
|
-
note: "Agent: agent",
|
|
958
|
-
});
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
it("does not suppress a later final after error closeout", async () => {
|
|
962
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
963
|
-
accountId: "main",
|
|
964
|
-
appId: "app_id",
|
|
965
|
-
appSecret: "app_secret",
|
|
966
|
-
domain: "feishu",
|
|
967
|
-
config: {
|
|
968
|
-
renderMode: "card",
|
|
969
|
-
streaming: true,
|
|
970
|
-
},
|
|
971
|
-
});
|
|
972
|
-
sendMediaFeishuMock.mockRejectedValueOnce(new Error("media failed"));
|
|
973
|
-
|
|
974
|
-
const { options } = createDispatcherHarness({
|
|
975
|
-
runtime: createRuntimeLogger(),
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
await expect(
|
|
979
|
-
options.deliver(
|
|
980
|
-
{ text: "First answer", mediaUrl: "https://example.com/a.png" },
|
|
981
|
-
{ kind: "final" },
|
|
982
|
-
),
|
|
983
|
-
).rejects.toThrow("media failed");
|
|
984
|
-
await Promise.all([
|
|
985
|
-
options.onError?.(new Error("media failed"), { kind: "final" }),
|
|
986
|
-
options.onIdle?.(),
|
|
987
|
-
]);
|
|
988
|
-
await options.deliver({ text: "Second answer" }, { kind: "final" });
|
|
989
|
-
await options.onIdle?.();
|
|
990
|
-
|
|
991
|
-
expect(streamingInstances).toHaveLength(2);
|
|
992
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("First answer", {
|
|
993
|
-
note: "Agent: agent",
|
|
994
|
-
});
|
|
995
|
-
expect(streamingInstances[1].close).toHaveBeenCalledWith("Second answer", {
|
|
996
|
-
note: "Agent: agent",
|
|
997
|
-
});
|
|
998
|
-
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
999
|
-
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
it("does not suppress a recovery final after late media failure", async () => {
|
|
1003
|
-
resolveFeishuAccountMock.mockReturnValue({
|
|
1004
|
-
accountId: "main",
|
|
1005
|
-
appId: "app_id",
|
|
1006
|
-
appSecret: "app_secret",
|
|
1007
|
-
domain: "feishu",
|
|
1008
|
-
config: {
|
|
1009
|
-
renderMode: "card",
|
|
1010
|
-
streaming: true,
|
|
1011
|
-
},
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
const { options } = createDispatcherHarness({
|
|
1015
|
-
runtime: createRuntimeLogger(),
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
await options.deliver({ text: "First answer" }, { kind: "final" });
|
|
1019
|
-
await options.onIdle?.();
|
|
1020
|
-
sendMediaFeishuMock.mockRejectedValueOnce(new Error("media failed"));
|
|
1021
|
-
await expect(
|
|
1022
|
-
options.deliver(
|
|
1023
|
-
{ text: "Late attachment", mediaUrl: "https://example.com/a.png" },
|
|
1024
|
-
{ kind: "final" },
|
|
1025
|
-
),
|
|
1026
|
-
).rejects.toThrow("media failed");
|
|
1027
|
-
await options.onError?.(new Error("media failed"), { kind: "final" });
|
|
1028
|
-
await options.deliver({ text: "Recovered answer" }, { kind: "final" });
|
|
1029
|
-
await options.onIdle?.();
|
|
1030
|
-
|
|
1031
|
-
expect(streamingInstances).toHaveLength(2);
|
|
1032
|
-
expect(streamingInstances[0].close).toHaveBeenCalledWith("First answer", {
|
|
1033
|
-
note: "Agent: agent",
|
|
1034
|
-
});
|
|
1035
|
-
expect(streamingInstances[1].close).toHaveBeenCalledWith("Recovered answer", {
|
|
1036
|
-
note: "Agent: agent",
|
|
1037
|
-
});
|
|
1038
|
-
expect(sendStructuredCardFeishuMock).not.toHaveBeenCalled();
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
it("cleans streaming state even when close throws", async () => {
|
|
1042
|
-
const origPush = streamingInstances.push.bind(streamingInstances);
|
|
1043
|
-
streamingInstances.push = (...args: StreamingSessionStub[]) => {
|
|
1044
|
-
if (args.length > 0 && streamingInstances.length === 0) {
|
|
1045
|
-
args[0].close = vi.fn(async () => {
|
|
1046
|
-
args[0].active = false;
|
|
1047
|
-
throw new Error("close failed");
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
return origPush(...args);
|
|
1051
|
-
};
|
|
1052
|
-
|
|
1053
|
-
try {
|
|
1054
|
-
const { options } = createDispatcherHarness({
|
|
1055
|
-
runtime: createRuntimeLogger(),
|
|
1056
|
-
});
|
|
1057
|
-
await options.deliver({ text: "```md\nfirst\n```" }, { kind: "final" });
|
|
1058
|
-
await expect(options.onIdle?.()).rejects.toThrow("close failed");
|
|
1059
|
-
await options.deliver({ text: "```md\nsecond\n```" }, { kind: "final" });
|
|
1060
|
-
await options.onIdle?.();
|
|
1061
|
-
|
|
1062
|
-
expect(streamingInstances).toHaveLength(2);
|
|
1063
|
-
expect(streamingInstances[1].close).toHaveBeenCalledWith("```md\nsecond\n```", {
|
|
1064
|
-
note: "Agent: agent",
|
|
1065
|
-
});
|
|
1066
|
-
} finally {
|
|
1067
|
-
streamingInstances.push = origPush;
|
|
1068
|
-
}
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
it("passes replyInThread to media attachments", async () => {
|
|
1072
|
-
const { options } = createDispatcherHarness({
|
|
1073
|
-
replyToMessageId: "om_msg",
|
|
1074
|
-
replyInThread: true,
|
|
1075
|
-
});
|
|
1076
|
-
await options.deliver({ mediaUrl: "https://example.com/a.png" }, { kind: "final" });
|
|
1077
|
-
|
|
1078
|
-
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
1079
|
-
expect.objectContaining({
|
|
1080
|
-
replyToMessageId: "om_msg",
|
|
1081
|
-
replyInThread: true,
|
|
1082
|
-
}),
|
|
1083
|
-
);
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
it("backs off streaming retries after start() throws (HTTP 400)", async () => {
|
|
1087
|
-
const errorMock = vi.fn();
|
|
1088
|
-
let shouldFailStart = true;
|
|
1089
|
-
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_000);
|
|
1090
|
-
|
|
1091
|
-
// Intercept streaming instance creation to make first start() reject
|
|
1092
|
-
const origPush = streamingInstances.push.bind(streamingInstances);
|
|
1093
|
-
streamingInstances.push = (...args: StreamingSessionStub[]) => {
|
|
1094
|
-
if (shouldFailStart) {
|
|
1095
|
-
args[0].start = vi
|
|
1096
|
-
.fn()
|
|
1097
|
-
.mockRejectedValue(new Error("Create card request failed with HTTP 400"));
|
|
1098
|
-
shouldFailStart = false;
|
|
1099
|
-
}
|
|
1100
|
-
return origPush(...args);
|
|
1101
|
-
};
|
|
1102
|
-
|
|
1103
|
-
try {
|
|
1104
|
-
createFeishuReplyDispatcher({
|
|
1105
|
-
cfg: {} as never,
|
|
1106
|
-
agentId: "agent",
|
|
1107
|
-
runtime: { log: vi.fn(), error: errorMock } as never,
|
|
1108
|
-
chatId: "oc_chat",
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
|
|
1112
|
-
|
|
1113
|
-
// First deliver with markdown triggers startStreaming - which will fail
|
|
1114
|
-
await options.deliver({ text: "```ts\nconst x = 1\n```" }, { kind: "final" });
|
|
1115
|
-
|
|
1116
|
-
// Wait for the async error to propagate
|
|
1117
|
-
await vi.waitFor(() => {
|
|
1118
|
-
expect(errorMock).toHaveBeenCalledWith(expect.stringContaining("streaming start failed"));
|
|
1119
|
-
});
|
|
1120
|
-
expect(streamingInstances).toHaveLength(1);
|
|
1121
|
-
expect(sendStructuredCardFeishuMock).toHaveBeenCalledTimes(1);
|
|
1122
|
-
|
|
1123
|
-
// Immediate next markdown reply should skip a new streaming start and
|
|
1124
|
-
// fall back directly to a normal card instead of paying the 400 latency.
|
|
1125
|
-
await options.deliver({ text: "```ts\nconst y = 2\n```" }, { kind: "final" });
|
|
1126
|
-
|
|
1127
|
-
expect(streamingInstances).toHaveLength(1);
|
|
1128
|
-
expect(sendStructuredCardFeishuMock).toHaveBeenCalledTimes(2);
|
|
1129
|
-
|
|
1130
|
-
// After the short backoff expires, retry streaming so fixed permissions
|
|
1131
|
-
// or transient Feishu failures recover without a process restart.
|
|
1132
|
-
nowSpy.mockReturnValue(62_000);
|
|
1133
|
-
await options.deliver({ text: "```ts\nconst z = 3\n```" }, { kind: "final" });
|
|
1134
|
-
await options.onIdle?.();
|
|
1135
|
-
|
|
1136
|
-
expect(streamingInstances).toHaveLength(2);
|
|
1137
|
-
expect(streamingInstances[1].start).toHaveBeenCalled();
|
|
1138
|
-
expect(streamingInstances[1].close).toHaveBeenCalled();
|
|
1139
|
-
} finally {
|
|
1140
|
-
streamingInstances.push = origPush;
|
|
1141
|
-
nowSpy.mockRestore();
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
});
|