@openclaw/feishu 2026.3.12 → 2026.3.13
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/package.json +1 -1
- package/src/accounts.test.ts +21 -16
- package/src/bot.ts +20 -11
- package/src/config-schema.test.ts +14 -24
- package/src/dedup.ts +103 -0
- package/src/media.test.ts +38 -61
- package/src/media.ts +64 -76
- package/src/monitor.account.ts +19 -18
- package/src/monitor.reaction.test.ts +106 -64
- package/src/monitor.startup.test.ts +16 -30
- package/src/monitor.transport.ts +104 -6
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +9 -97
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/outbound.test.ts +11 -16
- package/src/probe.test.ts +112 -113
- package/src/reactions.ts +20 -27
- package/src/reply-dispatcher.test.ts +65 -143
- package/src/reply-dispatcher.ts +37 -40
- package/src/send.reply-fallback.test.ts +50 -40
- package/src/send.ts +91 -82
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createFeishuRuntimeMockModule } from "./monitor.test-mocks.js";
|
|
4
|
+
import { withRunningWebhookMonitor } from "./monitor.webhook.test-helpers.js";
|
|
5
|
+
|
|
6
|
+
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("./probe.js", () => ({
|
|
9
|
+
probeFeishu: probeFeishuMock,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("./client.js", async () => {
|
|
13
|
+
const actual = await vi.importActual<typeof import("./client.js")>("./client.js");
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
vi.mock("./runtime.js", () => createFeishuRuntimeMockModule());
|
|
21
|
+
|
|
22
|
+
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
|
|
23
|
+
|
|
24
|
+
function signFeishuPayload(params: {
|
|
25
|
+
encryptKey: string;
|
|
26
|
+
payload: Record<string, unknown>;
|
|
27
|
+
timestamp?: string;
|
|
28
|
+
nonce?: string;
|
|
29
|
+
}): Record<string, string> {
|
|
30
|
+
const timestamp = params.timestamp ?? "1711111111";
|
|
31
|
+
const nonce = params.nonce ?? "nonce-test";
|
|
32
|
+
const signature = crypto
|
|
33
|
+
.createHash("sha256")
|
|
34
|
+
.update(timestamp + nonce + params.encryptKey + JSON.stringify(params.payload))
|
|
35
|
+
.digest("hex");
|
|
36
|
+
return {
|
|
37
|
+
"content-type": "application/json",
|
|
38
|
+
"x-lark-request-timestamp": timestamp,
|
|
39
|
+
"x-lark-request-nonce": nonce,
|
|
40
|
+
"x-lark-signature": signature,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function encryptFeishuPayload(encryptKey: string, payload: Record<string, unknown>): string {
|
|
45
|
+
const iv = crypto.randomBytes(16);
|
|
46
|
+
const key = crypto.createHash("sha256").update(encryptKey).digest();
|
|
47
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
|
48
|
+
const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
|
|
49
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
50
|
+
return Buffer.concat([iv, encrypted]).toString("base64");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function postSignedPayload(url: string, payload: Record<string, unknown>) {
|
|
54
|
+
return await fetch(url, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: signFeishuPayload({ encryptKey: "encrypt_key", payload }),
|
|
57
|
+
body: JSON.stringify(payload),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
stopFeishuMonitor();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Feishu webhook signed-request e2e", () => {
|
|
66
|
+
it("rejects invalid signatures with 401 instead of empty 200", async () => {
|
|
67
|
+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
68
|
+
|
|
69
|
+
await withRunningWebhookMonitor(
|
|
70
|
+
{
|
|
71
|
+
accountId: "invalid-signature",
|
|
72
|
+
path: "/hook-e2e-invalid-signature",
|
|
73
|
+
verificationToken: "verify_token",
|
|
74
|
+
encryptKey: "encrypt_key",
|
|
75
|
+
},
|
|
76
|
+
monitorFeishuProvider,
|
|
77
|
+
async (url) => {
|
|
78
|
+
const payload = { type: "url_verification", challenge: "challenge-token" };
|
|
79
|
+
const response = await fetch(url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
...signFeishuPayload({ encryptKey: "wrong_key", payload }),
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(payload),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(response.status).toBe(401);
|
|
88
|
+
expect(await response.text()).toBe("Invalid signature");
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects missing signature headers with 401", async () => {
|
|
94
|
+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
95
|
+
|
|
96
|
+
await withRunningWebhookMonitor(
|
|
97
|
+
{
|
|
98
|
+
accountId: "missing-signature",
|
|
99
|
+
path: "/hook-e2e-missing-signature",
|
|
100
|
+
verificationToken: "verify_token",
|
|
101
|
+
encryptKey: "encrypt_key",
|
|
102
|
+
},
|
|
103
|
+
monitorFeishuProvider,
|
|
104
|
+
async (url) => {
|
|
105
|
+
const response = await fetch(url, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
body: JSON.stringify({ type: "url_verification", challenge: "challenge-token" }),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(response.status).toBe(401);
|
|
112
|
+
expect(await response.text()).toBe("Invalid signature");
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns 400 for invalid json before invoking the sdk", async () => {
|
|
118
|
+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
119
|
+
|
|
120
|
+
await withRunningWebhookMonitor(
|
|
121
|
+
{
|
|
122
|
+
accountId: "invalid-json",
|
|
123
|
+
path: "/hook-e2e-invalid-json",
|
|
124
|
+
verificationToken: "verify_token",
|
|
125
|
+
encryptKey: "encrypt_key",
|
|
126
|
+
},
|
|
127
|
+
monitorFeishuProvider,
|
|
128
|
+
async (url) => {
|
|
129
|
+
const response = await fetch(url, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { "content-type": "application/json" },
|
|
132
|
+
body: "{not-json",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(response.status).toBe(400);
|
|
136
|
+
expect(await response.text()).toBe("Invalid JSON");
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("accepts signed plaintext url_verification challenges end-to-end", async () => {
|
|
142
|
+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
143
|
+
|
|
144
|
+
await withRunningWebhookMonitor(
|
|
145
|
+
{
|
|
146
|
+
accountId: "signed-challenge",
|
|
147
|
+
path: "/hook-e2e-signed-challenge",
|
|
148
|
+
verificationToken: "verify_token",
|
|
149
|
+
encryptKey: "encrypt_key",
|
|
150
|
+
},
|
|
151
|
+
monitorFeishuProvider,
|
|
152
|
+
async (url) => {
|
|
153
|
+
const payload = { type: "url_verification", challenge: "challenge-token" };
|
|
154
|
+
const response = await postSignedPayload(url, payload);
|
|
155
|
+
|
|
156
|
+
expect(response.status).toBe(200);
|
|
157
|
+
await expect(response.json()).resolves.toEqual({ challenge: "challenge-token" });
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("accepts signed non-challenge events and reaches the dispatcher", async () => {
|
|
163
|
+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
164
|
+
|
|
165
|
+
await withRunningWebhookMonitor(
|
|
166
|
+
{
|
|
167
|
+
accountId: "signed-dispatch",
|
|
168
|
+
path: "/hook-e2e-signed-dispatch",
|
|
169
|
+
verificationToken: "verify_token",
|
|
170
|
+
encryptKey: "encrypt_key",
|
|
171
|
+
},
|
|
172
|
+
monitorFeishuProvider,
|
|
173
|
+
async (url) => {
|
|
174
|
+
const payload = {
|
|
175
|
+
schema: "2.0",
|
|
176
|
+
header: { event_type: "unknown.event" },
|
|
177
|
+
event: {},
|
|
178
|
+
};
|
|
179
|
+
const response = await postSignedPayload(url, payload);
|
|
180
|
+
|
|
181
|
+
expect(response.status).toBe(200);
|
|
182
|
+
expect(await response.text()).toContain("no unknown.event event handle");
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("accepts signed encrypted url_verification challenges end-to-end", async () => {
|
|
188
|
+
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
189
|
+
|
|
190
|
+
await withRunningWebhookMonitor(
|
|
191
|
+
{
|
|
192
|
+
accountId: "encrypted-challenge",
|
|
193
|
+
path: "/hook-e2e-encrypted-challenge",
|
|
194
|
+
verificationToken: "verify_token",
|
|
195
|
+
encryptKey: "encrypt_key",
|
|
196
|
+
},
|
|
197
|
+
monitorFeishuProvider,
|
|
198
|
+
async (url) => {
|
|
199
|
+
const payload = {
|
|
200
|
+
encrypt: encryptFeishuPayload("encrypt_key", {
|
|
201
|
+
type: "url_verification",
|
|
202
|
+
challenge: "encrypted-challenge-token",
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
const response = await postSignedPayload(url, payload);
|
|
206
|
+
|
|
207
|
+
expect(response.status).toBe(200);
|
|
208
|
+
await expect(response.json()).resolves.toEqual({
|
|
209
|
+
challenge: "encrypted-challenge-token",
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import type { AddressInfo } from "node:net";
|
|
3
|
-
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
4
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
2
|
import {
|
|
6
3
|
createFeishuClientMockModule,
|
|
7
4
|
createFeishuRuntimeMockModule,
|
|
8
5
|
} from "./monitor.test-mocks.js";
|
|
6
|
+
import {
|
|
7
|
+
buildWebhookConfig,
|
|
8
|
+
getFreePort,
|
|
9
|
+
withRunningWebhookMonitor,
|
|
10
|
+
} from "./monitor.webhook.test-helpers.js";
|
|
9
11
|
|
|
10
12
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
11
13
|
|
|
@@ -33,98 +35,6 @@ import {
|
|
|
33
35
|
stopFeishuMonitor,
|
|
34
36
|
} from "./monitor.js";
|
|
35
37
|
|
|
36
|
-
async function getFreePort(): Promise<number> {
|
|
37
|
-
const server = createServer();
|
|
38
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
39
|
-
const address = server.address() as AddressInfo | null;
|
|
40
|
-
if (!address) {
|
|
41
|
-
throw new Error("missing server address");
|
|
42
|
-
}
|
|
43
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
44
|
-
return address.port;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function waitUntilServerReady(url: string): Promise<void> {
|
|
48
|
-
for (let i = 0; i < 50; i += 1) {
|
|
49
|
-
try {
|
|
50
|
-
const response = await fetch(url, { method: "GET" });
|
|
51
|
-
if (response.status >= 200 && response.status < 500) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// retry
|
|
56
|
-
}
|
|
57
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
58
|
-
}
|
|
59
|
-
throw new Error(`server did not start: ${url}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildConfig(params: {
|
|
63
|
-
accountId: string;
|
|
64
|
-
path: string;
|
|
65
|
-
port: number;
|
|
66
|
-
verificationToken?: string;
|
|
67
|
-
encryptKey?: string;
|
|
68
|
-
}): ClawdbotConfig {
|
|
69
|
-
return {
|
|
70
|
-
channels: {
|
|
71
|
-
feishu: {
|
|
72
|
-
enabled: true,
|
|
73
|
-
accounts: {
|
|
74
|
-
[params.accountId]: {
|
|
75
|
-
enabled: true,
|
|
76
|
-
appId: "cli_test",
|
|
77
|
-
appSecret: "secret_test", // pragma: allowlist secret
|
|
78
|
-
connectionMode: "webhook",
|
|
79
|
-
webhookHost: "127.0.0.1",
|
|
80
|
-
webhookPort: params.port,
|
|
81
|
-
webhookPath: params.path,
|
|
82
|
-
encryptKey: params.encryptKey,
|
|
83
|
-
verificationToken: params.verificationToken,
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
} as ClawdbotConfig;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function withRunningWebhookMonitor(
|
|
92
|
-
params: {
|
|
93
|
-
accountId: string;
|
|
94
|
-
path: string;
|
|
95
|
-
verificationToken: string;
|
|
96
|
-
encryptKey: string;
|
|
97
|
-
},
|
|
98
|
-
run: (url: string) => Promise<void>,
|
|
99
|
-
) {
|
|
100
|
-
const port = await getFreePort();
|
|
101
|
-
const cfg = buildConfig({
|
|
102
|
-
accountId: params.accountId,
|
|
103
|
-
path: params.path,
|
|
104
|
-
port,
|
|
105
|
-
encryptKey: params.encryptKey,
|
|
106
|
-
verificationToken: params.verificationToken,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const abortController = new AbortController();
|
|
110
|
-
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
|
111
|
-
const monitorPromise = monitorFeishuProvider({
|
|
112
|
-
config: cfg,
|
|
113
|
-
runtime,
|
|
114
|
-
abortSignal: abortController.signal,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const url = `http://127.0.0.1:${port}${params.path}`;
|
|
118
|
-
await waitUntilServerReady(url);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
await run(url);
|
|
122
|
-
} finally {
|
|
123
|
-
abortController.abort();
|
|
124
|
-
await monitorPromise;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
38
|
afterEach(() => {
|
|
129
39
|
clearFeishuWebhookRateLimitStateForTest();
|
|
130
40
|
stopFeishuMonitor();
|
|
@@ -134,7 +44,7 @@ describe("Feishu webhook security hardening", () => {
|
|
|
134
44
|
it("rejects webhook mode without verificationToken", async () => {
|
|
135
45
|
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
136
46
|
|
|
137
|
-
const cfg =
|
|
47
|
+
const cfg = buildWebhookConfig({
|
|
138
48
|
accountId: "missing-token",
|
|
139
49
|
path: "/hook-missing-token",
|
|
140
50
|
port: await getFreePort(),
|
|
@@ -148,7 +58,7 @@ describe("Feishu webhook security hardening", () => {
|
|
|
148
58
|
it("rejects webhook mode without encryptKey", async () => {
|
|
149
59
|
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
|
|
150
60
|
|
|
151
|
-
const cfg =
|
|
61
|
+
const cfg = buildWebhookConfig({
|
|
152
62
|
accountId: "missing-encrypt-key",
|
|
153
63
|
path: "/hook-missing-encrypt",
|
|
154
64
|
port: await getFreePort(),
|
|
@@ -167,6 +77,7 @@ describe("Feishu webhook security hardening", () => {
|
|
|
167
77
|
verificationToken: "verify_token",
|
|
168
78
|
encryptKey: "encrypt_key",
|
|
169
79
|
},
|
|
80
|
+
monitorFeishuProvider,
|
|
170
81
|
async (url) => {
|
|
171
82
|
const response = await fetch(url, {
|
|
172
83
|
method: "POST",
|
|
@@ -189,6 +100,7 @@ describe("Feishu webhook security hardening", () => {
|
|
|
189
100
|
verificationToken: "verify_token",
|
|
190
101
|
encryptKey: "encrypt_key",
|
|
191
102
|
},
|
|
103
|
+
monitorFeishuProvider,
|
|
192
104
|
async (url) => {
|
|
193
105
|
let saw429 = false;
|
|
194
106
|
for (let i = 0; i < 130; i += 1) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import type { AddressInfo } from "node:net";
|
|
3
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
4
|
+
import { vi } from "vitest";
|
|
5
|
+
import type { monitorFeishuProvider } from "./monitor.js";
|
|
6
|
+
|
|
7
|
+
export async function getFreePort(): Promise<number> {
|
|
8
|
+
const server = createServer();
|
|
9
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
10
|
+
const address = server.address() as AddressInfo | null;
|
|
11
|
+
if (!address) {
|
|
12
|
+
throw new Error("missing server address");
|
|
13
|
+
}
|
|
14
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
15
|
+
return address.port;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function waitUntilServerReady(url: string): Promise<void> {
|
|
19
|
+
for (let i = 0; i < 50; i += 1) {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, { method: "GET" });
|
|
22
|
+
if (response.status >= 200 && response.status < 500) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// retry
|
|
27
|
+
}
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`server did not start: ${url}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildWebhookConfig(params: {
|
|
34
|
+
accountId: string;
|
|
35
|
+
path: string;
|
|
36
|
+
port: number;
|
|
37
|
+
verificationToken?: string;
|
|
38
|
+
encryptKey?: string;
|
|
39
|
+
}): ClawdbotConfig {
|
|
40
|
+
return {
|
|
41
|
+
channels: {
|
|
42
|
+
feishu: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
accounts: {
|
|
45
|
+
[params.accountId]: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
appId: "cli_test",
|
|
48
|
+
appSecret: "secret_test", // pragma: allowlist secret
|
|
49
|
+
connectionMode: "webhook",
|
|
50
|
+
webhookHost: "127.0.0.1",
|
|
51
|
+
webhookPort: params.port,
|
|
52
|
+
webhookPath: params.path,
|
|
53
|
+
encryptKey: params.encryptKey,
|
|
54
|
+
verificationToken: params.verificationToken,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} as ClawdbotConfig;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function withRunningWebhookMonitor(
|
|
63
|
+
params: {
|
|
64
|
+
accountId: string;
|
|
65
|
+
path: string;
|
|
66
|
+
verificationToken: string;
|
|
67
|
+
encryptKey: string;
|
|
68
|
+
},
|
|
69
|
+
monitor: typeof monitorFeishuProvider,
|
|
70
|
+
run: (url: string) => Promise<void>,
|
|
71
|
+
) {
|
|
72
|
+
const port = await getFreePort();
|
|
73
|
+
const cfg = buildWebhookConfig({
|
|
74
|
+
accountId: params.accountId,
|
|
75
|
+
path: params.path,
|
|
76
|
+
port,
|
|
77
|
+
encryptKey: params.encryptKey,
|
|
78
|
+
verificationToken: params.verificationToken,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const abortController = new AbortController();
|
|
82
|
+
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
|
83
|
+
const monitorPromise = monitor({
|
|
84
|
+
config: cfg,
|
|
85
|
+
runtime,
|
|
86
|
+
abortSignal: abortController.signal,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const url = `http://127.0.0.1:${port}${params.path}`;
|
|
90
|
+
await waitUntilServerReady(url);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await run(url);
|
|
94
|
+
} finally {
|
|
95
|
+
abortController.abort();
|
|
96
|
+
await monitorPromise;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/outbound.test.ts
CHANGED
|
@@ -29,12 +29,16 @@ vi.mock("./runtime.js", () => ({
|
|
|
29
29
|
import { feishuOutbound } from "./outbound.js";
|
|
30
30
|
const sendText = feishuOutbound.sendText!;
|
|
31
31
|
|
|
32
|
+
function resetOutboundMocks() {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
35
|
+
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
36
|
+
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
33
40
|
beforeEach(() => {
|
|
34
|
-
|
|
35
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
36
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
37
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
41
|
+
resetOutboundMocks();
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
async function createTmpImage(ext = ".png"): Promise<{ dir: string; file: string }> {
|
|
@@ -181,10 +185,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
181
185
|
|
|
182
186
|
describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
183
187
|
beforeEach(() => {
|
|
184
|
-
|
|
185
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
186
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
187
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
188
|
+
resetOutboundMocks();
|
|
188
189
|
});
|
|
189
190
|
|
|
190
191
|
it("forwards replyToId as replyToMessageId to sendMessageFeishu", async () => {
|
|
@@ -249,10 +250,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
|
249
250
|
|
|
250
251
|
describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
251
252
|
beforeEach(() => {
|
|
252
|
-
|
|
253
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
254
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
255
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
253
|
+
resetOutboundMocks();
|
|
256
254
|
});
|
|
257
255
|
|
|
258
256
|
it("forwards replyToId to sendMediaFeishu", async () => {
|
|
@@ -292,10 +290,7 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
|
292
290
|
|
|
293
291
|
describe("feishuOutbound.sendMedia renderMode", () => {
|
|
294
292
|
beforeEach(() => {
|
|
295
|
-
|
|
296
|
-
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
297
|
-
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
298
|
-
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
293
|
+
resetOutboundMocks();
|
|
299
294
|
});
|
|
300
295
|
|
|
301
296
|
it("uses markdown cards for captions when renderMode=card", async () => {
|