@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,802 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:面板 API 端点的行为测试
|
|
3
|
+
* 依赖方向:测试文件,依赖 server/*、config/*
|
|
4
|
+
* 生命周期:每个用例一个临时配置目录、一个未监听的服务器、一组替身子系统
|
|
5
|
+
* 注意事项:`ApiDeps` 中有六个字段的类型是**带私有字段的类**(AccountManager、
|
|
6
|
+
* LoginManager、RenderRegistry、EventDispatcher、PluginHost、LoggerHub),
|
|
7
|
+
* 结构化替身无法直接赋值,只能 `as unknown as`。此项取舍并非规避实现成本:这些类各自
|
|
8
|
+
* 需要一整套内核依赖方可构造,为验证一个"只读模式返回 403"而将它们全部构造出来,
|
|
9
|
+
* 等同于把 api.ts 的测试变为 kernel/app.ts 的集成测试,一处失败即导致全体失败。
|
|
10
|
+
*
|
|
11
|
+
* 服务器为真实实现(经完整钩子链),配置亦为真实实现(需验证 SchemaError → 400)。
|
|
12
|
+
*/
|
|
13
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
14
|
+
import { tmpdir } from "node:os"
|
|
15
|
+
import { join, sep } from "node:path"
|
|
16
|
+
import type { Socket } from "node:net"
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
18
|
+
import type {
|
|
19
|
+
AccountRecord,
|
|
20
|
+
AccountState,
|
|
21
|
+
AdapterRegistryView,
|
|
22
|
+
BotRegistryView,
|
|
23
|
+
LogLevel,
|
|
24
|
+
Logger,
|
|
25
|
+
ResourceUsage,
|
|
26
|
+
RuntimePaths
|
|
27
|
+
} from "@yunzai-ng/types"
|
|
28
|
+
import { detectPlatform } from "../platform/detect.js"
|
|
29
|
+
import type { AccountManager } from "../adapter/accounts.js"
|
|
30
|
+
import type { LoginManager, LoginSnapshot } from "../adapter/login.js"
|
|
31
|
+
import type { EventDispatcher } from "../pipeline/dispatch.js"
|
|
32
|
+
import type { PluginHost } from "../plugin/host.js"
|
|
33
|
+
import type { PluginMarket } from "../plugin/market.js"
|
|
34
|
+
import type { RenderRegistry } from "../render/registry.js"
|
|
35
|
+
import type { LoggerHub } from "../logger/index.js"
|
|
36
|
+
import type { LogRecord } from "../logger/format.js"
|
|
37
|
+
import { ConfigStore } from "../config/store.js"
|
|
38
|
+
import { defineCoreConfig, type CoreConfigHandle } from "../config/core-config.js"
|
|
39
|
+
import { createManagedServer, type ManagedServer } from "./index.js"
|
|
40
|
+
import { API_SCOPE, registerApi, type ApiDeps } from "./api.js"
|
|
41
|
+
|
|
42
|
+
/** 本机对端,light-my-request 默认也是这个 */
|
|
43
|
+
const LOCAL_IP = "127.0.0.1"
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 拼一个 `injectWS` 的 upgradeContext
|
|
47
|
+
*
|
|
48
|
+
* 伪造的 raw request 上没有 `socket`,而鉴权要看 TCP 对端地址,所以得自己补一个。
|
|
49
|
+
* @param headers 额外的握手请求头
|
|
50
|
+
* @returns upgradeContext
|
|
51
|
+
*/
|
|
52
|
+
function upgradeCtx(headers: Record<string, string> = {}): { socket: Socket; headers: Record<string, string> } {
|
|
53
|
+
return { socket: { remoteAddress: LOCAL_IP } as unknown as Socket, headers }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 静音日志器
|
|
58
|
+
* @returns 只收集行的日志器
|
|
59
|
+
*/
|
|
60
|
+
function quietLogger(): Logger {
|
|
61
|
+
const push = (): void => undefined
|
|
62
|
+
const logger: Logger = {
|
|
63
|
+
level: "silent" as LogLevel,
|
|
64
|
+
trace: push,
|
|
65
|
+
debug: push,
|
|
66
|
+
info: push,
|
|
67
|
+
warn: push,
|
|
68
|
+
error: push,
|
|
69
|
+
fatal: push,
|
|
70
|
+
mark: push,
|
|
71
|
+
child: () => logger,
|
|
72
|
+
isLevelEnabled: () => false
|
|
73
|
+
}
|
|
74
|
+
return logger
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 造一条账号记录
|
|
79
|
+
* @param id 记录 id
|
|
80
|
+
* @returns 记录
|
|
81
|
+
*/
|
|
82
|
+
function accountRecord(id: string): AccountRecord {
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
adapterId: "napcat",
|
|
86
|
+
enabled: true,
|
|
87
|
+
config: { mode: "ws", url: "ws://127.0.0.1:3001", token: "机密" },
|
|
88
|
+
createdAt: 1,
|
|
89
|
+
updatedAt: 1
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 造一条账号状态
|
|
95
|
+
* @param id 记录 id
|
|
96
|
+
* @param status 状态
|
|
97
|
+
* @returns 状态
|
|
98
|
+
*/
|
|
99
|
+
function accountState(id: string, status: AccountState["status"] = "online"): AccountState {
|
|
100
|
+
return { record: accountRecord(id), status, since: 1, retries: 0 }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 替身子系统的可观测部分,用例据此断言"到底调没调" */
|
|
104
|
+
interface Spies {
|
|
105
|
+
/** 插件宿主 */
|
|
106
|
+
plugins: {
|
|
107
|
+
/** 列表 */
|
|
108
|
+
list: ReturnType<typeof vi.fn>
|
|
109
|
+
/** 单个 */
|
|
110
|
+
get: ReturnType<typeof vi.fn>
|
|
111
|
+
/** 重载 */
|
|
112
|
+
reload: ReturnType<typeof vi.fn>
|
|
113
|
+
/** 卸载 */
|
|
114
|
+
unload: ReturnType<typeof vi.fn>
|
|
115
|
+
/** 全量加载 */
|
|
116
|
+
loadAll: ReturnType<typeof vi.fn>
|
|
117
|
+
}
|
|
118
|
+
/** 插件市场 */
|
|
119
|
+
market: {
|
|
120
|
+
/** 索引快照 */
|
|
121
|
+
list: ReturnType<typeof vi.fn>
|
|
122
|
+
/** 安装 */
|
|
123
|
+
install: ReturnType<typeof vi.fn>
|
|
124
|
+
/** 更新 */
|
|
125
|
+
update: ReturnType<typeof vi.fn>
|
|
126
|
+
/** 卸载 */
|
|
127
|
+
remove: ReturnType<typeof vi.fn>
|
|
128
|
+
}
|
|
129
|
+
/** 账号管理器 */
|
|
130
|
+
accounts: {
|
|
131
|
+
/** 列表 */
|
|
132
|
+
list: ReturnType<typeof vi.fn>
|
|
133
|
+
/** 单个 */
|
|
134
|
+
get: ReturnType<typeof vi.fn>
|
|
135
|
+
/** 新建 */
|
|
136
|
+
create: ReturnType<typeof vi.fn>
|
|
137
|
+
/** 修改 */
|
|
138
|
+
update: ReturnType<typeof vi.fn>
|
|
139
|
+
/** 删除 */
|
|
140
|
+
remove: ReturnType<typeof vi.fn>
|
|
141
|
+
/** 连接 */
|
|
142
|
+
connect: ReturnType<typeof vi.fn>
|
|
143
|
+
/** 断开 */
|
|
144
|
+
disconnect: ReturnType<typeof vi.fn>
|
|
145
|
+
/** 重连 */
|
|
146
|
+
reconnect: ReturnType<typeof vi.fn>
|
|
147
|
+
}
|
|
148
|
+
/** 登录会话管理器 */
|
|
149
|
+
logins: {
|
|
150
|
+
/** 列表 */
|
|
151
|
+
list: ReturnType<typeof vi.fn>
|
|
152
|
+
/** 快照 */
|
|
153
|
+
snapshot: ReturnType<typeof vi.fn>
|
|
154
|
+
/** 开始 */
|
|
155
|
+
start: ReturnType<typeof vi.fn>
|
|
156
|
+
/** 答复 */
|
|
157
|
+
answer: ReturnType<typeof vi.fn>
|
|
158
|
+
/** 取消 */
|
|
159
|
+
cancel: ReturnType<typeof vi.fn>
|
|
160
|
+
/** 进行中数量 */
|
|
161
|
+
running: number
|
|
162
|
+
}
|
|
163
|
+
/** 日志枢纽 */
|
|
164
|
+
loggerHub: {
|
|
165
|
+
/** 回看 */
|
|
166
|
+
tail: ReturnType<typeof vi.fn>
|
|
167
|
+
/** 订阅者集合 */
|
|
168
|
+
listeners: Set<(rec: LogRecord) => void>
|
|
169
|
+
/** 文件路径 */
|
|
170
|
+
file: string
|
|
171
|
+
/** 级别 */
|
|
172
|
+
level: LogLevel
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 造一条登录会话快照
|
|
178
|
+
* @param id 会话 id
|
|
179
|
+
* @returns 快照
|
|
180
|
+
*/
|
|
181
|
+
function loginSnapshot(id: string): LoginSnapshot {
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
adapterId: "napcat",
|
|
185
|
+
mode: "qrcode",
|
|
186
|
+
status: "running",
|
|
187
|
+
steps: [{ type: "info", text: "请扫码" }],
|
|
188
|
+
pending: undefined,
|
|
189
|
+
error: undefined,
|
|
190
|
+
accountId: undefined,
|
|
191
|
+
startedAt: 1,
|
|
192
|
+
updatedAt: 1
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
describe("面板 API", () => {
|
|
197
|
+
let dir: string
|
|
198
|
+
let config: CoreConfigHandle
|
|
199
|
+
let store: ConfigStore
|
|
200
|
+
let server: ManagedServer
|
|
201
|
+
let spies: Spies
|
|
202
|
+
let deps: ApiDeps
|
|
203
|
+
let off: () => void
|
|
204
|
+
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
dir = await mkdtemp(join(tmpdir(), "yzng-api-"))
|
|
207
|
+
const logger = quietLogger()
|
|
208
|
+
store = new ConfigStore({ dir, logger, watch: false })
|
|
209
|
+
config = await defineCoreConfig(store)
|
|
210
|
+
server = await createManagedServer({ config, logger })
|
|
211
|
+
|
|
212
|
+
spies = {
|
|
213
|
+
plugins: {
|
|
214
|
+
list: vi.fn(() => [
|
|
215
|
+
{
|
|
216
|
+
name: "demo",
|
|
217
|
+
version: "1.0.0",
|
|
218
|
+
root: "/p/demo",
|
|
219
|
+
status: "loaded",
|
|
220
|
+
loadCost: 12,
|
|
221
|
+
commands: 3,
|
|
222
|
+
tasks: 1,
|
|
223
|
+
middlewares: 0,
|
|
224
|
+
builtin: false,
|
|
225
|
+
configured: false
|
|
226
|
+
}
|
|
227
|
+
]),
|
|
228
|
+
get: vi.fn((name: string) => (name === "demo" ? { name: "demo", status: "loaded" } : undefined)),
|
|
229
|
+
reload: vi.fn(async (name: string) => name === "demo"),
|
|
230
|
+
unload: vi.fn(async (name: string) => name === "demo"),
|
|
231
|
+
loadAll: vi.fn(async () => ({ loaded: ["fresh"], failed: [], skipped: [], cost: 1 }))
|
|
232
|
+
},
|
|
233
|
+
market: {
|
|
234
|
+
list: vi.fn(async () => ({
|
|
235
|
+
fetchedAt: 1,
|
|
236
|
+
cached: false,
|
|
237
|
+
sources: [{ url: "https://example.com/i.json", ok: true, count: 1 }],
|
|
238
|
+
plugins: [
|
|
239
|
+
{
|
|
240
|
+
name: "fresh",
|
|
241
|
+
title: "示例插件",
|
|
242
|
+
description: "用于测试",
|
|
243
|
+
tags: [],
|
|
244
|
+
official: true,
|
|
245
|
+
install: { type: "tarball", url: "https://example.com/f.tar.gz" },
|
|
246
|
+
source: "https://example.com/i.json",
|
|
247
|
+
installed: false
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
})),
|
|
251
|
+
install: vi.fn(async (name: string) => ({
|
|
252
|
+
name,
|
|
253
|
+
dir: join(dir, "plugins", name),
|
|
254
|
+
via: "tarball",
|
|
255
|
+
version: "1.0.0",
|
|
256
|
+
needsDependencies: false
|
|
257
|
+
})),
|
|
258
|
+
update: vi.fn(async (name: string) => ({
|
|
259
|
+
name,
|
|
260
|
+
dir: join(dir, "plugins", name),
|
|
261
|
+
via: "tarball",
|
|
262
|
+
version: "2.0.0",
|
|
263
|
+
needsDependencies: false
|
|
264
|
+
})),
|
|
265
|
+
remove: vi.fn(async (name: string) => name === "demo")
|
|
266
|
+
},
|
|
267
|
+
accounts: {
|
|
268
|
+
list: vi.fn(() => [accountState("a1")]),
|
|
269
|
+
get: vi.fn((id: string) => (id === "a1" ? accountState("a1") : undefined)),
|
|
270
|
+
create: vi.fn(async () => accountRecord("a2")),
|
|
271
|
+
update: vi.fn(async () => accountRecord("a1")),
|
|
272
|
+
remove: vi.fn(async () => true),
|
|
273
|
+
connect: vi.fn(async () => undefined),
|
|
274
|
+
disconnect: vi.fn(async () => undefined),
|
|
275
|
+
reconnect: vi.fn(async () => undefined)
|
|
276
|
+
},
|
|
277
|
+
logins: {
|
|
278
|
+
list: vi.fn(() => [loginSnapshot("l1")]),
|
|
279
|
+
snapshot: vi.fn((id: string) => (id === "l1" ? loginSnapshot("l1") : undefined)),
|
|
280
|
+
start: vi.fn(() => loginSnapshot("l1")),
|
|
281
|
+
answer: vi.fn(() => true),
|
|
282
|
+
cancel: vi.fn(() => true),
|
|
283
|
+
running: 1
|
|
284
|
+
},
|
|
285
|
+
loggerHub: {
|
|
286
|
+
tail: vi.fn(() => [{ level: "info" as LogLevel, time: 1, msg: "第一行" }]),
|
|
287
|
+
listeners: new Set<(rec: LogRecord) => void>(),
|
|
288
|
+
file: join(dir, "yunzai.log"),
|
|
289
|
+
level: "info" as LogLevel
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const adapters: AdapterRegistryView = {
|
|
294
|
+
list: () => [
|
|
295
|
+
{ id: "napcat", name: "NapCat (OneBot v11)", platform: "qq", accountSchema: { type: "object" }, loginModes: [] }
|
|
296
|
+
],
|
|
297
|
+
get: () => undefined
|
|
298
|
+
}
|
|
299
|
+
const bots: BotRegistryView = { get: () => undefined, bySelfId: () => undefined, online: () => [], size: 1 }
|
|
300
|
+
|
|
301
|
+
deps = {
|
|
302
|
+
version: "9.9.9",
|
|
303
|
+
paths: { home: dir, config: dir, data: dir, logs: dir, temp: dir, plugins: dir } as unknown as RuntimePaths,
|
|
304
|
+
platform: detectPlatform(),
|
|
305
|
+
logger: quietLogger(),
|
|
306
|
+
config,
|
|
307
|
+
configStore: store,
|
|
308
|
+
loggerHub: {
|
|
309
|
+
...spies.loggerHub,
|
|
310
|
+
subscribe: (fn: (rec: LogRecord) => void) => {
|
|
311
|
+
spies.loggerHub.listeners.add(fn)
|
|
312
|
+
return () => void spies.loggerHub.listeners.delete(fn)
|
|
313
|
+
}
|
|
314
|
+
} as unknown as LoggerHub,
|
|
315
|
+
plugins: spies.plugins as unknown as PluginHost,
|
|
316
|
+
market: spies.market as unknown as PluginMarket,
|
|
317
|
+
registries: {
|
|
318
|
+
commands: () => [{ name: "#帮助", patterns: ["#帮助"], plugin: "demo", master: false, admin: false, hidden: false, disabled: false }],
|
|
319
|
+
tasks: () => [{ name: "推送", schedule: "0 0 8 * * *", plugin: "demo", running: false, skipped: 0 }],
|
|
320
|
+
middlewares: () => [{ plugin: "demo", priority: 50, kinds: ["message"] as const }] as never
|
|
321
|
+
},
|
|
322
|
+
adapters,
|
|
323
|
+
accounts: spies.accounts as unknown as AccountManager,
|
|
324
|
+
logins: spies.logins as unknown as LoginManager,
|
|
325
|
+
bots,
|
|
326
|
+
renderers: { size: 1, list: () => [{ id: "puppeteer", name: "Puppeteer", owner: "renderer-puppeteer", preferred: true, available: true, lastError: undefined, succeeded: 4, failed: 0 }] } as unknown as RenderRegistry,
|
|
327
|
+
dispatcher: { handled: 42, queued: 0 } as unknown as EventDispatcher,
|
|
328
|
+
server,
|
|
329
|
+
status: () => "running",
|
|
330
|
+
startedAt: () => Date.now() - 1000,
|
|
331
|
+
usage: () => ({ rss: 1, heapUsed: 1, heapTotal: 1, external: 1, cpu: 0 }) as ResourceUsage,
|
|
332
|
+
system: async () => ({
|
|
333
|
+
disks: [{ mount: "C:\\", total: 1000, free: 400, used: 600 }],
|
|
334
|
+
gpus: [{ name: "GeForce RTX 4090", load: 0.31, memoryUsed: 2048, memoryTotal: 24576 }]
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
off = registerApi(server, deps)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
afterEach(async () => {
|
|
342
|
+
off()
|
|
343
|
+
await server.close()
|
|
344
|
+
await rm(dir, { recursive: true, force: true })
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 发一个面板 API 请求
|
|
349
|
+
* @param method HTTP 方法
|
|
350
|
+
* @param path 相对 `API_SCOPE` 的路径
|
|
351
|
+
* @param payload 请求体
|
|
352
|
+
* @returns 注入响应
|
|
353
|
+
*/
|
|
354
|
+
const call = async (
|
|
355
|
+
method: string,
|
|
356
|
+
path: string,
|
|
357
|
+
payload?: unknown
|
|
358
|
+
): Promise<{ statusCode: number; json: () => Record<string, unknown>; body: string }> => {
|
|
359
|
+
const res = await server.raw.inject({
|
|
360
|
+
method: method as "GET",
|
|
361
|
+
url: `${API_SCOPE}/${path}`,
|
|
362
|
+
...(payload === undefined ? {} : { payload: payload as object, headers: { "content-type": "application/json" } })
|
|
363
|
+
})
|
|
364
|
+
return { statusCode: res.statusCode, json: () => res.json() as Record<string, unknown>, body: res.body }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
describe("概览", () => {
|
|
368
|
+
it("汇总版本、状态与各子系统计数", async () => {
|
|
369
|
+
const res = await call("GET", "overview")
|
|
370
|
+
expect(res.statusCode).toBe(200)
|
|
371
|
+
const body = res.json()
|
|
372
|
+
expect(body.version).toBe("9.9.9")
|
|
373
|
+
expect(body.status).toBe("running")
|
|
374
|
+
expect(body.counts).toEqual({
|
|
375
|
+
plugins: 1,
|
|
376
|
+
pluginsFailed: 0,
|
|
377
|
+
commands: 1,
|
|
378
|
+
tasks: 1,
|
|
379
|
+
adapters: 1,
|
|
380
|
+
accounts: 1,
|
|
381
|
+
online: 1,
|
|
382
|
+
renderers: 1,
|
|
383
|
+
logins: 1
|
|
384
|
+
})
|
|
385
|
+
expect(body.pipeline).toEqual({ handled: 42, queued: 0 })
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe("系统信息", () => {
|
|
390
|
+
it("回出磁盘与显卡", async () => {
|
|
391
|
+
const res = await call("GET", "system")
|
|
392
|
+
expect(res.statusCode).toBe(200)
|
|
393
|
+
const body = res.json() as unknown as {
|
|
394
|
+
disks: Array<{ mount: string; total: number }>
|
|
395
|
+
gpus?: Array<{ name: string }>
|
|
396
|
+
}
|
|
397
|
+
expect(body.disks[0]?.mount).toBe("C:\\")
|
|
398
|
+
expect(body.gpus?.[0]?.name).toBe("GeForce RTX 4090")
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
/*
|
|
402
|
+
* CPU 与内存不进这个端点:它们已在 `GET overview` 里。同一事实供两份,
|
|
403
|
+
* 两处的采样时刻还不一样,面板上就会出现「CPU 卡片与 CPU 环不是一个数」。
|
|
404
|
+
* 这一条断言的是「日后有人往这里加 cpu 字段会红」。
|
|
405
|
+
*/
|
|
406
|
+
it("不含 CPU 与内存 —— 那两项的唯一来源是概览", async () => {
|
|
407
|
+
const body = (await call("GET", "system")).json()
|
|
408
|
+
expect(body.cpu).toBeUndefined()
|
|
409
|
+
expect(body.usage).toBeUndefined()
|
|
410
|
+
expect(body.memory).toBeUndefined()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it("只读模式下照常可用 —— 它报容量与型号,不含任何路径", async () => {
|
|
414
|
+
await config.patch({ server: { readonly: true } }, "api")
|
|
415
|
+
expect((await call("GET", "system")).statusCode).toBe(200)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
describe("中间件清单", () => {
|
|
420
|
+
it("回出所属插件、优先级与适用大类", async () => {
|
|
421
|
+
const res = await call("GET", "middlewares")
|
|
422
|
+
expect(res.statusCode).toBe(200)
|
|
423
|
+
const body = res.json() as unknown as Array<{ plugin: string; priority: number; kinds: string[] }>
|
|
424
|
+
expect(body[0]).toEqual({ plugin: "demo", priority: 50, kinds: ["message"] })
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
/*
|
|
428
|
+
* 插件卡片一直显示着中间件的**条数**,却看不到是哪几条 —— 排查「消息被谁拦下了」
|
|
429
|
+
* 时条数没有用。这一条断言的是「清单与计数出自同一处」。
|
|
430
|
+
*/
|
|
431
|
+
it("与概览里的计数同源 —— 两处不该各数一遍", async () => {
|
|
432
|
+
const list = (await call("GET", "middlewares")).json() as unknown as unknown[]
|
|
433
|
+
expect(list).toHaveLength(1)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
describe("配置", () => {
|
|
438
|
+
it("列出已声明的配置,并给出表单描述", async () => {
|
|
439
|
+
const list = (await call("GET", "config")).json() as unknown as Array<{ name: string; schema: unknown }>
|
|
440
|
+
expect(list.some(c => c.name === "yunzai")).toBe(true)
|
|
441
|
+
|
|
442
|
+
const one = await call("GET", "config/yunzai")
|
|
443
|
+
expect(one.statusCode).toBe(200)
|
|
444
|
+
expect(one.json().name).toBe("yunzai")
|
|
445
|
+
expect(one.json().schema).toBeDefined()
|
|
446
|
+
expect((one.json().value as { server: { port: number } }).server.port).toBe(2536)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it("未声明的配置回 404", async () => {
|
|
450
|
+
const res = await call("GET", "config/不存在")
|
|
451
|
+
expect(res.statusCode).toBe(404)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it("PATCH 深合并,只改传进来的字段", async () => {
|
|
455
|
+
const res = await call("PATCH", "config/yunzai", { server: { port: 3999 } })
|
|
456
|
+
expect(res.statusCode).toBe(200)
|
|
457
|
+
expect(config.get().server.port).toBe(3999)
|
|
458
|
+
// 同层的其他字段不该被抹掉
|
|
459
|
+
expect(config.get().server.host).toBe("127.0.0.1")
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it("校验失败回 400,并带上逐字段的 issues", async () => {
|
|
463
|
+
const res = await call("PATCH", "config/yunzai", { server: { port: 999999 } })
|
|
464
|
+
expect(res.statusCode).toBe(400)
|
|
465
|
+
const issues = res.json().issues as Array<{ path: string }>
|
|
466
|
+
expect(issues.some(i => i.path === "server.port")).toBe(true)
|
|
467
|
+
// 配置必须原样不动
|
|
468
|
+
expect(config.get().server.port).toBe(2536)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it("reset 恢复默认值", async () => {
|
|
472
|
+
await call("PATCH", "config/yunzai", { server: { port: 3999 } })
|
|
473
|
+
const res = await call("POST", "config/yunzai/reset")
|
|
474
|
+
expect(res.statusCode).toBe(200)
|
|
475
|
+
expect(config.get().server.port).toBe(2536)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it("请求体不是对象时回 400", async () => {
|
|
479
|
+
const res = await call("PATCH", "config/yunzai", [1, 2, 3])
|
|
480
|
+
expect(res.statusCode).toBe(400)
|
|
481
|
+
expect(String(res.json().error)).toContain("JSON 对象")
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe("只读模式", () => {
|
|
486
|
+
beforeEach(async () => {
|
|
487
|
+
await config.patch({ server: { readonly: true } }, "api")
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it("读接口照常,写接口一律 403", async () => {
|
|
491
|
+
expect((await call("GET", "config/yunzai")).statusCode).toBe(200)
|
|
492
|
+
expect((await call("GET", "accounts")).statusCode).toBe(200)
|
|
493
|
+
|
|
494
|
+
for (const [method, path] of [
|
|
495
|
+
["PATCH", "config/yunzai"],
|
|
496
|
+
["POST", "config/yunzai/reset"],
|
|
497
|
+
["POST", "plugins/demo/reload"],
|
|
498
|
+
["POST", "accounts"],
|
|
499
|
+
["DELETE", "accounts/a1"],
|
|
500
|
+
["POST", "logins"],
|
|
501
|
+
["POST", "market/install"],
|
|
502
|
+
["POST", "market/demo/update"],
|
|
503
|
+
["DELETE", "market/demo"]
|
|
504
|
+
] as const) {
|
|
505
|
+
const res = await call(method, path, {})
|
|
506
|
+
expect(res.statusCode, `${method} ${path}`).toBe(403)
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it("只读模式不影响插件自己注册的路由", async () => {
|
|
511
|
+
// 适配器的 webhook 不该因为"面板设成只读"而收不到消息,见 api.ts 第 2 条
|
|
512
|
+
server.route("/plugin/napcat", "POST", "hook", () => ({ ok: true }), { auth: false })
|
|
513
|
+
const res = await server.raw.inject({ method: "POST", url: "/plugin/napcat/hook", payload: { a: 1 } })
|
|
514
|
+
expect(res.statusCode).toBe(200)
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it("**目录浏览在只读模式下停用** —— 它只为改配置服务,此时只剩扩权", async () => {
|
|
518
|
+
const res = await call("GET", "fs")
|
|
519
|
+
expect(res.statusCode).toBe(403)
|
|
520
|
+
expect(String(res.json().error)).toContain("目录浏览已停用")
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
describe("目录浏览", () => {
|
|
525
|
+
it("列出指定目录,只回名字与是否目录", async () => {
|
|
526
|
+
const res = await call("GET", `fs?path=${encodeURIComponent(dir)}`)
|
|
527
|
+
expect(res.statusCode).toBe(200)
|
|
528
|
+
const body = res.json() as unknown as {
|
|
529
|
+
path: string
|
|
530
|
+
entries: Array<Record<string, unknown>>
|
|
531
|
+
truncated: boolean
|
|
532
|
+
sep: string
|
|
533
|
+
}
|
|
534
|
+
expect(body.path).toBe(dir)
|
|
535
|
+
expect(body.truncated).toBe(false)
|
|
536
|
+
expect(body.sep).toBe(sep)
|
|
537
|
+
// 临时目录里此刻有 defineCoreConfig 写出的 yunzai.yaml
|
|
538
|
+
const names = body.entries.map(e => e.name)
|
|
539
|
+
expect(names).toContain("yunzai.yaml")
|
|
540
|
+
for (const entry of body.entries) expect(Object.keys(entry).sort()).toEqual(["dir", "name"])
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it("不存在的目录回 404,相对路径回 400", async () => {
|
|
544
|
+
expect((await call("GET", `fs?path=${encodeURIComponent(join(dir, "无此目录"))}`)).statusCode).toBe(404)
|
|
545
|
+
expect((await call("GET", "fs?path=plugins")).statusCode).toBe(400)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it("不带 path 时给出最外层,不报错", async () => {
|
|
549
|
+
const res = await call("GET", "fs")
|
|
550
|
+
expect(res.statusCode).toBe(200)
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it("与其余 /api/* 一样要令牌", async () => {
|
|
554
|
+
await config.patch({ server: { token: "秘密令牌" } }, "api")
|
|
555
|
+
const bare = await server.raw.inject({ method: "GET", url: `${API_SCOPE}/fs` })
|
|
556
|
+
expect(bare.statusCode).toBe(401)
|
|
557
|
+
const withToken = await server.raw.inject({
|
|
558
|
+
method: "GET",
|
|
559
|
+
url: `${API_SCOPE}/fs`,
|
|
560
|
+
headers: { "x-yunzai-token": "秘密令牌" }
|
|
561
|
+
})
|
|
562
|
+
expect(withToken.statusCode).toBe(200)
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
describe("插件", () => {
|
|
567
|
+
it("列出插件、取单个、未知插件 404", async () => {
|
|
568
|
+
const list = (await call("GET", "plugins")).json() as unknown as Array<{ name: string }>
|
|
569
|
+
expect(list[0]?.name).toBe("demo")
|
|
570
|
+
expect((await call("GET", "plugins/demo")).statusCode).toBe(200)
|
|
571
|
+
expect((await call("GET", "plugins/无名")).statusCode).toBe(404)
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("reload 与 unload 转发到宿主,失败时不返回 200", async () => {
|
|
575
|
+
expect((await call("POST", "plugins/demo/reload")).statusCode).toBe(200)
|
|
576
|
+
expect(spies.plugins.reload).toHaveBeenCalledWith("demo")
|
|
577
|
+
|
|
578
|
+
expect((await call("POST", "plugins/无名/reload")).statusCode).toBe(400)
|
|
579
|
+
expect((await call("POST", "plugins/无名/unload")).statusCode).toBe(404)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it("命令、任务、渲染器清单都能读", async () => {
|
|
583
|
+
expect(((await call("GET", "commands")).json() as unknown as unknown[]).length).toBe(1)
|
|
584
|
+
expect(((await call("GET", "tasks")).json() as unknown as unknown[]).length).toBe(1)
|
|
585
|
+
expect(((await call("GET", "renderers")).json() as unknown as unknown[]).length).toBe(1)
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe("账号", () => {
|
|
590
|
+
it("新建成功回 201,并把校验后的记录带回来", async () => {
|
|
591
|
+
const res = await call("POST", "accounts", { adapterId: "napcat", config: { mode: "ws" }, label: "主号" })
|
|
592
|
+
expect(res.statusCode).toBe(201)
|
|
593
|
+
expect(res.json().id).toBe("a2")
|
|
594
|
+
expect(spies.accounts.create).toHaveBeenCalledWith("napcat", { mode: "ws" }, "主号", true)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it("缺 adapterId 回 400,配置校验失败也回 400", async () => {
|
|
598
|
+
expect((await call("POST", "accounts", { config: {} })).statusCode).toBe(400)
|
|
599
|
+
|
|
600
|
+
spies.accounts.create.mockRejectedValueOnce(new Error("字段 url 必填"))
|
|
601
|
+
const res = await call("POST", "accounts", { adapterId: "napcat", config: {} })
|
|
602
|
+
expect(res.statusCode).toBe(400)
|
|
603
|
+
expect(String(res.json().error)).toContain("字段 url 必填")
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it("未知账号一律 404,且不会误调管理器", async () => {
|
|
607
|
+
expect((await call("PATCH", "accounts/nope", {})).statusCode).toBe(404)
|
|
608
|
+
expect((await call("POST", "accounts/nope/connect")).statusCode).toBe(404)
|
|
609
|
+
expect(spies.accounts.update).not.toHaveBeenCalled()
|
|
610
|
+
expect(spies.accounts.connect).not.toHaveBeenCalled()
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it("连接类动作回动作之后的状态,删除回 204", async () => {
|
|
614
|
+
const connected = await call("POST", "accounts/a1/connect")
|
|
615
|
+
expect(connected.statusCode).toBe(200)
|
|
616
|
+
expect((connected.json().record as { id: string }).id).toBe("a1")
|
|
617
|
+
expect(spies.accounts.connect).toHaveBeenCalledWith("a1")
|
|
618
|
+
|
|
619
|
+
await call("POST", "accounts/a1/disconnect")
|
|
620
|
+
expect(spies.accounts.disconnect).toHaveBeenCalledWith("a1", "面板手动断开")
|
|
621
|
+
|
|
622
|
+
const removed = await call("DELETE", "accounts/a1")
|
|
623
|
+
expect(removed.statusCode).toBe(204)
|
|
624
|
+
expect(spies.accounts.remove).toHaveBeenCalledWith("a1")
|
|
625
|
+
})
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
describe("交互式登录", () => {
|
|
629
|
+
it("开始登录立刻回初始快照,不等流程结束", async () => {
|
|
630
|
+
const res = await call("POST", "logins", { adapterId: "napcat", mode: "qrcode" })
|
|
631
|
+
expect(res.statusCode).toBe(201)
|
|
632
|
+
expect(res.json().status).toBe("running")
|
|
633
|
+
expect(spies.logins.start).toHaveBeenCalledWith("napcat", "qrcode", undefined)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it("适配器不支持登录时回 400", async () => {
|
|
637
|
+
spies.logins.start.mockImplementationOnce(() => {
|
|
638
|
+
throw new Error("适配器 napcat 不支持交互式登录")
|
|
639
|
+
})
|
|
640
|
+
const res = await call("POST", "logins", { adapterId: "napcat", mode: "qrcode" })
|
|
641
|
+
expect(res.statusCode).toBe(400)
|
|
642
|
+
expect(String(res.json().error)).toContain("不支持交互式登录")
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it("答复要带整数 seq,答复结果与最新快照一起回", async () => {
|
|
646
|
+
expect((await call("POST", "logins/l1/answer", { value: "1234" })).statusCode).toBe(400)
|
|
647
|
+
|
|
648
|
+
const res = await call("POST", "logins/l1/answer", { seq: 2, value: "1234" })
|
|
649
|
+
expect(res.statusCode).toBe(200)
|
|
650
|
+
expect(res.json().accepted).toBe(true)
|
|
651
|
+
expect((res.json().session as { id: string }).id).toBe("l1")
|
|
652
|
+
expect(spies.logins.answer).toHaveBeenCalledWith("l1", 2, "1234")
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it("取消会话,未知会话读快照回 404", async () => {
|
|
656
|
+
expect((await call("DELETE", "logins/l1")).json().cancelled).toBe(true)
|
|
657
|
+
expect((await call("GET", "logins/无名")).statusCode).toBe(404)
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
describe("日志", () => {
|
|
662
|
+
it("回看时把 limit / level / scope / keyword 传给日志枢纽", async () => {
|
|
663
|
+
const res = await call("GET", "logs?limit=5&level=warn&scope=kernel&keyword=失败")
|
|
664
|
+
expect(res.statusCode).toBe(200)
|
|
665
|
+
expect(spies.loggerHub.tail).toHaveBeenCalledWith({
|
|
666
|
+
limit: 5,
|
|
667
|
+
level: "warn",
|
|
668
|
+
scope: "kernel",
|
|
669
|
+
keyword: "失败"
|
|
670
|
+
})
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it("非法 limit 与非法 level 都退回缺省,而不是报错", async () => {
|
|
674
|
+
await call("GET", "logs?limit=abc&level=喧闹")
|
|
675
|
+
expect(spies.loggerHub.tail).toHaveBeenCalledWith({ limit: 200 })
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it("limit 有上限,防止一次把整个环形缓冲搬进内存", async () => {
|
|
679
|
+
await call("GET", "logs?limit=999999")
|
|
680
|
+
expect(spies.loggerHub.tail).toHaveBeenCalledWith({ limit: 2000 })
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it("WebSocket 推送实时日志,按握手时的条件过滤,断开后摘除订阅", async () => {
|
|
684
|
+
const ws = await server.raw.injectWS(`${API_SCOPE}/logs?level=warn`, upgradeCtx())
|
|
685
|
+
expect(spies.loggerHub.listeners.size).toBe(1)
|
|
686
|
+
|
|
687
|
+
const got = await new Promise<string>(resolve => {
|
|
688
|
+
ws.on("message", (raw: unknown) => {
|
|
689
|
+
resolve(String(raw))
|
|
690
|
+
})
|
|
691
|
+
for (const fn of spies.loggerHub.listeners) {
|
|
692
|
+
// 级别低于 warn 的这条必须被过滤掉,否则下面的断言会读到它
|
|
693
|
+
fn({ level: "debug", time: 1, msg: "不该出现" })
|
|
694
|
+
fn({ level: "error", time: 2, msg: "出事了" })
|
|
695
|
+
}
|
|
696
|
+
})
|
|
697
|
+
expect(JSON.parse(got)).toEqual({ level: "error", time: 2, msg: "出事了" })
|
|
698
|
+
|
|
699
|
+
ws.terminate()
|
|
700
|
+
// 订阅者表是进程级的,连接断开后必须完整摘除,见 api.ts 中的相关注释
|
|
701
|
+
await vi.waitFor(() => {
|
|
702
|
+
expect(spies.loggerHub.listeners.size).toBe(0)
|
|
703
|
+
})
|
|
704
|
+
})
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
describe("插件市场", () => {
|
|
708
|
+
it("列出索引,refresh=1 时强制刷新", async () => {
|
|
709
|
+
const res = await call("GET", "market")
|
|
710
|
+
expect(res.statusCode).toBe(200)
|
|
711
|
+
expect((res.json().plugins as Array<{ name: string }>)[0]?.name).toBe("fresh")
|
|
712
|
+
expect(spies.market.list).toHaveBeenCalledWith(false)
|
|
713
|
+
|
|
714
|
+
await call("GET", "market?refresh=1")
|
|
715
|
+
expect(spies.market.list).toHaveBeenCalledWith(true)
|
|
716
|
+
|
|
717
|
+
await call("POST", "market/refresh")
|
|
718
|
+
expect(spies.market.list).toHaveBeenLastCalledWith(true)
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
it("安装后默认立即加载,并把本次加载的插件名带回来", async () => {
|
|
722
|
+
const res = await call("POST", "market/install", { name: "fresh" })
|
|
723
|
+
expect(res.statusCode).toBe(200)
|
|
724
|
+
expect(spies.market.install).toHaveBeenCalledWith("fresh")
|
|
725
|
+
expect(spies.plugins.loadAll).toHaveBeenCalled()
|
|
726
|
+
expect(res.json().loaded).toEqual(["fresh"])
|
|
727
|
+
expect(res.json().version).toBe("1.0.0")
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
it("load 为 false 时只落盘不加载", async () => {
|
|
731
|
+
const res = await call("POST", "market/install", { name: "fresh", load: false })
|
|
732
|
+
expect(res.statusCode).toBe(200)
|
|
733
|
+
expect(spies.plugins.loadAll).not.toHaveBeenCalled()
|
|
734
|
+
expect(res.json().loaded).toEqual([])
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it("缺少 name 回 400,安装失败也回 400", async () => {
|
|
738
|
+
expect((await call("POST", "market/install", {})).statusCode).toBe(400)
|
|
739
|
+
|
|
740
|
+
spies.market.install.mockRejectedValueOnce(new Error("插件 x 已安装,如需覆盖请先卸载"))
|
|
741
|
+
const res = await call("POST", "market/install", { name: "x" })
|
|
742
|
+
expect(res.statusCode).toBe(400)
|
|
743
|
+
expect(String(res.json().error)).toContain("已安装")
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
it("更新已加载的插件时先卸载再覆盖安装", async () => {
|
|
747
|
+
const res = await call("POST", "market/demo/update", {})
|
|
748
|
+
expect(res.statusCode).toBe(200)
|
|
749
|
+
expect(spies.plugins.unload).toHaveBeenCalledWith("demo")
|
|
750
|
+
expect(spies.market.update).toHaveBeenCalledWith("demo")
|
|
751
|
+
expect(res.json().unloaded).toBe(true)
|
|
752
|
+
expect(res.json().version).toBe("2.0.0")
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
it("卸载先摘内存再删目录,目录不存在回 404", async () => {
|
|
756
|
+
const res = await call("DELETE", "market/demo")
|
|
757
|
+
expect(res.statusCode).toBe(200)
|
|
758
|
+
expect(res.json()).toMatchObject({ name: "demo", unloaded: true, removed: true })
|
|
759
|
+
|
|
760
|
+
// 未加载的插件不该触发 unload,删不到目录时回 404
|
|
761
|
+
spies.plugins.unload.mockClear()
|
|
762
|
+
const missing = await call("DELETE", "market/无名")
|
|
763
|
+
expect(missing.statusCode).toBe(404)
|
|
764
|
+
expect(spies.plugins.unload).not.toHaveBeenCalled()
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
it("未启用市场的部署一律回 501", async () => {
|
|
768
|
+
off()
|
|
769
|
+
const without: ApiDeps = { ...deps }
|
|
770
|
+
delete (without as { market?: unknown }).market
|
|
771
|
+
off = registerApi(server, without)
|
|
772
|
+
|
|
773
|
+
expect((await call("GET", "market")).statusCode).toBe(501)
|
|
774
|
+
expect((await call("POST", "market/install", { name: "fresh" })).statusCode).toBe(501)
|
|
775
|
+
})
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
describe("服务器自省", () => {
|
|
779
|
+
it("列出监听信息、连接数与三张路由表", async () => {
|
|
780
|
+
server.route("/plugin/demo", "GET", "ping", () => "pong")
|
|
781
|
+
const body = (await call("GET", "server")).json()
|
|
782
|
+
expect(body.port).toBe(2536)
|
|
783
|
+
expect(body.readonly).toBe(false)
|
|
784
|
+
expect(body.connections).toBe(0)
|
|
785
|
+
const routes = body.routes as Array<{ pattern: string; scope: string }>
|
|
786
|
+
expect(routes.some(r => r.pattern === "/plugin/demo/ping")).toBe(true)
|
|
787
|
+
expect(routes.some(r => r.scope === API_SCOPE)).toBe(true)
|
|
788
|
+
expect((body.websockets as Array<{ pattern: string }>).some(w => w.pattern === `${API_SCOPE}/logs`)).toBe(true)
|
|
789
|
+
})
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
describe("生命周期", () => {
|
|
793
|
+
it("registerApi 的 Disposer 会完整摘除全部端点", async () => {
|
|
794
|
+
expect((await call("GET", "overview")).statusCode).toBe(200)
|
|
795
|
+
off()
|
|
796
|
+
expect((await call("GET", "overview")).statusCode).toBe(404)
|
|
797
|
+
// afterEach 会再调一次,必须幂等
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
})
|
|
802
|
+
|