@capybara-hq/testing 0.0.0-stage → 0.1.0-rc.0

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.
Files changed (89) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +34 -2
  4. package/dist/containers.d.ts +102 -0
  5. package/dist/containers.js +314 -0
  6. package/dist/dual-replica.d.ts +121 -0
  7. package/dist/dual-replica.js +247 -0
  8. package/dist/extension-config.d.ts +9 -0
  9. package/dist/extension-config.js +16 -0
  10. package/dist/fake-channel.d.ts +162 -0
  11. package/dist/fake-channel.js +325 -0
  12. package/dist/fake-e2b.d.ts +104 -0
  13. package/dist/fake-e2b.js +500 -0
  14. package/dist/fake-git-server.d.ts +57 -0
  15. package/dist/fake-git-server.js +323 -0
  16. package/dist/fake-lark.d.ts +234 -0
  17. package/dist/fake-lark.js +837 -0
  18. package/dist/fake-marketplace.d.ts +25 -0
  19. package/dist/fake-marketplace.js +58 -0
  20. package/dist/fake-mcp-server.d.ts +134 -0
  21. package/dist/fake-mcp-server.js +308 -0
  22. package/dist/fake-model-server.d.ts +28 -0
  23. package/dist/fake-model-server.js +67 -0
  24. package/dist/fake-npm-registry.d.ts +59 -0
  25. package/dist/fake-npm-registry.js +174 -0
  26. package/dist/fake-oauth-server.d.ts +63 -0
  27. package/dist/fake-oauth-server.js +284 -0
  28. package/dist/fake-openapi-server.d.ts +50 -0
  29. package/dist/fake-openapi-server.js +278 -0
  30. package/dist/fake-slack.d.ts +258 -0
  31. package/dist/fake-slack.js +654 -0
  32. package/dist/fixtures.d.ts +47 -0
  33. package/dist/fixtures.js +90 -0
  34. package/dist/global-setup.d.ts +15 -0
  35. package/dist/global-setup.js +58 -0
  36. package/dist/index.d.ts +25 -0
  37. package/dist/index.js +25 -0
  38. package/dist/kernel-helpers.d.ts +125 -0
  39. package/dist/kernel-helpers.js +336 -0
  40. package/dist/media-samples.d.ts +10 -0
  41. package/dist/media-samples.js +76 -0
  42. package/dist/model-conformance.d.ts +61 -0
  43. package/dist/model-conformance.js +287 -0
  44. package/dist/replica-runner.d.ts +1 -0
  45. package/dist/replica-runner.js +59 -0
  46. package/dist/sse.d.ts +20 -0
  47. package/dist/sse.js +87 -0
  48. package/fixtures/extensions/fake-bootstrap-a/.capybara-plugin/plugin.json +7 -0
  49. package/fixtures/extensions/fake-bootstrap-a/package.json +12 -0
  50. package/fixtures/extensions/fake-bootstrap-a/src/index.ts +62 -0
  51. package/fixtures/extensions/fake-bootstrap-b/.capybara-plugin/plugin.json +7 -0
  52. package/fixtures/extensions/fake-bootstrap-b/migrations/0001_init.sql +10 -0
  53. package/fixtures/extensions/fake-bootstrap-b/package.json +12 -0
  54. package/fixtures/extensions/fake-bootstrap-b/src/index.ts +123 -0
  55. package/fixtures/extensions/fake-connector/.capybara-plugin/plugin.json +7 -0
  56. package/fixtures/extensions/fake-connector/connectors/fake-demo.json +12 -0
  57. package/fixtures/extensions/fake-connector/package.json +13 -0
  58. package/fixtures/extensions/fake-connector/src/index.ts +477 -0
  59. package/fixtures/extensions/fake-dep-provider/.capybara-plugin/plugin.json +7 -0
  60. package/fixtures/extensions/fake-dep-provider/package.json +12 -0
  61. package/fixtures/extensions/fake-dep-provider/src/index.ts +19 -0
  62. package/fixtures/extensions/fake-dep-requirer/.capybara-plugin/plugin.json +8 -0
  63. package/fixtures/extensions/fake-dep-requirer/package.json +12 -0
  64. package/fixtures/extensions/fake-dep-requirer/src/index.ts +21 -0
  65. package/fixtures/extensions/fake-embed/.capybara-plugin/plugin.json +7 -0
  66. package/fixtures/extensions/fake-embed/package.json +12 -0
  67. package/fixtures/extensions/fake-embed/src/index.ts +207 -0
  68. package/fixtures/extensions/fake-im/.capybara-plugin/plugin.json +7 -0
  69. package/fixtures/extensions/fake-im/package.json +12 -0
  70. package/fixtures/extensions/fake-im/src/channel.ts +224 -0
  71. package/fixtures/extensions/fake-im/src/directory.ts +25 -0
  72. package/fixtures/extensions/fake-im/src/identity.ts +66 -0
  73. package/fixtures/extensions/fake-im/src/index.ts +91 -0
  74. package/fixtures/extensions/fake-object-store/.capybara-plugin/plugin.json +7 -0
  75. package/fixtures/extensions/fake-object-store/package.json +15 -0
  76. package/fixtures/extensions/fake-object-store/src/index.ts +210 -0
  77. package/fixtures/extensions/fake-openapi/.capybara-plugin/plugin.json +7 -0
  78. package/fixtures/extensions/fake-openapi/package.json +12 -0
  79. package/fixtures/extensions/fake-openapi/src/index.ts +258 -0
  80. package/fixtures/extensions/fake-storage-user/.capybara-plugin/plugin.json +7 -0
  81. package/fixtures/extensions/fake-storage-user/package.json +12 -0
  82. package/fixtures/extensions/fake-storage-user/src/index.ts +58 -0
  83. package/fixtures/extensions/fake-tools-ui/.capybara-plugin/plugin.json +7 -0
  84. package/fixtures/extensions/fake-tools-ui/connectors/fake-notes.json +11 -0
  85. package/fixtures/extensions/fake-tools-ui/migrations/0001_init.sql +7 -0
  86. package/fixtures/extensions/fake-tools-ui/package.json +12 -0
  87. package/fixtures/extensions/fake-tools-ui/skills/fake-notes/SKILL.md +7 -0
  88. package/fixtures/extensions/fake-tools-ui/src/index.ts +140 -0
  89. package/package.json +66 -3
