@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.
Files changed (357) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +51 -0
  3. package/dist/adapter/accounts.d.ts +174 -0
  4. package/dist/adapter/accounts.d.ts.map +1 -0
  5. package/dist/adapter/accounts.js +653 -0
  6. package/dist/adapter/accounts.js.map +1 -0
  7. package/dist/adapter/bots.d.ts +142 -0
  8. package/dist/adapter/bots.d.ts.map +1 -0
  9. package/dist/adapter/bots.js +454 -0
  10. package/dist/adapter/bots.js.map +1 -0
  11. package/dist/adapter/host.d.ts +114 -0
  12. package/dist/adapter/host.d.ts.map +1 -0
  13. package/dist/adapter/host.js +90 -0
  14. package/dist/adapter/host.js.map +1 -0
  15. package/dist/adapter/login.d.ts +174 -0
  16. package/dist/adapter/login.d.ts.map +1 -0
  17. package/dist/adapter/login.js +345 -0
  18. package/dist/adapter/login.js.map +1 -0
  19. package/dist/adapter/registry.d.ts +102 -0
  20. package/dist/adapter/registry.d.ts.map +1 -0
  21. package/dist/adapter/registry.js +150 -0
  22. package/dist/adapter/registry.js.map +1 -0
  23. package/dist/config/core-config.d.ts +151 -0
  24. package/dist/config/core-config.d.ts.map +1 -0
  25. package/dist/config/core-config.js +338 -0
  26. package/dist/config/core-config.js.map +1 -0
  27. package/dist/config/schema.d.ts +449 -0
  28. package/dist/config/schema.d.ts.map +1 -0
  29. package/dist/config/schema.js +871 -0
  30. package/dist/config/schema.js.map +1 -0
  31. package/dist/config/store.d.ts +184 -0
  32. package/dist/config/store.d.ts.map +1 -0
  33. package/dist/config/store.js +428 -0
  34. package/dist/config/store.js.map +1 -0
  35. package/dist/config/yaml.d.ts +23 -0
  36. package/dist/config/yaml.d.ts.map +1 -0
  37. package/dist/config/yaml.js +108 -0
  38. package/dist/config/yaml.js.map +1 -0
  39. package/dist/http/client.d.ts +82 -0
  40. package/dist/http/client.d.ts.map +1 -0
  41. package/dist/http/client.js +658 -0
  42. package/dist/http/client.js.map +1 -0
  43. package/dist/index.d.ts +77 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +94 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kernel/app.d.ts +244 -0
  48. package/dist/kernel/app.d.ts.map +1 -0
  49. package/dist/kernel/app.js +654 -0
  50. package/dist/kernel/app.js.map +1 -0
  51. package/dist/kernel/kv-sink.d.ts +34 -0
  52. package/dist/kernel/kv-sink.d.ts.map +1 -0
  53. package/dist/kernel/kv-sink.js +25 -0
  54. package/dist/kernel/kv-sink.js.map +1 -0
  55. package/dist/kernel/policy.d.ts +77 -0
  56. package/dist/kernel/policy.d.ts.map +1 -0
  57. package/dist/kernel/policy.js +99 -0
  58. package/dist/kernel/policy.js.map +1 -0
  59. package/dist/kernel/runtime.d.ts +117 -0
  60. package/dist/kernel/runtime.d.ts.map +1 -0
  61. package/dist/kernel/runtime.js +197 -0
  62. package/dist/kernel/runtime.js.map +1 -0
  63. package/dist/kernel/sql-sink.d.ts +36 -0
  64. package/dist/kernel/sql-sink.d.ts.map +1 -0
  65. package/dist/kernel/sql-sink.js +114 -0
  66. package/dist/kernel/sql-sink.js.map +1 -0
  67. package/dist/logger/format.d.ts +67 -0
  68. package/dist/logger/format.d.ts.map +1 -0
  69. package/dist/logger/format.js +218 -0
  70. package/dist/logger/format.js.map +1 -0
  71. package/dist/logger/index.d.ts +97 -0
  72. package/dist/logger/index.d.ts.map +1 -0
  73. package/dist/logger/index.js +363 -0
  74. package/dist/logger/index.js.map +1 -0
  75. package/dist/logger/rotate.d.ts +47 -0
  76. package/dist/logger/rotate.d.ts.map +1 -0
  77. package/dist/logger/rotate.js +242 -0
  78. package/dist/logger/rotate.js.map +1 -0
  79. package/dist/message/segment.d.ts +255 -0
  80. package/dist/message/segment.d.ts.map +1 -0
  81. package/dist/message/segment.js +510 -0
  82. package/dist/message/segment.js.map +1 -0
  83. package/dist/message/split.d.ts +34 -0
  84. package/dist/message/split.d.ts.map +1 -0
  85. package/dist/message/split.js +79 -0
  86. package/dist/message/split.js.map +1 -0
  87. package/dist/message/target.d.ts +29 -0
  88. package/dist/message/target.d.ts.map +1 -0
  89. package/dist/message/target.js +34 -0
  90. package/dist/message/target.js.map +1 -0
  91. package/dist/pipeline/cooldown.d.ts +84 -0
  92. package/dist/pipeline/cooldown.d.ts.map +1 -0
  93. package/dist/pipeline/cooldown.js +94 -0
  94. package/dist/pipeline/cooldown.js.map +1 -0
  95. package/dist/pipeline/dispatch.d.ts +98 -0
  96. package/dist/pipeline/dispatch.d.ts.map +1 -0
  97. package/dist/pipeline/dispatch.js +323 -0
  98. package/dist/pipeline/dispatch.js.map +1 -0
  99. package/dist/pipeline/event.d.ts +130 -0
  100. package/dist/pipeline/event.d.ts.map +1 -0
  101. package/dist/pipeline/event.js +538 -0
  102. package/dist/pipeline/event.js.map +1 -0
  103. package/dist/pipeline/middleware.d.ts +63 -0
  104. package/dist/pipeline/middleware.d.ts.map +1 -0
  105. package/dist/pipeline/middleware.js +174 -0
  106. package/dist/pipeline/middleware.js.map +1 -0
  107. package/dist/pipeline/prompt.d.ts +80 -0
  108. package/dist/pipeline/prompt.d.ts.map +1 -0
  109. package/dist/pipeline/prompt.js +155 -0
  110. package/dist/pipeline/prompt.js.map +1 -0
  111. package/dist/pipeline/router.d.ts +98 -0
  112. package/dist/pipeline/router.d.ts.map +1 -0
  113. package/dist/pipeline/router.js +489 -0
  114. package/dist/pipeline/router.js.map +1 -0
  115. package/dist/platform/detect.d.ts +24 -0
  116. package/dist/platform/detect.d.ts.map +1 -0
  117. package/dist/platform/detect.js +178 -0
  118. package/dist/platform/detect.js.map +1 -0
  119. package/dist/platform/paths.d.ts +38 -0
  120. package/dist/platform/paths.d.ts.map +1 -0
  121. package/dist/platform/paths.js +129 -0
  122. package/dist/platform/paths.js.map +1 -0
  123. package/dist/platform/system.d.ts +71 -0
  124. package/dist/platform/system.d.ts.map +1 -0
  125. package/dist/platform/system.js +242 -0
  126. package/dist/platform/system.js.map +1 -0
  127. package/dist/plugin/context.d.ts +92 -0
  128. package/dist/plugin/context.d.ts.map +1 -0
  129. package/dist/plugin/context.js +573 -0
  130. package/dist/plugin/context.js.map +1 -0
  131. package/dist/plugin/define.d.ts +92 -0
  132. package/dist/plugin/define.d.ts.map +1 -0
  133. package/dist/plugin/define.js +82 -0
  134. package/dist/plugin/define.js.map +1 -0
  135. package/dist/plugin/discover.d.ts +106 -0
  136. package/dist/plugin/discover.d.ts.map +1 -0
  137. package/dist/plugin/discover.js +254 -0
  138. package/dist/plugin/discover.js.map +1 -0
  139. package/dist/plugin/events.d.ts +109 -0
  140. package/dist/plugin/events.d.ts.map +1 -0
  141. package/dist/plugin/events.js +209 -0
  142. package/dist/plugin/events.js.map +1 -0
  143. package/dist/plugin/hooks.d.ts +266 -0
  144. package/dist/plugin/hooks.d.ts.map +1 -0
  145. package/dist/plugin/hooks.js +84 -0
  146. package/dist/plugin/hooks.js.map +1 -0
  147. package/dist/plugin/host.d.ts +160 -0
  148. package/dist/plugin/host.d.ts.map +1 -0
  149. package/dist/plugin/host.js +546 -0
  150. package/dist/plugin/host.js.map +1 -0
  151. package/dist/plugin/market.d.ts +264 -0
  152. package/dist/plugin/market.d.ts.map +1 -0
  153. package/dist/plugin/market.js +700 -0
  154. package/dist/plugin/market.js.map +1 -0
  155. package/dist/plugin/services.d.ts +116 -0
  156. package/dist/plugin/services.d.ts.map +1 -0
  157. package/dist/plugin/services.js +213 -0
  158. package/dist/plugin/services.js.map +1 -0
  159. package/dist/plugin/tar.d.ts +65 -0
  160. package/dist/plugin/tar.d.ts.map +1 -0
  161. package/dist/plugin/tar.js +277 -0
  162. package/dist/plugin/tar.js.map +1 -0
  163. package/dist/render/registry.d.ts +127 -0
  164. package/dist/render/registry.d.ts.map +1 -0
  165. package/dist/render/registry.js +306 -0
  166. package/dist/render/registry.js.map +1 -0
  167. package/dist/scheduler/index.d.ts +67 -0
  168. package/dist/scheduler/index.d.ts.map +1 -0
  169. package/dist/scheduler/index.js +279 -0
  170. package/dist/scheduler/index.js.map +1 -0
  171. package/dist/server/api.d.ts +179 -0
  172. package/dist/server/api.d.ts.map +1 -0
  173. package/dist/server/api.js +602 -0
  174. package/dist/server/api.js.map +1 -0
  175. package/dist/server/auth.d.ts +83 -0
  176. package/dist/server/auth.d.ts.map +1 -0
  177. package/dist/server/auth.js +178 -0
  178. package/dist/server/auth.js.map +1 -0
  179. package/dist/server/browse.d.ts +60 -0
  180. package/dist/server/browse.d.ts.map +1 -0
  181. package/dist/server/browse.js +187 -0
  182. package/dist/server/browse.js.map +1 -0
  183. package/dist/server/files.d.ts +27 -0
  184. package/dist/server/files.d.ts.map +1 -0
  185. package/dist/server/files.js +76 -0
  186. package/dist/server/files.js.map +1 -0
  187. package/dist/server/index.d.ts +189 -0
  188. package/dist/server/index.d.ts.map +1 -0
  189. package/dist/server/index.js +1123 -0
  190. package/dist/server/index.js.map +1 -0
  191. package/dist/server/table.d.ts +105 -0
  192. package/dist/server/table.d.ts.map +1 -0
  193. package/dist/server/table.js +289 -0
  194. package/dist/server/table.js.map +1 -0
  195. package/dist/store/index.d.ts +50 -0
  196. package/dist/store/index.d.ts.map +1 -0
  197. package/dist/store/index.js +120 -0
  198. package/dist/store/index.js.map +1 -0
  199. package/dist/store/json.d.ts +73 -0
  200. package/dist/store/json.d.ts.map +1 -0
  201. package/dist/store/json.js +174 -0
  202. package/dist/store/json.js.map +1 -0
  203. package/dist/store/kv.d.ts +126 -0
  204. package/dist/store/kv.d.ts.map +1 -0
  205. package/dist/store/kv.js +232 -0
  206. package/dist/store/kv.js.map +1 -0
  207. package/dist/store/level.d.ts +103 -0
  208. package/dist/store/level.d.ts.map +1 -0
  209. package/dist/store/level.js +119 -0
  210. package/dist/store/level.js.map +1 -0
  211. package/dist/store/memory.d.ts +61 -0
  212. package/dist/store/memory.d.ts.map +1 -0
  213. package/dist/store/memory.js +94 -0
  214. package/dist/store/memory.js.map +1 -0
  215. package/dist/store/sql.d.ts +142 -0
  216. package/dist/store/sql.d.ts.map +1 -0
  217. package/dist/store/sql.js +246 -0
  218. package/dist/store/sql.js.map +1 -0
  219. package/dist/testing/fake.d.ts +109 -0
  220. package/dist/testing/fake.d.ts.map +1 -0
  221. package/dist/testing/fake.js +196 -0
  222. package/dist/testing/fake.js.map +1 -0
  223. package/dist/testing/index.d.ts +16 -0
  224. package/dist/testing/index.d.ts.map +1 -0
  225. package/dist/testing/index.js +16 -0
  226. package/dist/testing/index.js.map +1 -0
  227. package/dist/testing/mock-adapter.d.ts +205 -0
  228. package/dist/testing/mock-adapter.d.ts.map +1 -0
  229. package/dist/testing/mock-adapter.js +309 -0
  230. package/dist/testing/mock-adapter.js.map +1 -0
  231. package/dist/util/deep.d.ts +108 -0
  232. package/dist/util/deep.d.ts.map +1 -0
  233. package/dist/util/deep.js +275 -0
  234. package/dist/util/deep.js.map +1 -0
  235. package/dist/util/defer.d.ts +107 -0
  236. package/dist/util/defer.d.ts.map +1 -0
  237. package/dist/util/defer.js +163 -0
  238. package/dist/util/defer.js.map +1 -0
  239. package/dist/util/dispose.d.ts +88 -0
  240. package/dist/util/dispose.d.ts.map +1 -0
  241. package/dist/util/dispose.js +143 -0
  242. package/dist/util/dispose.js.map +1 -0
  243. package/dist/util/duration.d.ts +28 -0
  244. package/dist/util/duration.d.ts.map +1 -0
  245. package/dist/util/duration.js +77 -0
  246. package/dist/util/duration.js.map +1 -0
  247. package/dist/util/fs.d.ts +110 -0
  248. package/dist/util/fs.d.ts.map +1 -0
  249. package/dist/util/fs.js +269 -0
  250. package/dist/util/fs.js.map +1 -0
  251. package/dist/util/id.d.ts +49 -0
  252. package/dist/util/id.d.ts.map +1 -0
  253. package/dist/util/id.js +87 -0
  254. package/dist/util/id.js.map +1 -0
  255. package/dist/util/lru.d.ts +74 -0
  256. package/dist/util/lru.d.ts.map +1 -0
  257. package/dist/util/lru.js +156 -0
  258. package/dist/util/lru.js.map +1 -0
  259. package/dist/util/queue.d.ts +64 -0
  260. package/dist/util/queue.d.ts.map +1 -0
  261. package/dist/util/queue.js +147 -0
  262. package/dist/util/queue.js.map +1 -0
  263. package/dist/util/text.d.ts +86 -0
  264. package/dist/util/text.d.ts.map +1 -0
  265. package/dist/util/text.js +182 -0
  266. package/dist/util/text.js.map +1 -0
  267. package/package.json +44 -0
  268. package/src/adapter/accounts.ts +758 -0
  269. package/src/adapter/bots.ts +582 -0
  270. package/src/adapter/host.ts +224 -0
  271. package/src/adapter/login.ts +481 -0
  272. package/src/adapter/registry.ts +202 -0
  273. package/src/config/core-config.test.ts +60 -0
  274. package/src/config/core-config.ts +401 -0
  275. package/src/config/schema.test.ts +179 -0
  276. package/src/config/schema.ts +1036 -0
  277. package/src/config/store.test.ts +245 -0
  278. package/src/config/store.ts +506 -0
  279. package/src/config/yaml.ts +119 -0
  280. package/src/http/client.test.ts +568 -0
  281. package/src/http/client.ts +777 -0
  282. package/src/index.ts +106 -0
  283. package/src/kernel/app.test.ts +267 -0
  284. package/src/kernel/app.ts +843 -0
  285. package/src/kernel/kv-sink.ts +58 -0
  286. package/src/kernel/policy.ts +120 -0
  287. package/src/kernel/runtime.test.ts +542 -0
  288. package/src/kernel/runtime.ts +307 -0
  289. package/src/kernel/sql-sink.ts +154 -0
  290. package/src/logger/format.ts +259 -0
  291. package/src/logger/index.ts +427 -0
  292. package/src/logger/rotate.ts +265 -0
  293. package/src/message/segment.test.ts +335 -0
  294. package/src/message/segment.ts +569 -0
  295. package/src/message/split.ts +97 -0
  296. package/src/message/target.ts +48 -0
  297. package/src/pipeline/cooldown.ts +136 -0
  298. package/src/pipeline/dispatch.ts +419 -0
  299. package/src/pipeline/event.ts +735 -0
  300. package/src/pipeline/middleware.test.ts +426 -0
  301. package/src/pipeline/middleware.ts +199 -0
  302. package/src/pipeline/prompt.ts +212 -0
  303. package/src/pipeline/router.test.ts +432 -0
  304. package/src/pipeline/router.ts +575 -0
  305. package/src/platform/detect.ts +178 -0
  306. package/src/platform/paths.ts +147 -0
  307. package/src/platform/system.test.ts +102 -0
  308. package/src/platform/system.ts +289 -0
  309. package/src/plugin/context.test.ts +459 -0
  310. package/src/plugin/context.ts +757 -0
  311. package/src/plugin/define.test.ts +120 -0
  312. package/src/plugin/define.ts +142 -0
  313. package/src/plugin/discover.test.ts +253 -0
  314. package/src/plugin/discover.ts +353 -0
  315. package/src/plugin/events.test.ts +162 -0
  316. package/src/plugin/events.ts +266 -0
  317. package/src/plugin/hooks.ts +373 -0
  318. package/src/plugin/host.test.ts +696 -0
  319. package/src/plugin/host.ts +738 -0
  320. package/src/plugin/market.test.ts +781 -0
  321. package/src/plugin/market.ts +870 -0
  322. package/src/plugin/services.test.ts +135 -0
  323. package/src/plugin/services.ts +261 -0
  324. package/src/plugin/tar.test.ts +158 -0
  325. package/src/plugin/tar.ts +309 -0
  326. package/src/render/registry.test.ts +127 -0
  327. package/src/render/registry.ts +439 -0
  328. package/src/scheduler/index.ts +357 -0
  329. package/src/server/api.test.ts +802 -0
  330. package/src/server/api.ts +869 -0
  331. package/src/server/auth.ts +201 -0
  332. package/src/server/browse.test.ts +193 -0
  333. package/src/server/browse.ts +240 -0
  334. package/src/server/files.ts +97 -0
  335. package/src/server/index.test.ts +566 -0
  336. package/src/server/index.ts +1386 -0
  337. package/src/server/table.ts +351 -0
  338. package/src/store/index.ts +156 -0
  339. package/src/store/json.ts +198 -0
  340. package/src/store/kv.test.ts +307 -0
  341. package/src/store/kv.ts +256 -0
  342. package/src/store/level.ts +167 -0
  343. package/src/store/memory.ts +109 -0
  344. package/src/store/sql.test.ts +204 -0
  345. package/src/store/sql.ts +324 -0
  346. package/src/testing/fake.ts +297 -0
  347. package/src/testing/index.ts +15 -0
  348. package/src/testing/mock-adapter.ts +588 -0
  349. package/src/util/deep.ts +272 -0
  350. package/src/util/defer.ts +211 -0
  351. package/src/util/dispose.ts +169 -0
  352. package/src/util/duration.ts +82 -0
  353. package/src/util/fs.ts +279 -0
  354. package/src/util/id.ts +94 -0
  355. package/src/util/lru.ts +185 -0
  356. package/src/util/queue.ts +167 -0
  357. package/src/util/text.ts +189 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * 模块职责:插件上下文(`ctx`)行为测试
