@yunzai-ng/core 0.1.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.
- package/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:运行期子系统装配的端到端测试 —— 阶段三的验收用例
|
|
3
|
+
* 依赖方向:测试文件,依赖 kernel/app、kernel/runtime、testing/mock-adapter
|
|
4
|
+
* 生命周期:每个用例一套临时主目录 + 一个应用实例,afterEach 停机并删目录
|
|
5
|
+
* 注意事项:这组用例走的是**完整真实链路**:真实的 `createApp()`、真实地从磁盘 import
|
|
6
|
+
* 一个插件、真实地经 `ctx.registerAdapter()` 注册适配器、真实地建立账号连接、
|
|
7
|
+
* 事件真实地自 `host.submit()` 进入管线。其间任何一处装配错误
|
|
8
|
+
* (前缀策略未注入路由、prompts 只提供给工厂而未提供给分发器、账号上线时机
|
|
9
|
+
* 挂错事件)都会在此处显现 —— 这正是 kernel/runtime.ts 存在的理由,
|
|
10
|
+
* 其十余条装配若以单测逐个 mock,反而测不出"装配至错误的对象"。
|
|
11
|
+
*
|
|
12
|
+
* 夹具插件从 `globalThis` 取 Mock 适配器:临时目录中没有 node_modules,
|
|
13
|
+
* 其无法 import 到 `@yunzai-ng/core`。与 plugin/host.test.ts 的做法一致。
|
|
14
|
+
*
|
|
15
|
+
* 断言一律等待 `mock.waitForSend()`,不应写 `await mock.driver.receive*()` ——
|
|
16
|
+
* 投递是单向的,见 testing/mock-adapter.ts 文件头第 2 条。
|
|
17
|
+
*/
|
|
18
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
19
|
+
import { tmpdir } from "node:os"
|
|
20
|
+
import { join } from "node:path"
|
|
21
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
22
|
+
import type { AdapterProvider, CommandDoneInfo, MessageSentInfo } from "@yunzai-ng/types"
|
|
23
|
+
import { createMockAdapter, type MockAdapter, type MockAdapterOptions } from "../testing/mock-adapter.js"
|
|
24
|
+
import { createApp, type App } from "./app.js"
|
|
25
|
+
|
|
26
|
+
/** 夹具插件名,也是它的目录名 */
|
|
27
|
+
const PLUGIN = "mock-adapter"
|
|
28
|
+
|
|
29
|
+
/** 夹具与测试之间的共享通道 */
|
|
30
|
+
interface TestGlobals {
|
|
31
|
+
/** 注入给夹具的 Mock 适配器提供方 */
|
|
32
|
+
__yzngMock?: AdapterProvider
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 取共享全局 */
|
|
36
|
+
const shared = (): TestGlobals => globalThis as unknown as TestGlobals
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 夹具插件源码
|
|
40
|
+
*
|
|
41
|
+
* 注册适配器 + 五条命令 + 一个改写型中间件,覆盖 `installRuntime` 里几乎每根线。
|
|
42
|
+
* 用字符串拼接而不是模板字符串:这段本身就活在模板字符串里,再嵌一层 `${}`
|
|
43
|
+
* 只会让人分不清哪个 `$` 是给谁的。
|
|
44
|
+
*/
|
|
45
|
+
const PLUGIN_SOURCE = `const brand = Symbol.for("yunzai-ng.plugin")
|
|
46
|
+
export default {
|
|
47
|
+
name: "${PLUGIN}",
|
|
48
|
+
version: "0.0.1",
|
|
49
|
+
description: "测试用:注册内存 Mock 适配器并挂几条命令",
|
|
50
|
+
setup(ctx) {
|
|
51
|
+
ctx.registerAdapter(globalThis.__yzngMock)
|
|
52
|
+
|
|
53
|
+
ctx.command("#ping").action(e => e.reply("pong"))
|
|
54
|
+
ctx.command("#谁", { desc: "回显发送者" }).action(e => e.reply("你是 " + e.sender.uid))
|
|
55
|
+
ctx.command("#重启", { master: true }).action(e => e.reply("好的主人"))
|
|
56
|
+
// 故意抛错:验证 command/done 在失败路径同样触发(统计要算成功率)
|
|
57
|
+
ctx.command("#炸").action(() => { throw new Error("故意炸的") })
|
|
58
|
+
ctx.command("#改名").action(async e => {
|
|
59
|
+
const next = await e.prompt({ tip: "要改成什么?" })
|
|
60
|
+
await e.reply(next ? "已改为 " + next.text : "没等到")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// 改写型中间件:验证改写 message 后 refresh() 的结果确实被命令路由看见
|
|
64
|
+
ctx.middleware(async (e, next) => {
|
|
65
|
+
if (e.kind === "message" && e.text === "喵ping") {
|
|
66
|
+
e.message = [{ type: "text", text: "#ping" }]
|
|
67
|
+
e.refresh()
|
|
68
|
+
}
|
|
69
|
+
await next()
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
[brand]: true
|
|
73
|
+
}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
/** 当前用例的应用实例 */
|
|
77
|
+
let current: App | undefined
|
|
78
|
+
/** 当前用例的临时主目录 */
|
|
79
|
+
let root: string | undefined
|
|
80
|
+
|
|
81
|
+
/** 一套跑起来的应用与它的 Mock 适配器 */
|
|
82
|
+
interface Fixture {
|
|
83
|
+
/** 应用实例,已 `start()` */
|
|
84
|
+
app: App
|
|
85
|
+
/** Mock 适配器 */
|
|
86
|
+
mock: MockAdapter
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 建主目录、写夹具插件、起应用
|
|
91
|
+
* @param extra 追加到 `config/yunzai.yaml` 的 YAML 片段
|
|
92
|
+
* @param mockOpts Mock 适配器选项
|
|
93
|
+
* @returns 应用与 Mock 适配器(应用已启动,但还没有账号)
|
|
94
|
+
*/
|
|
95
|
+
async function boot(extra = "", mockOpts: MockAdapterOptions = {}): Promise<Fixture> {
|
|
96
|
+
const home = await mkdtemp(join(tmpdir(), "yzng-runtime-"))
|
|
97
|
+
root = home
|
|
98
|
+
await mkdir(join(home, "config"), { recursive: true })
|
|
99
|
+
await writeFile(join(home, "config", "yunzai.yaml"), `store:\n driver: memory\n sqlite: false\n${extra}`, "utf8")
|
|
100
|
+
await mkdir(join(home, "plugins", PLUGIN), { recursive: true })
|
|
101
|
+
await writeFile(join(home, "plugins", PLUGIN, "index.js"), PLUGIN_SOURCE, "utf8")
|
|
102
|
+
|
|
103
|
+
const mock = createMockAdapter(mockOpts)
|
|
104
|
+
shared().__yzngMock = mock.provider
|
|
105
|
+
|
|
106
|
+
const app = await createApp({
|
|
107
|
+
home,
|
|
108
|
+
version: "9.9.9-test",
|
|
109
|
+
console: false,
|
|
110
|
+
watchConfig: false,
|
|
111
|
+
stopTimeout: 300
|
|
112
|
+
})
|
|
113
|
+
current = app
|
|
114
|
+
|
|
115
|
+
const report = await app.start()
|
|
116
|
+
expect(report.failed).toEqual([])
|
|
117
|
+
expect(report.loaded).toEqual([PLUGIN])
|
|
118
|
+
return { app, mock }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 给应用加一个 Mock 账号并等它上线
|
|
123
|
+
* @param app 应用
|
|
124
|
+
* @param selfId 账号 id
|
|
125
|
+
* @returns 账号记录 id
|
|
126
|
+
*/
|
|
127
|
+
async function login(app: App, selfId = "10000"): Promise<string> {
|
|
128
|
+
const record = await app.runtime.accounts.create("mock", { selfId })
|
|
129
|
+
expect(app.runtime.accounts.get(record.id)?.status).toBe("online")
|
|
130
|
+
return record.id
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
afterEach(async () => {
|
|
134
|
+
await current?.stop()
|
|
135
|
+
current = undefined
|
|
136
|
+
delete shared().__yzngMock
|
|
137
|
+
if (root) await rm(root, { recursive: true, force: true })
|
|
138
|
+
root = undefined
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe("installRuntime 装配", () => {
|
|
142
|
+
it("把五个接缝和四个视图都换成真实现", async () => {
|
|
143
|
+
const { app } = await boot()
|
|
144
|
+
|
|
145
|
+
// 接缝:换的是同一个对象而不是又包一层,插件拿到的就是这几个注册表本身
|
|
146
|
+
expect(app.hooks.commands).toBe(app.runtime.router)
|
|
147
|
+
expect(app.hooks.middlewares).toBe(app.runtime.middlewares)
|
|
148
|
+
expect(app.hooks.tasks).toBe(app.runtime.scheduler)
|
|
149
|
+
expect(app.hooks.render).toBe(app.runtime.renderers)
|
|
150
|
+
expect(app.hooks.adapters).toBe(app.runtime.adapters)
|
|
151
|
+
|
|
152
|
+
expect(app.subsystems.adapters).toBe(app.runtime.adapters)
|
|
153
|
+
expect(app.subsystems.bots).toBe(app.runtime.bots)
|
|
154
|
+
expect(app.subsystems.accounts).toBe(app.runtime.accounts)
|
|
155
|
+
// registries 是新造的闭包,只能验行为:夹具插件注册了 5 条命令
|
|
156
|
+
expect(app.subsystems.registries.commands()).toHaveLength(5)
|
|
157
|
+
|
|
158
|
+
// AppView 是活的:装配期填的子系统立刻能从插件那一侧看到
|
|
159
|
+
expect(app.view.adapters.list().map(a => a.id)).toEqual(["mock"])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("账号上线后 pickBot 能拿到它,下线后拿不到", async () => {
|
|
163
|
+
const { app } = await boot()
|
|
164
|
+
expect(app.hooks.bots.pick()).toBeUndefined()
|
|
165
|
+
|
|
166
|
+
const id = await login(app)
|
|
167
|
+
|
|
168
|
+
const bot = app.hooks.bots.pick()
|
|
169
|
+
expect(bot?.selfId).toBe("10000")
|
|
170
|
+
// 记录 id 与平台 selfId 两种写法都要能查到
|
|
171
|
+
expect(app.hooks.bots.pick(id)?.selfId).toBe("10000")
|
|
172
|
+
expect(app.hooks.bots.pick("10000")).toBe(bot)
|
|
173
|
+
expect(app.view.bots.size).toBe(1)
|
|
174
|
+
|
|
175
|
+
await app.runtime.accounts.disconnect(id, "用例主动断开")
|
|
176
|
+
expect(app.view.bots.size).toBe(0)
|
|
177
|
+
expect(app.hooks.bots.pick()).toBeUndefined()
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe("消息链路", () => {
|
|
182
|
+
it("私聊收到 #ping 回 pong", async () => {
|
|
183
|
+
const { app, mock } = await boot()
|
|
184
|
+
await login(app)
|
|
185
|
+
|
|
186
|
+
mock.driver.receivePrivate("#ping")
|
|
187
|
+
|
|
188
|
+
const [reply] = await mock.waitForSend()
|
|
189
|
+
expect(reply?.text).toBe("pong")
|
|
190
|
+
expect(reply?.target).toEqual({ scene: "private", uid: "20000" })
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("群里收到 #ping 回到群里", async () => {
|
|
194
|
+
const { app, mock } = await boot()
|
|
195
|
+
await login(app)
|
|
196
|
+
|
|
197
|
+
mock.driver.receiveGroup("#ping", { gid: "800001", uid: "20001" })
|
|
198
|
+
|
|
199
|
+
const [reply] = await mock.waitForSend()
|
|
200
|
+
expect(reply?.text).toBe("pong")
|
|
201
|
+
expect(reply?.target).toEqual({ scene: "group", gid: "800001" })
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it("事件字段被内核补全:发送者、selfId 都对得上", async () => {
|
|
205
|
+
const { app, mock } = await boot()
|
|
206
|
+
await login(app, "10086")
|
|
207
|
+
|
|
208
|
+
mock.driver.receivePrivate("#谁", { uid: "20002" })
|
|
209
|
+
|
|
210
|
+
const [reply] = await mock.waitForSend()
|
|
211
|
+
expect(reply?.text).toBe("你是 20002")
|
|
212
|
+
expect(reply?.selfId).toBe("10086")
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("没有前缀的消息不进命令路由", async () => {
|
|
216
|
+
const { app, mock } = await boot()
|
|
217
|
+
await login(app)
|
|
218
|
+
|
|
219
|
+
// 先发一条不带前缀的,再发一条带前缀的;等到后者的回复时前者早已走完
|
|
220
|
+
mock.driver.receivePrivate("ping")
|
|
221
|
+
mock.driver.receivePrivate("#ping")
|
|
222
|
+
|
|
223
|
+
await mock.waitForSend()
|
|
224
|
+
expect(mock.texts).toEqual(["pong"])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("中间件改写消息后,路由按改写后的内容匹配", async () => {
|
|
228
|
+
const { app, mock } = await boot()
|
|
229
|
+
await login(app)
|
|
230
|
+
|
|
231
|
+
mock.driver.receivePrivate("喵ping")
|
|
232
|
+
|
|
233
|
+
const [reply] = await mock.waitForSend()
|
|
234
|
+
expect(reply?.text).toBe("pong")
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it("仅主人命令对非主人静默,对主人生效", async () => {
|
|
238
|
+
const { app, mock } = await boot("bot:\n masterQQ:\n - '30000'\n")
|
|
239
|
+
await login(app)
|
|
240
|
+
expect(app.policy.isMaster("30000")).toBe(true)
|
|
241
|
+
|
|
242
|
+
mock.driver.receivePrivate("#重启", { uid: "20000" })
|
|
243
|
+
mock.driver.receivePrivate("#重启", { uid: "30000" })
|
|
244
|
+
|
|
245
|
+
await mock.waitForSend()
|
|
246
|
+
// 非主人那条既不回复也不报错 —— 有权限提示的话这里会是两条
|
|
247
|
+
expect(mock.texts).toEqual(["好的主人"])
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it("e.prompt() 能等到同会话的下一条消息", async () => {
|
|
251
|
+
const { app, mock } = await boot()
|
|
252
|
+
await login(app)
|
|
253
|
+
|
|
254
|
+
mock.driver.receivePrivate("#改名")
|
|
255
|
+
const [tip] = await mock.waitForSend()
|
|
256
|
+
expect(tip?.text).toBe("要改成什么?")
|
|
257
|
+
|
|
258
|
+
// 必须等待至等待者确实登记完成:waitForSend 在 driver.sendMessage 中即 resolve,
|
|
259
|
+
// 此时插件尚未从 `await e.reply(tip)` 返回,更未进入 prompts.wait()。
|
|
260
|
+
// 若在此时投入答案,prompts.offer() 找不到等待者,该消息将进入命令路由并被丢弃。
|
|
261
|
+
await vi.waitFor(() => expect(app.runtime.prompts.pending).toBe(1))
|
|
262
|
+
|
|
263
|
+
// 这条会被 prompt 截住,不进命令路由(否则会当成未知命令丢掉)
|
|
264
|
+
mock.driver.receivePrivate("小明")
|
|
265
|
+
const sends = await mock.waitForSend(2)
|
|
266
|
+
expect(sends[1]?.text).toBe("已改为 小明")
|
|
267
|
+
expect(app.runtime.prompts.pending).toBe(0)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it("机器人自己发的消息被忽略", async () => {
|
|
271
|
+
const { app, mock } = await boot()
|
|
272
|
+
await login(app)
|
|
273
|
+
|
|
274
|
+
mock.driver.receivePrivate("#ping", { uid: "10000" })
|
|
275
|
+
mock.driver.receivePrivate("#ping", { uid: "20000" })
|
|
276
|
+
|
|
277
|
+
await mock.waitForSend()
|
|
278
|
+
expect(mock.texts).toEqual(["pong"])
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe("适配器生命周期", () => {
|
|
283
|
+
it("适配器插件卸载后账号自动下线", async () => {
|
|
284
|
+
const { app, mock } = await boot()
|
|
285
|
+
const id = await login(app)
|
|
286
|
+
expect(app.view.bots.size).toBe(1)
|
|
287
|
+
|
|
288
|
+
await app.plugins.unload(PLUGIN)
|
|
289
|
+
|
|
290
|
+
// 应当同步完整摘除:插件卸载后 pickBot 立即不能再给出该账号
|
|
291
|
+
expect(app.view.bots.size).toBe(0)
|
|
292
|
+
expect(app.hooks.bots.pick()).toBeUndefined()
|
|
293
|
+
expect(app.view.adapters.list()).toEqual([])
|
|
294
|
+
|
|
295
|
+
const state = app.runtime.accounts.get(id)
|
|
296
|
+
expect(state?.status).toBe("offline")
|
|
297
|
+
expect(state?.error).toContain("已卸载")
|
|
298
|
+
await vi.waitFor(() => expect(mock.driver.disconnects).toBe(1))
|
|
299
|
+
|
|
300
|
+
// 账号记录本身还在:适配器插件装回来就该自动连上,不该要求用户重新加账号
|
|
301
|
+
expect(app.runtime.accounts.list()).toHaveLength(1)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("适配器报告下线时走完整下线流程", async () => {
|
|
305
|
+
const { app, mock } = await boot()
|
|
306
|
+
const id = await login(app)
|
|
307
|
+
|
|
308
|
+
mock.driver.goOffline("对端关闭了连接")
|
|
309
|
+
|
|
310
|
+
// 等待状态而不是等待 `bots.size`:size 仅统计在线门面,而 goOffline() 已同步将
|
|
311
|
+
// driver.online 置为 false,首次轮询即成立 —— 其时异步的下线流程尚未执行。
|
|
312
|
+
await vi.waitFor(() => expect(app.runtime.accounts.get(id)?.status).toBe("offline"))
|
|
313
|
+
// total 统计的是注册表中真实存在的条目,方可验证门面确实已被摘除
|
|
314
|
+
expect(app.runtime.bots.total).toBe(0)
|
|
315
|
+
expect(app.hooks.bots.pick()).toBeUndefined()
|
|
316
|
+
expect(app.runtime.accounts.get(id)?.error).toBe("对端关闭了连接")
|
|
317
|
+
expect(mock.driver.disconnects).toBe(1)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("连接失败的账号不进 Bot 注册表,状态是 error", async () => {
|
|
321
|
+
const { app, mock } = await boot("", { failConnect: "Mock 就是不让连" })
|
|
322
|
+
|
|
323
|
+
const record = await app.runtime.accounts.create("mock", { selfId: "10000" })
|
|
324
|
+
|
|
325
|
+
const state = app.runtime.accounts.get(record.id)
|
|
326
|
+
expect(state?.status).toBe("error")
|
|
327
|
+
expect(state?.error).toContain("Mock 就是不让连")
|
|
328
|
+
expect(app.view.bots.size).toBe(0)
|
|
329
|
+
// 驱动建出来了、连过一次、也被收拾掉了 —— 不能留一个没人引用还在收消息的幽灵
|
|
330
|
+
expect(mock.driver.connects).toBe(1)
|
|
331
|
+
expect(mock.driver.disconnects).toBe(1)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it("账号被 disable 后事件不再进管线", async () => {
|
|
335
|
+
const { app, mock } = await boot()
|
|
336
|
+
const id = await login(app)
|
|
337
|
+
|
|
338
|
+
await app.runtime.accounts.setEnabled(id, false)
|
|
339
|
+
expect(app.view.bots.size).toBe(0)
|
|
340
|
+
|
|
341
|
+
// 宿主的 signal 已经 abort,submit() 会直接丢掉
|
|
342
|
+
mock.driver.receivePrivate("#ping")
|
|
343
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
344
|
+
expect(mock.sent).toEqual([])
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("断开一个仍处于 connect() 等待中的账号:先 abort 再等待,不会被阻塞", async () => {
|
|
348
|
+
const { app } = await boot()
|
|
349
|
+
|
|
350
|
+
// 被动接入的适配器(反向 WS、HTTP 上报)的 connect() 即为此形态:持续等待
|
|
351
|
+
// 对端连入。disconnect() 若先 await 该 promise 再 abort,便会一直等到
|
|
352
|
+
// 该等待自身超时为止 —— 表现为 Ctrl-C 之后进程半分钟无法退出
|
|
353
|
+
const mock = createMockAdapter({ id: "blocking" })
|
|
354
|
+
const provider = {
|
|
355
|
+
...mock.provider,
|
|
356
|
+
id: "blocking",
|
|
357
|
+
async createBot(account: unknown, host: Parameters<AdapterProvider["createBot"]>[1]) {
|
|
358
|
+
const base = await mock.provider.createBot(account as never, host)
|
|
359
|
+
// 用原型委托而不是展开:`online` 是 getter,展开会把它压成一个定值
|
|
360
|
+
return Object.create(base, {
|
|
361
|
+
connect: {
|
|
362
|
+
value: () =>
|
|
363
|
+
new Promise<void>((_resolve, reject) => {
|
|
364
|
+
host.signal.addEventListener("abort", () => reject(new Error("账号已停用")), { once: true })
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
}) as Awaited<ReturnType<AdapterProvider["createBot"]>>
|
|
368
|
+
}
|
|
369
|
+
} as unknown as AdapterProvider
|
|
370
|
+
|
|
371
|
+
app.runtime.adapters.register(provider, PLUGIN)
|
|
372
|
+
// 建为禁用态,否则 create() 自身即会阻塞于 connect() 上
|
|
373
|
+
const record = await app.runtime.accounts.create("blocking", { selfId: "10000" }, undefined, false)
|
|
374
|
+
|
|
375
|
+
void app.runtime.accounts.connect(record.id)
|
|
376
|
+
await vi.waitFor(() => expect(app.runtime.accounts.get(record.id)?.status).toBe("connecting"))
|
|
377
|
+
|
|
378
|
+
const started = Date.now()
|
|
379
|
+
await app.runtime.accounts.disconnect(record.id, "用例主动断开")
|
|
380
|
+
expect(Date.now() - started).toBeLessThan(1000)
|
|
381
|
+
expect(app.runtime.accounts.get(record.id)?.status).toBe("disabled")
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it("被 abort 打断的连接不算失败,不会排一个没人清的重连", async () => {
|
|
385
|
+
const { app } = await boot()
|
|
386
|
+
|
|
387
|
+
const mock = createMockAdapter({ id: "blocking2" })
|
|
388
|
+
const provider = {
|
|
389
|
+
...mock.provider,
|
|
390
|
+
id: "blocking2",
|
|
391
|
+
async createBot(account: unknown, host: Parameters<AdapterProvider["createBot"]>[1]) {
|
|
392
|
+
const base = await mock.provider.createBot(account as never, host)
|
|
393
|
+
return Object.create(base, {
|
|
394
|
+
connect: {
|
|
395
|
+
value: () =>
|
|
396
|
+
new Promise<void>((_resolve, reject) => {
|
|
397
|
+
host.signal.addEventListener("abort", () => reject(new Error("账号已停用")), { once: true })
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}) as Awaited<ReturnType<AdapterProvider["createBot"]>>
|
|
401
|
+
}
|
|
402
|
+
} as unknown as AdapterProvider
|
|
403
|
+
|
|
404
|
+
app.runtime.adapters.register(provider, PLUGIN)
|
|
405
|
+
const record = await app.runtime.accounts.create("blocking2", { selfId: "10001" }, undefined, false)
|
|
406
|
+
|
|
407
|
+
void app.runtime.accounts.connect(record.id)
|
|
408
|
+
await vi.waitFor(() => expect(app.runtime.accounts.get(record.id)?.status).toBe("connecting"))
|
|
409
|
+
await app.runtime.accounts.disconnect(record.id, "用例主动断开")
|
|
410
|
+
|
|
411
|
+
// 状态是"已禁用"而不是"error":主动断开不是连接失败。若这里变成 error,
|
|
412
|
+
// 说明重连也被排上了 —— 而 disconnect 的清定时器早就跑过,那个定时器没人清
|
|
413
|
+
const state = app.runtime.accounts.get(record.id)
|
|
414
|
+
expect(state?.status).toBe("disabled")
|
|
415
|
+
expect(state?.error).toBeUndefined()
|
|
416
|
+
|
|
417
|
+
// 再等一会儿,确认没有定时器把它重新连起来
|
|
418
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
419
|
+
expect(app.runtime.accounts.get(record.id)?.status).toBe("disabled")
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it("停机时断开全部账号", async () => {
|
|
423
|
+
const { app, mock } = await boot()
|
|
424
|
+
await login(app)
|
|
425
|
+
|
|
426
|
+
await app.stop()
|
|
427
|
+
|
|
428
|
+
expect(app.view.bots.size).toBe(0)
|
|
429
|
+
expect(mock.driver.disconnects).toBe(1)
|
|
430
|
+
expect(mock.driver.online).toBe(false)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
/*
|
|
435
|
+
* 埋点与统计事件
|
|
436
|
+
*
|
|
437
|
+
* 走完整真实链路而非单测分发器:这几条断言的价值全在「默认配置下、真实日志级别下
|
|
438
|
+
* 看得见」——而那恰恰是被 `isLevelEnabled("debug")` 包住时测不出来的那一类缺陷。
|
|
439
|
+
* 只 mock 一个 logger 去断言「调用过 info」并不能说明默认级别下它会被记下。
|
|
440
|
+
*/
|
|
441
|
+
describe("埋点与统计事件", () => {
|
|
442
|
+
it("收到的消息记明协议与账号,而非只记场景与发送者", async () => {
|
|
443
|
+
const { app, mock } = await boot()
|
|
444
|
+
await login(app, "10086")
|
|
445
|
+
|
|
446
|
+
mock.driver.receivePrivate("#ping", { uid: "20007" })
|
|
447
|
+
await mock.waitForSend()
|
|
448
|
+
|
|
449
|
+
const line = app.loggerHub.tail().find(r => r.msg.includes("20007"))
|
|
450
|
+
expect(line).toBeDefined()
|
|
451
|
+
// 同一进程可同时挂多个平台的多个号,缺了这一段就答不出「这条是哪个号收到的」
|
|
452
|
+
expect(line?.msg).toContain("[mock:10086]")
|
|
453
|
+
expect(line?.msg).toContain("[私聊]")
|
|
454
|
+
expect(line?.level).toBe("info")
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it("命令命中与处理完毕都记在 info 级 —— 默认级别下看得见", async () => {
|
|
458
|
+
const { app, mock } = await boot()
|
|
459
|
+
await login(app)
|
|
460
|
+
|
|
461
|
+
mock.driver.receivePrivate("#ping")
|
|
462
|
+
await mock.waitForSend()
|
|
463
|
+
|
|
464
|
+
// 取 info 及以上,模拟默认配置下使用者实际看到的内容
|
|
465
|
+
await vi.waitFor(() => {
|
|
466
|
+
const msgs = app.loggerHub.tail({ level: "info" }).map(r => r.msg)
|
|
467
|
+
expect(msgs.some(m => m.includes("命令命中 mock-adapter:#ping"))).toBe(true)
|
|
468
|
+
expect(msgs.some(m => /命令 mock-adapter:#ping 处理完毕,耗时 \d+ms/.test(m))).toBe(true)
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it("命令跑完发 command/done,带插件、命令、耗时与成功标记", async () => {
|
|
473
|
+
const { app, mock } = await boot()
|
|
474
|
+
await login(app)
|
|
475
|
+
|
|
476
|
+
const seen: CommandDoneInfo[] = []
|
|
477
|
+
app.events.on("command/done", (_e, info) => void seen.push(info))
|
|
478
|
+
|
|
479
|
+
mock.driver.receivePrivate("#ping")
|
|
480
|
+
await mock.waitForSend()
|
|
481
|
+
|
|
482
|
+
await vi.waitFor(() => expect(seen).toHaveLength(1))
|
|
483
|
+
expect(seen[0]).toMatchObject({ plugin: PLUGIN, command: "#ping", ok: true })
|
|
484
|
+
expect(seen[0]?.cost).toBeGreaterThanOrEqual(0)
|
|
485
|
+
expect(seen[0]?.error).toBeUndefined()
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it("命令抛错同样发 command/done,且 ok 为假 —— 否则统计出的成功率恒为 100%", async () => {
|
|
489
|
+
const { app, mock } = await boot()
|
|
490
|
+
await login(app)
|
|
491
|
+
|
|
492
|
+
const seen: CommandDoneInfo[] = []
|
|
493
|
+
app.events.on("command/done", (_e, info) => void seen.push(info))
|
|
494
|
+
|
|
495
|
+
mock.driver.receivePrivate("#炸")
|
|
496
|
+
|
|
497
|
+
await vi.waitFor(() => expect(seen).toHaveLength(1))
|
|
498
|
+
expect(seen[0]).toMatchObject({ plugin: PLUGIN, command: "#炸", ok: false })
|
|
499
|
+
expect(seen[0]?.error).toContain("故意炸的")
|
|
500
|
+
// 失败也要有耗时:统计「平均耗时」时若失败那次缺项,均值会偏向成功的快路径
|
|
501
|
+
expect(seen[0]?.cost).toBeGreaterThanOrEqual(0)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it("消息发出后发 message/sent,带目标、段数与图文构成", async () => {
|
|
505
|
+
const { app, mock } = await boot()
|
|
506
|
+
const accountId = await login(app, "10010")
|
|
507
|
+
|
|
508
|
+
const seen: MessageSentInfo[] = []
|
|
509
|
+
app.events.on("message/sent", info => void seen.push(info))
|
|
510
|
+
|
|
511
|
+
mock.driver.receiveGroup("#ping", { gid: "800900", uid: "20008" })
|
|
512
|
+
await mock.waitForSend()
|
|
513
|
+
|
|
514
|
+
await vi.waitFor(() => expect(seen).toHaveLength(1))
|
|
515
|
+
expect(seen[0]).toMatchObject({
|
|
516
|
+
accountId,
|
|
517
|
+
adapterId: "mock",
|
|
518
|
+
platform: "mock",
|
|
519
|
+
target: { scene: "group", gid: "800900" },
|
|
520
|
+
segments: 1,
|
|
521
|
+
ok: true
|
|
522
|
+
})
|
|
523
|
+
// 逐类计数而非只记总段数:「图片发不出去」与「文字发不出去」是两类故障
|
|
524
|
+
expect(seen[0]?.kinds).toEqual({ text: 1 })
|
|
525
|
+
expect(seen[0]?.cost).toBeGreaterThanOrEqual(0)
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it("发出的消息也记一行 info —— 只记进来不记出去,一次问答在日志里只剩上半句", async () => {
|
|
529
|
+
const { app, mock } = await boot()
|
|
530
|
+
await login(app, "10011")
|
|
531
|
+
|
|
532
|
+
mock.driver.receiveGroup("#ping", { gid: "800901" })
|
|
533
|
+
await mock.waitForSend()
|
|
534
|
+
|
|
535
|
+
await vi.waitFor(() => {
|
|
536
|
+
const line = app.loggerHub.tail({ level: "info" }).find(r => r.msg.includes("已发出"))
|
|
537
|
+
expect(line).toBeDefined()
|
|
538
|
+
expect(line?.msg).toContain("[mock:10011]")
|
|
539
|
+
expect(line?.msg).toContain("text×1")
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
})
|