@@ -0,0 +1,278 @@
1
+ import { createServer } from "node:http";
2
+ /** 1×1 透明 PNG。 */
3
+ const PNG = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64");
4
+ /**
5
+ * 默认文档(OpenAPI 3.0.3):宠物 CRUD + 几个用于测试的操作。servers 用服务器变量给出端口(测试默认值替换),
6
+ * 参数与模式用内部 `$ref`,认证为 API key。
7
+ */
8
+ export function defaultFakeOpenApiSpec(base, apiKeyIn = "header") {
9
+ const port = new URL(base).port;
10
+ return {
11
+ openapi: "3.0.3",
12
+ info: { title: "Fake Pet Store", version: "1.0.0" },
13
+ servers: [{ url: "http://127.0.0.1:{port}/v1", variables: { port: { default: port } } }],
14
+ security: [{ apiKey: [] }],
15
+ paths: {
16
+ "/pets": {
17
+ get: {
18
+ operationId: "listPets",
19
+ summary: "List pets",
20
+ description: "Returns all pets, optionally filtered by tag.",
21
+ parameters: [
22
+ {
23
+ name: "limit",
24
+ in: "query",
25
+ description: "Maximum number of pets to return",
26
+ schema: { type: "integer", minimum: 1 },
27
+ },
28
+ {
29
+ name: "tags",
30
+ in: "query",
31
+ description: "Only pets with one of these tags",
32
+ schema: { type: "array", items: { type: "string" } },
33
+ },
34
+ ],
35
+ responses: { "200": { description: "Pets" } },
36
+ },
37
+ post: {
38
+ operationId: "createPet",
39
+ summary: "Create a pet",
40
+ requestBody: {
41
+ required: true,
42
+ content: { "application/json": { schema: { $ref: "#/components/schemas/NewPet" } } },
43
+ },
44
+ responses: { "201": { description: "Created" } },
45
+ },
46
+ },
47
+ "/pets/{petId}": {
48
+ parameters: [{ $ref: "#/components/parameters/PetId" }],
49
+ get: { operationId: "getPet", summary: "Get a pet by id", responses: {} },
50
+ patch: {
51
+ operationId: "updatePet",
52
+ summary: "Update a pet",
53
+ requestBody: {
54
+ content: { "application/json": { schema: { $ref: "#/components/schemas/NewPet" } } },
55
+ },
56
+ responses: {},
57
+ },
58
+ delete: { operationId: "deletePet", summary: "Delete a pet", responses: {} },
59
+ },
60
+ "/pets/{petId}/photo": {
61
+ parameters: [{ $ref: "#/components/parameters/PetId" }],
62
+ get: { operationId: "getPetPhoto", summary: "Download the pet's photo", responses: {} },
63
+ },
64
+ "/echo": {
65
+ get: {
66
+ operationId: "echo",
67
+ summary: "Echo the query string and trace header",
68
+ parameters: [
69
+ { name: "ids", in: "query", explode: false, schema: { type: "array", items: {} } },
70
+ {
71
+ name: "filter",
72
+ in: "query",
73
+ style: "deepObject",
74
+ schema: { type: "object", additionalProperties: { type: "string" } },
75
+ },
76
+ { name: "X-Trace-Id", in: "header", schema: { type: "string" } },
77
+ ],
78
+ responses: {},
79
+ },
80
+ },
81
+ "/notes": {
82
+ post: {
83
+ operationId: "createNote",
84
+ summary: "Store a plain-text note",
85
+ requestBody: {
86
+ required: true,
87
+ content: { "text/plain": { schema: { type: "string" } } },
88
+ },
89
+ responses: {},
90
+ },
91
+ },
92
+ "/boom": { get: { operationId: "boom", summary: "Always fails", responses: {} } },
93
+ "/moved": { get: { operationId: "moved", summary: "Always redirects", responses: {} } },
94
+ },
95
+ components: {
96
+ parameters: {
97
+ PetId: {
98
+ name: "petId",
99
+ in: "path",
100
+ required: true,
101
+ description: "The pet id",
102
+ schema: { type: "integer" },
103
+ },
104
+ },
105
+ schemas: {
106
+ NewPet: {
107
+ type: "object",
108
+ required: ["name"],
109
+ properties: {
110
+ name: { type: "string" },
111
+ tag: { type: "string", nullable: true },
112
+ },
113
+ },
114
+ },
115
+ securitySchemes: {
116
+ apiKey: apiKeyIn === "header"
117
+ ? { type: "apiKey", in: "header", name: "X-API-Key" }
118
+ : { type: "apiKey", in: "query", name: "api_key" },
119
+ },
120
+ },
121
+ };
122
+ }
123
+ const readBody = (req) => new Promise((resolve, reject) => {
124
+ const chunks = [];
125
+ req.on("data", (c) => chunks.push(c));
126
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
127
+ req.on("error", reject);
128
+ });
129
+ const json = (res, status, body) => {
130
+ res.writeHead(status, { "content-type": "application/json" });
131
+ res.end(JSON.stringify(body));
132
+ };
133
+ /**
134
+ * 本地假 OpenAPI 服务(127.0.0.1 随机端口):`/openapi.json` 给出文档(或按选项 302),`/v1/*` 实现文档里的
135
+ * 宠物 CRUD(内存)、回显、纯文本、二进制、500 与 302 操作;可要求 API key(请求头或 query)。
136
+ */
137
+ export async function startFakeOpenApiServer(opts = {}) {
138
+ const keyIn = opts.apiKeyIn ?? "header";
139
+ const requests = [];
140
+ let pets = [];
141
+ let nextId = 1;
142
+ let base = "";
143
+ let spec;
144
+ const handle = async (req, res) => {
145
+ const u = new URL(req.url ?? "/", base);
146
+ const body = await readBody(req);
147
+ const method = (req.method ?? "GET").toUpperCase();
148
+ requests.push({
149
+ method,
150
+ path: u.pathname,
151
+ query: u.search.replace(/^\?/, ""),
152
+ headers: { ...req.headers },
153
+ body,
154
+ });
155
+ if (u.pathname === "/openapi.json") {
156
+ if (opts.specRedirect) {
157
+ res.writeHead(302, { location: `${base}/elsewhere.json` });
158
+ res.end();
159
+ return;
160
+ }
161
+ json(res, 200, spec);
162
+ return;
163
+ }
164
+ if (!u.pathname.startsWith("/v1/")) {
165
+ json(res, 404, { error: "not_found" });
166
+ return;
167
+ }
168
+ const got = keyIn === "header" ? req.headers["x-api-key"] : (u.searchParams.get("api_key") ?? undefined);
169
+ const echo = opts.echoKey ? { receivedKey: got ?? null } : {};
170
+ if (opts.apiKey !== undefined && got !== opts.apiKey) {
171
+ json(res, 401, { error: "unauthorized", ...echo });
172
+ return;
173
+ }
174
+ const path = u.pathname.slice(3);
175
+ const m = /^\/pets\/([^/]+)(\/photo)?$/.exec(path);
176
+ if (path === "/pets" && method === "GET") {
177
+ const tags = u.searchParams.getAll("tags");
178
+ let out = tags.length > 0 ? pets.filter((p) => p.tag && tags.includes(p.tag)) : pets;
179
+ const limit = u.searchParams.get("limit");
180
+ if (limit !== null)
181
+ out = out.slice(0, Number(limit));
182
+ json(res, 200, out);
183
+ return;
184
+ }
185
+ if (path === "/pets" && method === "POST") {
186
+ const b = JSON.parse(body);
187
+ if (typeof b.name !== "string") {
188
+ json(res, 400, { error: "name is required" });
189
+ return;
190
+ }
191
+ const pet = {
192
+ id: nextId++,
193
+ name: b.name,
194
+ ...(typeof b.tag === "string" && { tag: b.tag }),
195
+ };
196
+ pets.push(pet);
197
+ json(res, 201, pet);
198
+ return;
199
+ }
200
+ if (m) {
201
+ const id = Number(decodeURIComponent(m[1]));
202
+ const pet = pets.find((p) => p.id === id);
203
+ if (!pet) {
204
+ json(res, 404, { error: `pet ${m[1]} not found`, ...echo });
205
+ return;
206
+ }
207
+ if (m[2] && method === "GET") {
208
+ res.writeHead(200, { "content-type": "image/png" });
209
+ res.end(PNG);
210
+ return;
211
+ }
212
+ if (method === "GET") {
213
+ json(res, 200, pet);
214
+ return;
215
+ }
216
+ if (method === "PATCH") {
217
+ const b = JSON.parse(body || "{}");
218
+ if (typeof b.name === "string")
219
+ pet.name = b.name;
220
+ if (typeof b.tag === "string")
221
+ pet.tag = b.tag;
222
+ json(res, 200, pet);
223
+ return;
224
+ }
225
+ if (method === "DELETE") {
226
+ pets = pets.filter((p) => p.id !== id);
227
+ res.writeHead(204);
228
+ res.end();
229
+ return;
230
+ }
231
+ }
232
+ if (path === "/echo") {
233
+ json(res, 200, { query: u.search.replace(/^\?/, ""), trace: req.headers["x-trace-id"] });
234
+ return;
235
+ }
236
+ if (path === "/notes" && method === "POST") {
237
+ res.writeHead(201, { "content-type": "text/plain; charset=utf-8" });
238
+ res.end(`stored: ${body}`);
239
+ return;
240
+ }
241
+ if (path === "/boom") {
242
+ json(res, 500, { error: "boom", ...echo });
243
+ return;
244
+ }
245
+ if (path === "/moved") {
246
+ res.writeHead(302, { location: "https://example.invalid/elsewhere" });
247
+ res.end();
248
+ return;
249
+ }
250
+ json(res, 404, { error: "not_found" });
251
+ };
252
+ const http = createServer((req, res) => {
253
+ handle(req, res).catch((e) => {
254
+ if (!res.headersSent)
255
+ json(res, 500, { error: "server_error", detail: String(e) });
256
+ else
257
+ res.end();
258
+ });
259
+ });
260
+ await new Promise((resolve) => http.listen(0, "127.0.0.1", resolve));
261
+ const { port } = http.address();
262
+ base = `http://127.0.0.1:${port}`;
263
+ spec = opts.spec ? opts.spec(base) : defaultFakeOpenApiSpec(base, keyIn);
264
+ return {
265
+ url: base,
266
+ specUrl: `${base}/openapi.json`,
267
+ spec: () => spec,
268
+ setSpec: (s) => {
269
+ spec = s;
270
+ },
271
+ requests,
272
+ pets: () => pets.map((p) => ({ ...p })),
273
+ stop: () => new Promise((resolve, reject) => {
274
+ http.closeAllConnections();
275
+ http.close((err) => (err ? reject(err) : resolve()));
276
+ }),
277
+ };
278
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * 可编程的假 Slack(Slack 渠道插件测试用),扮演两块能力:
3
+ *
4
+ * - **Web API**:`POST /api/<method>`(表单或 JSON 请求体,令牌取 `Authorization: Bearer`),实现插件用到的
5
+ * `auth.test`、`chat.postMessage`、`chat.update`、`users.info`、`apps.connections.open`。机器人令牌 / 应用令牌
6
+ * 与构造时给的不符 → `invalid_auth`;`chat.postMessage` 文本超过 40000 字符、`chat.update` 超过 4000 字符 →
7
+ * `msg_too_long`(与 Slack 文档一致);{@link FakeSlackServer.failNext} 可让某个方法返回平台错误或 HTTP 状态码
8
+ * (429 带 `Retry-After`)。WebClient 用 `slackApiUrl: server.apiUrl` 指过来。
9
+ * - **Socket Mode**:`apps.connections.open` 返回 `ws://…/link?ticket=<n>`;连上后先发 `hello`,测试用
10
+ * {@link FakeSlackServer.push}(或 `pushEvent` / `pushInteractive`)推信封,记下客户端回的确认(`acks`)。
11
+ * Envelopes are routed per app (`appToken`): a push goes to the most recently opened socket of that
12
+ * app, and is queued while the app has no open socket (delivered, in push order, when one opens).
13
+ * With `redeliverUnacked`, an envelope delivered to a socket that closes before acking it is
14
+ * retried on the app's next socket with `retry_attempt` / `retry_reason`, like Slack does.
15
+ * - **Several apps**: {@link FakeSlackOptions.apps} adds Slack apps (one per channel connection) to the
16
+ * same fake; each has its own app / bot token and bot user, and `auth.test` answers per bot token.
17
+ *
18
+ * - **Sign in with Slack(OpenID Connect)**:`GET /openid/connect/authorize`(测试在授权地址后加 `&as=<user id>`
19
+ * 表示以该用户登录,302 回 `redirect_uri?code=…&state=…`;`client_id` 不符或用户不存在 → `error=access_denied`)、
20
+ * `openid.connect.token`(`client_id` / `client_secret` / `code` / `redirect_uri`,授权码单次有效,成功返回
21
+ * access token 与 RS256 签名的 `id_token`,`iss` = `https://slack.com`)、`openid.connect.userInfo`(Bearer
22
+ * access token)、`GET /openid/connect/keys`(JWKS)。{@link FakeSlackServer.tamperNextIdToken} 篡改下一个
23
+ * ID token 的声明或改用陌生密钥签名。
24
+ * - **通讯录**:`users.list`(按 `limit` 分页,游标 `response_metadata.next_cursor`;总在最前面带上 Slackbot
25
+ * `USLACKBOT`,与 Slack 一样它的 `is_bot` 为 false)。
26
+ *
27
+ * 记录:`messages`(发出的消息)、`updates`(对已发消息的更新)、`calls`(每次 Web API 调用)。
28
+ */
29
+ export interface FakeSlackUser {
30
+ id: string;
31
+ teamId: string;
32
+ name: string;
33
+ realName?: string;
34
+ displayName?: string;
35
+ image72?: string;
36
+ isBot?: boolean;
37
+ /** 资料邮箱(`users.list` 的 `profile.email`、OIDC 的 `email`)。 */
38
+ email?: string;
39
+ /** OIDC 的 `email_verified`(默认 false)。 */
40
+ emailVerified?: boolean;
41
+ /** 已停用(`users.list` 的 `deleted`)。 */
42
+ deleted?: boolean;
43
+ /** 多频道访客(`is_restricted`)。 */
44
+ isRestricted?: boolean;
45
+ /** 单频道访客(`is_ultra_restricted`)。 */
46
+ isUltraRestricted?: boolean;
47
+ }
48
+ /** 机器人发出(或更新)的一条消息。 */
49
+ export interface FakeSlackMessage {
50
+ channel: string;
51
+ ts: string;
52
+ text: string;
53
+ blocks?: unknown[];
54
+ threadTs?: string;
55
+ at: number;
56
+ }
57
+ /** 一次 Web API 调用(请求体已解析;`blocks` 等 JSON 字段已解码)。 */
58
+ export interface FakeSlackCall {
59
+ method: string;
60
+ token: string;
61
+ body: Record<string, unknown>;
62
+ }
63
+ /** 客户端对信封的确认。 */
64
+ export interface FakeSlackAck {
65
+ envelopeId: string;
66
+ payload: unknown;
67
+ at: number;
68
+ }
69
+ /**
70
+ * 让接下来的调用失败:`error` 为平台错误(HTTP 200 + `ok: false`),`status` 为 HTTP 状态码;`body` 给了就以
71
+ * HTTP 200 原样返回它(模拟成功但字段缺失的响应)。
72
+ */
73
+ export interface FakeSlackFailure {
74
+ error?: string;
75
+ body?: Record<string, unknown>;
76
+ status?: number;
77
+ /** 429 时的 `Retry-After`(秒)。 */
78
+ retryAfter?: number;
79
+ }
80
+ /** One more Slack app on the same fake (another channel connection with its own tokens). */
81
+ export interface FakeSlackApp {
82
+ appToken: string;
83
+ botToken: string;
84
+ /** Defaults to the server's `teamId`. */
85
+ teamId?: string;
86
+ /** Defaults to the server's `botUserId`. */
87
+ botUserId?: string;
88
+ /** Defaults to the server's `botId`. */
89
+ botId?: string;
90
+ }
91
+ /** Which app an envelope is for (default: the server's own `appToken`). */
92
+ export interface FakeSlackPushTarget {
93
+ appToken?: string;
94
+ }
95
+ export interface FakeSlackOptions {
96
+ teamId?: string;
97
+ botUserId?: string;
98
+ botId?: string;
99
+ botName?: string;
100
+ botToken?: string;
101
+ appToken?: string;
102
+ /** Sign in with Slack 的 OAuth 客户端。 */
103
+ clientId?: string;
104
+ clientSecret?: string;
105
+ /** Extra Slack apps served alongside the default one (whose tokens are `appToken` / `botToken`). */
106
+ apps?: FakeSlackApp[];
107
+ /**
108
+ * Redeliver unacked envelopes (after Slack Socket Mode: an envelope not acked is retried). The fake
109
+ * redelivers on socket close only: when the socket an envelope went to closes before the client
110
+ * acked it, the envelope goes again to the app's next open socket, with `retry_attempt` (1, 2, …)
111
+ * and `retry_reason: "timeout"`. Real Slack also retries on an ack timeout while the socket stays
112
+ * open; the fake does not. Default off, so tests that leave an envelope unacked on purpose don't
113
+ * see it again in a later connection.
114
+ */
115
+ redeliverUnacked?: boolean;
116
+ }
117
+ export interface FakeSlackServer {
118
+ /** HTTP 根地址。 */
119
+ url: string;
120
+ /** WebClient 的 `slackApiUrl`(`<url>/api/`)。 */
121
+ apiUrl: string;
122
+ teamId: string;
123
+ botUserId: string;
124
+ botId: string;
125
+ botToken: string;
126
+ appToken: string;
127
+ clientId: string;
128
+ clientSecret: string;
129
+ messages: FakeSlackMessage[];
130
+ updates: FakeSlackMessage[];
131
+ calls: FakeSlackCall[];
132
+ acks: FakeSlackAck[];
133
+ users: Map<string, FakeSlackUser>;
134
+ /** `apps.connections.open` 成功的次数(given an `appToken`: for that app only)。 */
135
+ connectionsOpened(appToken?: string): number;
136
+ /** 当前打开的 WebSocket 数(given an `appToken`: of that app only)。 */
137
+ openSockets(appToken?: string): number;
138
+ /** 同时打开的 WebSocket 数峰值(given an `appToken`: of that app only)。 */
139
+ maxConcurrentSockets(appToken?: string): number;
140
+ /**
141
+ * 当前打开的长连接各自的对端(客户端)TCP 端口,按打开顺序(given an `appToken`: of that app only)。
142
+ * 双副本测试据此(经操作系统查端口属于哪个进程)找出持有长连接的副本——真实插件不带 `x-pid` 之类的标记。
143
+ */
144
+ socketPeerPorts(appToken?: string): number[];
145
+ /**
146
+ * Envelopes pushed but not acked yet: queued (no open socket) or delivered and awaiting the ack
147
+ * (given an `appToken`: of that app only). Without `redeliverUnacked`, an envelope whose socket
148
+ * closed unacked is dropped from this count.
149
+ */
150
+ pendingEnvelopes(appToken?: string): number;
151
+ failNext(method: string, failure: FakeSlackFailure, count?: number): void;
152
+ /** 之后该方法的每次调用先等 `ms` 毫秒再响应(0 取消)。 */
153
+ delay(method: string, ms: number): void;
154
+ /** 接下来 `count` 条 Socket Mode 连接在发出 hello 后立即被关闭(不发 disconnect 消息)。 */
155
+ dropNextConnections(count: number): void;
156
+ /**
157
+ * 推一个原始信封给该 app 最近打开的一条连接,返回 envelope_id;the app has no open socket → queued
158
+ * and delivered when one opens. Throws for an unknown `appToken`.
159
+ */
160
+ push(envelope: {
161
+ type: string;
162
+ payload: unknown;
163
+ envelope_id?: string;
164
+ }, target?: FakeSlackPushTarget): string;
165
+ /**
166
+ * 推一个 Events API 事件(`events_api` 信封,外层 `event_callback` 带 team_id / event_id;team_id
167
+ * defaults to the target app's team)。
168
+ */
169
+ pushEvent(event: Record<string, unknown>, opts?: {
170
+ eventId?: string;
171
+ teamId?: string;
172
+ } & FakeSlackPushTarget): string;
173
+ /** 推一个交互负载(`interactive` 信封,如 block_actions)。 */
174
+ pushInteractive(payload: Record<string, unknown>, target?: FakeSlackPushTarget): string;
175
+ /** 等到该信封被确认(超时抛错)。 */
176
+ waitForAck(envelopeId: string, timeoutMs?: number): Promise<FakeSlackAck>;
177
+ /** 是否已确认该信封。 */
178
+ acked(envelopeId: string): boolean;
179
+ /** 发 `disconnect` 并关闭所有连接(模拟 Slack 回收连接)。 */
180
+ disconnectAll(reason?: string): void;
181
+ /**
182
+ * 篡改下一个签发的 ID token:`claims` 覆盖(或新增)声明,值为 undefined 即去掉该声明;`foreignKey` 时用
183
+ * 不在 JWKS 里的密钥签名;`kid` 改写头部的 key id。
184
+ */
185
+ tamperNextIdToken(claims: Record<string, unknown>, opts?: {
186
+ foreignKey?: boolean;
187
+ kid?: string;
188
+ }): void;
189
+ /** 消息最终显示的文本(有更新取最后一次)。 */
190
+ finalText(m: FakeSlackMessage): string;
191
+ /** 消息最终的 blocks(有更新取最后一次)。 */
192
+ finalBlocks(m: FakeSlackMessage): unknown[] | undefined;
193
+ stop(): Promise<void>;
194
+ }
195
+ /** 启动假 Slack(监听 127.0.0.1 随机端口)。 */
196
+ export declare function startFakeSlack(opts?: FakeSlackOptions): Promise<FakeSlackServer>;
197
+ /** 私聊消息事件(`message.im`;默认频道 `D<user>`)。 */
198
+ export declare function slackDmEvent(user: string, text: string, opts?: {
199
+ ts?: string;
200
+ channel?: string;
201
+ team?: string;
202
+ extra?: Record<string, unknown>;
203
+ }): Record<string, unknown>;
204
+ /** 频道里 @ 机器人的事件(`app_mention`;`threadTs` 给了表示在话题里)。 */
205
+ export declare function slackMentionEvent(user: string, botUserId: string, text: string, opts?: {
206
+ ts?: string;
207
+ channel?: string;
208
+ threadTs?: string;
209
+ team?: string;
210
+ }): Record<string, unknown>;
211
+ /**
212
+ * 用户点了某条消息上的按钮(`block_actions`)。`message` 为假 Slack 记下的那条消息,`actionId` / `value`
213
+ * 取自其 blocks 里的按钮;`stateValues` 为 `state.values`(多选提交时带上勾选项)。
214
+ */
215
+ export declare function slackBlockAction(user: {
216
+ id: string;
217
+ teamId: string;
218
+ name?: string;
219
+ }, message: FakeSlackMessage, action: {
220
+ actionId: string;
221
+ value?: string;
222
+ blockId?: string;
223
+ }, opts?: {
224
+ actionTs?: string;
225
+ stateValues?: Record<string, unknown>;
226
+ triggerId?: string;
227
+ }): Record<string, unknown>;
228
+ /** 在消息的 blocks 里找按钮(按 action_id 前缀或谓词),返回 `{ actionId, value, blockId }`。 */
229
+ export declare function findSlackButton(blocks: unknown[] | undefined, match: (el: {
230
+ action_id: string;
231
+ value?: string;
232
+ url?: string;
233
+ text?: {
234
+ text?: string;
235
+ };
236
+ }) => boolean): {
237
+ actionId: string;
238
+ value?: string;
239
+ blockId?: string;
240
+ url?: string;
241
+ } | undefined;
242
+ /**
243
+ * An Events API `event_callback` body: what Slack posts to a webhook Request URL, and the payload
244
+ * of a Socket Mode `events_api` envelope (the same object, so both inbound modes see one shape).
245
+ */
246
+ export declare function slackEventCallback(event: Record<string, unknown>, opts?: {
247
+ teamId?: string;
248
+ eventId?: string;
249
+ }): Record<string, unknown>;
250
+ /**
251
+ * Signs a Slack webhook request the way Slack does
252
+ * (https://docs.slack.dev/authentication/verifying-requests-from-slack/): returns the
253
+ * `x-slack-request-timestamp` and `x-slack-signature` headers, where the signature is
254
+ * `v0=` + hex HMAC-SHA256(signingSecret, `v0:<ts>:<raw body>`). `ts` is in seconds.
255
+ */
256
+ export declare function signSlackWebhook(body: string | Uint8Array, signingSecret: string, ts: number): Record<string, string>;
257
+ /** The form body Slack posts for an interactive payload (`payload=<url-encoded JSON>`). */
258
+ export declare const slackInteractiveForm: (payload: Record<string, unknown>) => string;