3
+ * 依赖方向:测试文件,依赖 plugin/{context,events,services}、testing/fake
4
+ * 生命周期:每个用例一份上下文 + 一份回收簿
5
+ * 注意事项:本文件最重要的一组断言是**"回收簿一经 dispose,各子系统中即一条不剩"**——
6
+ * "一切皆可为插件"的前提是插件能被彻底移除。
7
+ */
8
+ import { join, resolve } from "node:path"
9
+ import { tmpdir } from "node:os"
10
+ import { describe, expect, it, vi } from "vitest"
11
+ import type {
12
+ AdapterProvider,
13
+ ConfigHandle,
14
+ ImageSegment,
15
+ KvDriver,
16
+ KvNamespace,
17
+ RenderResult,
18
+ RendererProvider
19
+ } from "@yunzai-ng/types"
20
+ import { createEventBus } from "./events.js"
21
+ import { createServiceRegistry } from "./services.js"
22
+ import { createPluginContext } from "./context.js"
23
+ import { DisposalRegistry } from "../util/dispose.js"
24
+ import { fakeAppView, fakeHttp, fakeLogger, recordingHooks } from "../testing/fake.js"
25
+
26
+ /** 假插件根目录(多数用例只做路径计算,不必真的存在) */
27
+ const ROOT = join(tmpdir(), "yzng-ctx-plugin")
28
+
29
+ /** 假数据目录 */
30
+ const DATA = join(tmpdir(), "yzng-ctx-data")
31
+
32
+ /**
33
+ * 装配一份上下文及其周边
34
+ * @returns 上下文与可断言的周边对象
35
+ */
36
+ function setup() {
37
+ const logger = fakeLogger()
38
+ const { hooks, recorded } = recordingHooks()
39
+ const services = createServiceRegistry()
40
+ const events = createEventBus({ logger })
41
+ const registry = new DisposalRegistry("demo")
42
+ const abort = new AbortController()
43
+
44
+ const { ctx, counters } = createPluginContext({
45
+ name: "demo",
46
+ version: "1.2.3",
47
+ root: ROOT,
48
+ dataDir: DATA,
49
+ logger,
50
+ // 上下文只是把它们原样挂上,用例里不会调用其方法
51
+ kv: {} as unknown as KvNamespace,
52
+ config: {} as unknown as ConfigHandle<unknown>,
53
+ app: fakeAppView(),
54
+ http: fakeHttp(),
55
+ services,
56
+ events,
57
+ hooks,
58
+ registry,
59
+ signal: abort.signal
60
+ })
61
+
62
+ return { ctx, counters, hooks, recorded, services, events, registry, abort, logger }
63
+ }
64
+
65
+ describe("PluginContext 基础", () => {
66
+ it("原样透传插件身份与目录", () => {
67
+ const { ctx, abort } = setup()
68
+ expect(ctx.name).toBe("demo")
69
+ expect(ctx.version).toBe("1.2.3")
70
+ expect(ctx.root).toBe(ROOT)
71
+ expect(ctx.dataDir).toBe(DATA)
72
+ expect(ctx.signal).toBe(abort.signal)
73
+ })
74
+ })
75
+
76
+ describe("ctx.command", () => {
77
+ it("登记进命令路由并计数", () => {
78
+ const { ctx, counters, recorded } = setup()
79
+ ctx.command("#ping").action(() => undefined)
80
+
81
+ expect(recorded.commands).toHaveLength(1)
82
+ expect(recorded.commands[0]?.plugin).toBe("demo")
83
+ expect(recorded.commands[0]?.patterns).toEqual(["#ping"])
84
+ expect(recorded.commands[0]?.handler).toBeTypeOf("function")
85
+ expect(counters.commands).toBe(1)
86
+ })
87
+
88
+ it("alias 选项与 alias() 都进 patterns", () => {
89
+ const { ctx, recorded } = setup()
90
+ ctx.command("#体力", { alias: ["#树脂"] }).alias("#tili")
91
+
92
+ expect(recorded.commands[0]?.patterns).toEqual(["#体力", "#树脂", "#tili"])
93
+ })
94
+
95
+ it("只有改了索引键才通知路由重新索引", () => {
96
+ const { ctx, recorded } = setup()
97
+ const cmd = ctx.command("#a")
98
+
99
+ cmd.desc("说明").master().cooldown("5s").priority(3)
100
+ expect(recorded.reindexed).toHaveLength(0)
101
+
102
+ cmd.alias("#b")
103
+ expect(recorded.reindexed).toHaveLength(1)
104
+ })
105
+
106
+ it("alias() 传空不产生多余的重新索引", () => {
107
+ const { ctx, recorded } = setup()
108
+ ctx.command("#a").alias()
109
+ expect(recorded.reindexed).toHaveLength(0)
110
+ })
111
+
112
+ it("链式改动落在 options 上", () => {
113
+ const { ctx, recorded } = setup()
114
+ ctx.command("#a").desc("说明").master().admin().scene("group").cooldown("5s", "user").priority(3)
115
+
116
+ expect(recorded.commands[0]?.options).toMatchObject({
117
+ desc: "说明",
118
+ master: true,
119
+ admin: true,
120
+ scene: ["group"],
121
+ cooldown: "5s",
122
+ cooldownScope: "user",
123
+ priority: 3
124
+ })
125
+ })
126
+
127
+ it("复用同一份 options 声明多条命令时互不串台", () => {
128
+ const { ctx, recorded } = setup()
129
+ // 插件作者常这么写:const opts = { desc: "..." },然后声明一串命令
130
+ const shared = { desc: "共享说明" }
131
+ ctx.command("#a", shared).desc("只改我")
132
+ ctx.command("#b", shared)
133
+
134
+ expect(recorded.commands[0]?.options.desc).toBe("只改我")
135
+ expect(recorded.commands[1]?.options.desc).toBe("共享说明")
136
+ // 更不该改到调用方手里的那个字面量
137
+ expect(shared.desc).toBe("共享说明")
138
+ })
139
+
140
+ it("重复设置 action 会告警(通常源于复制粘贴后未修改)", () => {
141
+ const { ctx, logger } = setup()
142
+ ctx
143
+ .command("#a")
144
+ .action(() => undefined)
145
+ .action(() => undefined)
146
+
147
+ expect(logger.lines.some(l => l.includes("#a") && l.includes("重复设置"))).toBe(true)
148
+ })
149
+
150
+ it("builder.dispose() 摘除命令并回退计数", () => {
151
+ const { ctx, counters, recorded } = setup()
152
+ const cmd = ctx.command("#a")
153
+ cmd.dispose()
154
+
155
+ expect(recorded.commands).toHaveLength(0)
156
+ expect(counters.commands).toBe(0)
157
+ })
158
+ })
159
+
160
+ describe("ctx.middleware / cron / every", () => {
161
+ it("中间件登记与注销", () => {
162
+ const { ctx, counters, recorded } = setup()
163
+ const undo = ctx.middleware(async (_e, next) => next(), { priority: 5 })
164
+
165
+ expect(recorded.middlewares).toHaveLength(1)
166
+ expect(recorded.middlewares[0]?.options).toEqual({ priority: 5 })
167
+ expect(counters.middlewares).toBe(1)
168
+
169
+ undo()
170
+ expect(recorded.middlewares).toHaveLength(0)
171
+ expect(counters.middlewares).toBe(0)
172
+ })
173
+
174
+ it("cron 原样传表达式", () => {
175
+ const { ctx, recorded } = setup()
176
+ ctx.cron("0 0 8 * * *", () => undefined, { name: "早报" })
177
+
178
+ expect(recorded.tasks[0]).toMatchObject({ plugin: "demo", kind: "cron", schedule: "0 0 8 * * *" })
179
+ })
180
+
181
+ it("every 把时长解析成毫秒", () => {
182
+ const { ctx, recorded } = setup()
183
+ ctx.every("30s", () => undefined)
184
+
185
+ expect(recorded.tasks[0]).toMatchObject({ kind: "interval", schedule: 30_000 })
186
+ })
187
+
188
+ it("every 周期小于 1 秒直接抛错,且不留下半个登记", () => {
189
+ const { ctx, counters, recorded } = setup()
190
+ expect(() => ctx.every("500ms", () => undefined)).toThrow(/最小 1s/)
191
+ expect(recorded.tasks).toHaveLength(0)
192
+ expect(counters.tasks).toBe(0)
193
+ })
194
+ })
195
+
196
+ describe("ctx 服务互操作", () => {
197
+ it("provide / inject / require 走服务注册表", () => {
198
+ const { ctx, counters, services } = setup()
199
+ const api = { ping: () => "pong" }
200
+ ctx.provide("demo.api", api)
201
+
202
+ expect(services.get("demo.api")).toBe(api)
203
+ expect(ctx.inject<typeof api>("demo.api")).toBe(api)
204
+ expect(ctx.require<typeof api>("demo.api")).toBe(api)
205
+ expect(counters.services).toBe(1)
206
+ })
207
+
208
+ it("inject 缺失返回 undefined,require 缺失抛错并点名本插件", () => {
209
+ const { ctx } = setup()
210
+ expect(ctx.inject("nope")).toBeUndefined()
211
+ expect(() => ctx.require("nope")).toThrow(/demo/)
212
+ })
213
+
214
+ it("waitFor 已就绪时立即拿到", async () => {
215
+ const { ctx } = setup()
216
+ ctx.provide("k", 42)
217
+ await expect(ctx.waitFor<number>("k")).resolves.toBe(42)
218
+ })
219
+
220
+ it("插件被卸载时 waitFor 立即以 undefined 结束(否则闭包被长期持有)", async () => {
221
+ const { ctx, abort } = setup()
222
+ // 缺省超时 30s:若不监听 abort,该 promise 会将已卸载插件的闭包持有 30 秒
223
+ const pending = ctx.waitFor("never.coming")
224
+ abort.abort()
225
+ await expect(pending).resolves.toBeUndefined()
226
+ })
227
+
228
+ it("已卸载的插件再调 waitFor 直接返回 undefined", async () => {
229
+ const { ctx, abort } = setup()
230
+ abort.abort()
231
+ await expect(ctx.waitFor("anything")).resolves.toBeUndefined()
232
+ })
233
+ })
234
+
235
+ describe("ctx 服务器相关", () => {
236
+ it("路由、WebSocket、静态目录都挂在 /plugin/<插件名> 作用域下", () => {
237
+ const { ctx, counters, recorded } = setup()
238
+ ctx.route("GET", "/status", async () => undefined)
239
+ ctx.websocket("/live", () => undefined)
240
+ ctx.static("/assets", join(ROOT, "resources"))
241
+
242
+ expect(recorded.routes).toEqual([
243
+ { scope: "/plugin/demo", method: "GET", path: "/status" },
244
+ { scope: "/plugin/demo", method: "WS", path: "/live" }
245
+ ])
246
+ expect(recorded.statics).toEqual([
247
+ { scope: "/plugin/demo", urlPath: "/assets", dir: join(ROOT, "resources"), spa: false }
248
+ ])
249
+ expect(counters.routes).toBe(2)
250
+ })
251
+
252
+ it("ctx.panel 挂在站点根路径,归属标识带插件名", () => {
253
+ const { ctx, recorded } = setup()
254
+ ctx.panel(join(ROOT, "panel-dist"))
255
+
256
+ // 归属不是 /plugin/demo:接管面板的意义在于占据 `/`,
257
+ // 而作用域之下的页面无法成为默认入口
258
+ expect(recorded.statics).toEqual([
259
+ { scope: "plugin:demo", urlPath: "/", dir: join(ROOT, "panel-dist"), spa: true }
260
+ ])
261
+ })
262
+
263
+ it("ctx.panel 的挂载随插件卸载一起回收", () => {
264
+ const { ctx, recorded, registry } = setup()
265
+ ctx.panel(join(ROOT, "panel-dist"))
266
+ expect(registry.dispose()).toEqual([])
267
+ expect(recorded.statics).toEqual([])
268
+ })
269
+ })
270
+
271
+ describe("ctx.render", () => {
272
+ it("模板根与资源根由内核按插件目录算好(插件只写相对名)", async () => {
273
+ const { ctx, recorded } = setup()
274
+ await ctx.render("player/daily-note", { uid: 1 })
275
+
276
+ expect(recorded.renders[0]).toMatchObject({
277
+ template: "player/daily-note",
278
+ data: { uid: 1 },
279
+ templateRoot: join(ROOT, "templates"),
280
+ resourceRoot: join(ROOT, "resources"),
281
+ origin: "demo"
282
+ })
283
+ })
284
+
285
+ it("单图返回一个图片段,且是 buffer 直传而非 base64", async () => {
286
+ const { ctx } = setup()
287
+ const seg = (await ctx.render("t")) as ImageSegment
288
+
289
+ expect(Array.isArray(seg)).toBe(false)
290
+ expect(seg.type).toBe("image")
291
+ expect(seg.file).toMatchObject({ kind: "buffer", mime: "image/jpeg" })
292
+ })
293
+
294
+ it("按 opts.type 给出对应 MIME", async () => {
295
+ const { ctx } = setup()
296
+ const seg = (await ctx.render("t", {}, { type: "png" })) as ImageSegment
297
+ expect(seg.file).toMatchObject({ mime: "image/png" })
298
+ })
299
+
300
+ it("多图返回数组(分页长图)", async () => {
301
+ const { ctx, hooks } = setup()
302
+ hooks.render = {
303
+ ...hooks.render,
304
+ render: async () => ({ images: [new Uint8Array([1]), new Uint8Array([2])], cost: 1, renderer: "fake" })
305
+ }
306
+
307
+ const seg = await ctx.render("t")
308
+ expect(Array.isArray(seg)).toBe(true)
309
+ expect(seg).toHaveLength(2)
310
+ })
311
+
312
+ it("渲染器返回空结果时抛错,而不是发一条空消息出去", async () => {
313
+ const { ctx, hooks } = setup()
314
+ hooks.render = {
315
+ ...hooks.render,
316
+ render: async (): Promise<RenderResult> => ({ images: [], cost: 0, renderer: "空渲染器" })
317
+ }
318
+
319
+ await expect(ctx.render("t")).rejects.toThrow(/没有产出图片/)
320
+ })
321
+ })
322
+
323
+ describe("ctx.sql", () => {
324
+ it("同名库只打开一次(多连接会在 WAL 下互相等锁)", async () => {
325
+ const { ctx, recorded } = setup()
326
+ const [a, b] = await Promise.all([ctx.sql(), ctx.sql()])
327
+
328
+ expect(a).toBe(b)
329
+ expect(recorded.sqlOpened).toEqual(["demo/main"])
330
+ })
331
+
332
+ it("不同库名各打开一次", async () => {
333
+ const { ctx, recorded } = setup()
334
+ await ctx.sql()
335
+ await ctx.sql("gacha")
336
+ expect(recorded.sqlOpened).toEqual(["demo/main", "demo/gacha"])
337
+ })
338
+
339
+ it("打开失败不留坏缓存,重试仍有机会成功", async () => {
340
+ const { ctx, hooks, recorded } = setup()
341
+ const real = hooks.sql.open
342
+ let attempt = 0
343
+ hooks.sql = {
344
+ ...hooks.sql,
345
+ open: async (plugin, name) => {
346
+ attempt++
347
+ if (attempt === 1) throw new Error("磁盘忙")
348
+ return real(plugin, name)
349
+ }
350
+ }
351
+
352
+ await expect(ctx.sql()).rejects.toThrow(/磁盘忙/)
353
+ await expect(ctx.sql()).resolves.toBeDefined()
354
+ expect(recorded.sqlOpened).toEqual(["demo/main"])
355
+ })
356
+ })
357
+
358
+ describe("ctx.resource", () => {
359
+ it("拼出插件内的绝对路径", () => {
360
+ const { ctx } = setup()
361
+ expect(ctx.resource("resources", "img", "bg.png")).toBe(join(ROOT, "resources", "img", "bg.png"))
362
+ })
363
+
364
+ it("挡住越出插件根的路径(参数可能来自用户消息)", () => {
365
+ const { ctx } = setup()
366
+ expect(() => ctx.resource("..", "..", "etc", "passwd")).toThrow()
367
+ expect(() => ctx.resource(resolve("/etc/passwd"))).toThrow()
368
+ })
369
+ })
370
+
371
+ describe("回收:卸载必须完整归还全部资源", () => {
372
+ it("registry.dispose() 之后各子系统中不残留任何条目", async () => {
373
+ const { ctx, counters, recorded, services, events, registry } = setup()
374
+
375
+ ctx.command("#a").action(() => undefined)
376
+ ctx.command("#b")
377
+ ctx.middleware(async (_e, next) => next())
378
+ ctx.cron("0 0 8 * * *", () => undefined)
379
+ ctx.every("10s", () => undefined)
380
+ ctx.on("app/ready", () => undefined)
381
+ ctx.provide("demo.api", { x: 1 })
382
+ ctx.route("GET", "/s", async () => undefined)
383
+ ctx.websocket("/w", () => undefined)
384
+ ctx.static("/a", ROOT)
385
+ ctx.panel(join(ROOT, "panel-dist"))
386
+ ctx.registerAdapter({ id: "fake-adapter" } as unknown as AdapterProvider)
387
+ ctx.registerRenderer({ id: "fake-renderer" } as unknown as RendererProvider)
388
+ ctx.registerKvDriver({ id: "fake-kv" } as unknown as KvDriver)
389
+ await ctx.sql()
390
+
391
+ const failures = registry.dispose()
392
+ expect(failures).toEqual([])
393
+
394
+ expect(recorded.commands).toEqual([])
395
+ expect(recorded.middlewares).toEqual([])
396
+ expect(recorded.tasks).toEqual([])
397
+ expect(recorded.routes).toEqual([])
398
+ expect(recorded.statics).toEqual([])
399
+ expect(recorded.adapters).toEqual([])
400
+ expect(recorded.renderers).toEqual([])
401
+ expect(recorded.kvDrivers).toEqual([])
402
+ // SQL 连接的生死由存储层按插件统一管理,卸载时要求它关掉本插件的全部库
403
+ expect(recorded.sqlClosed).toEqual(["demo"])
404
+
405
+ expect(services.size).toBe(0)
406
+ expect(events.count("app/ready")).toBe(0)
407
+ expect(counters).toEqual({ commands: 0, middlewares: 0, tasks: 0, routes: 0, services: 0 })
408
+ })
409
+
410
+ it("onDispose 注册的清理函数会被调用", () => {
411
+ const { ctx, registry } = setup()
412
+ const clean = vi.fn()
413
+ ctx.onDispose(clean)
414
+
415
+ registry.dispose()
416
+ expect(clean).toHaveBeenCalledTimes(1)
417
+ })
418
+
419
+ it("卸载后迟到的注册被立即回收(异步 setup 慢一步的情形)", () => {
420
+ const { ctx, counters, recorded, registry } = setup()
421
+ registry.dispose()
422
+
423
+ // 插件的 setup 里 `void (async () => { ...await...; ctx.command(...) })()`
424
+ // 这种写法很常见,卸载后它可能才跑到注册那一行
425
+ ctx.command("#late")
426
+ ctx.middleware(async (_e, next) => next())
427
+
428
+ expect(recorded.commands).toEqual([])
429
+ expect(recorded.middlewares).toEqual([])
430
+ expect(counters.commands).toBe(0)
431
+ expect(counters.middlewares).toBe(0)
432
+ })
433
+
434
+ it("cache() 随卸载清空", () => {
435
+ const { ctx, registry } = setup()
436
+ const cache = ctx.cache<number>({ max: 10 })
437
+ cache.set("k", 1)
438
+ expect(cache.get("k")).toBe(1)
439
+
440
+ registry.dispose()
441
+ expect(cache.get("k")).toBeUndefined()
442
+ expect(cache.size).toBe(0)
443
+ })
444
+
445
+ it("单个 disposer 幂等,不会把计数扣成负数", () => {
446
+ const { ctx, counters } = setup()
447
+ const undo = ctx.middleware(async (_e, next) => next())
448
+ undo()
449
+ undo()
450
+ expect(counters.middlewares).toBe(0)
451
+ })
452
+ })
453
+
454
+ describe("ctx.pickBot", () => {
455
+ it("走账号管理器;无账号时给 undefined 而不是抛错", () => {
456
+ const { ctx } = setup()
457
+ expect(ctx.pickBot()).toBeUndefined()
458
+ })
459
+ })