@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,696 @@
1
+ /**
2
+ * 模块职责:插件宿主的加载 / 失败隔离 / 卸载 / 热重载测试
3
+ * 依赖方向:测试文件,依赖 plugin/host、config/store、store、testing/fake
4
+ * 生命周期:每个用例一套临时目录 + 一个宿主,afterEach 全部关闭
5
+ * 注意事项:夹具插件是**确实写入磁盘后再 import 进来**的,而非在内存中构造对象。
6
+ * 该链路(发现 → 导入 → 装配上下文 → setup → 回收)出现过的问题几乎均位于
7
+ * 真实 import 的那一段:默认导出书写错误、语法错误、setup 无响应、
8
+ * 卸载后模块缓存仍存在。构造替身对象无法覆盖这些情形。
9
+ *
10
+ * 临时目录中不存在 node_modules,夹具无法 `import "@yunzai-ng/core"`。
11
+ * 因此夹具采用两条注入通道:
12
+ * 1) 品牌标记使用 `Symbol.for("yunzai-ng.plugin")` —— 注册表符号,全进程共享;
13
+ * 2) 需要 `s`(配置 schema)时从 `globalThis` 取测试注入的那一份 ——
14
+ * `instanceof Schema` 要求同一个类实例,只能以此方式传递。
15
+ */
16
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
17
+ import { tmpdir } from "node:os"
18
+ import { join } from "node:path"
19
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
20
+ import type { RuntimePaths } from "@yunzai-ng/types"
21
+ import { createConfigStore, type ConfigStore } from "../config/store.js"
22
+ import { s } from "../config/schema.js"
23
+ import { openKv, type KvStore } from "../store/index.js"
24
+ import { createEventBus, type CoreEventBus } from "./events.js"
25
+ import { createServiceRegistry, type ServiceRegistry } from "./services.js"
26
+ import { PluginHost, createPluginsView } from "./host.js"
27
+ import { fakeAppView, fakeHttp, fakeLogger, recordingHooks, type FakeLogger, type Recorded } from "../testing/fake.js"
28
+
29
+ /** 夹具与测试之间的共享通道 */
30
+ interface TestGlobals {
31
+ /** 夹具插件写下的执行痕迹(顺序敏感的断言靠它) */
32
+ __yzngMarks?: string[]
33
+ /** 注入给夹具的 schema 构造器 */
34
+ __yzngS?: typeof s
35
+ /** 夹具回填的配置快照 */
36
+ __yzngConfig?: unknown
37
+ }
38
+
39
+ /** 取共享全局 */
40
+ const shared = (): TestGlobals => globalThis as unknown as TestGlobals
41
+
42
+ /** 取执行痕迹数组 */
43
+ function marks(): string[] {
44
+ const g = shared()
45
+ g.__yzngMarks ??= []
46
+ return g.__yzngMarks
47
+ }
48
+
49
+ /** 夹具里给对象打品牌的前缀代码 */
50
+ const BRAND = 'const brand = Symbol.for("yunzai-ng.plugin")\n'
51
+
52
+ /** 一个宿主及其周边 */
53
+ interface Harness {
54
+ /** 宿主 */
55
+ host: PluginHost
56
+ /** 插件目录 */
57
+ pluginsDir: string
58
+ /** 配置目录 */
59
+ configDir: string
60
+ /** 日志 */
61
+ logger: FakeLogger
62
+ /** 子系统录制结果 */
63
+ recorded: Recorded
64
+ /** 服务注册表 */
65
+ services: ServiceRegistry
66
+ /** 事件总线 */
67
+ events: CoreEventBus
68
+ /** 配置仓库 */
69
+ config: ConfigStore
70
+ /** KV */
71
+ kv: KvStore
72
+ /** 根临时目录 */
73
+ root: string
74
+ }
75
+
76
+ let harness: Harness | undefined
77
+
78
+ /**
79
+ * 起一套宿主
80
+ * @param opts 可选参数
81
+ * @param opts.setupTimeout setup 超时毫秒
82
+ * @param opts.disabled 禁用的插件名
83
+ * @returns 宿主与周边
84
+ */
85
+ async function makeHarness(opts: { setupTimeout?: number; disabled?: string[] } = {}): Promise<Harness> {
86
+ const root = await mkdtemp(join(tmpdir(), "yzng-host-"))
87
+ const paths: RuntimePaths = {
88
+ home: root,
89
+ config: join(root, "config"),
90
+ data: join(root, "data"),
91
+ logs: join(root, "logs"),
92
+ temp: join(root, "temp"),
93
+ plugins: join(root, "plugins"),
94
+ runtime: root,
95
+ cache: join(root, "cache")
96
+ }
97
+ for (const dir of Object.values(paths)) await mkdir(dir, { recursive: true })
98
+
99
+ const logger = fakeLogger()
100
+ const { hooks, recorded } = recordingHooks()
101
+ const config = createConfigStore({ dir: paths.config, logger, watch: false })
102
+ // memory 驱动:用例只关心"命名空间给对了没",不需要真的落盘
103
+ const kv = await openKv({ dir: join(root, "kv"), driver: "memory", logger })
104
+ const services = createServiceRegistry()
105
+ const events = createEventBus({ logger })
106
+
107
+ const host = new PluginHost({
108
+ paths,
109
+ logger,
110
+ config,
111
+ kv,
112
+ http: fakeHttp(),
113
+ services,
114
+ events,
115
+ hooks,
116
+ app: () => fakeAppView(),
117
+ setupTimeout: opts.setupTimeout ?? 2000,
118
+ ...(opts.disabled ? { disabled: opts.disabled } : {})
119
+ })
120
+
121
+ harness = { host, pluginsDir: paths.plugins, configDir: paths.config, logger, recorded, services, events, config, kv, root }
122
+ return harness
123
+ }
124
+
125
+ /**
126
+ * 往插件目录里写一个插件
127
+ * @param h 宿主套件
128
+ * @param name 目录名
129
+ * @param body 模块源码
130
+ * @param manifest package.json 内容;省略则该目录不含 package.json
131
+ */
132
+ async function writePlugin(
133
+ h: Harness,
134
+ name: string,
135
+ body: string,
136
+ manifest?: Record<string, unknown>
137
+ ): Promise<void> {
138
+ const dir = join(h.pluginsDir, name)
139
+ await mkdir(dir, { recursive: true })
140
+ await writeFile(join(dir, "index.js"), body, "utf8")
141
+ if (manifest !== undefined) await writeFile(join(dir, "package.json"), JSON.stringify(manifest), "utf8")
142
+ }
143
+
144
+ beforeEach(() => {
145
+ shared().__yzngMarks = []
146
+ shared().__yzngS = s
147
+ delete shared().__yzngConfig
148
+ })
149
+
150
+ afterEach(async () => {
151
+ if (!harness) return
152
+ const h = harness
153
+ harness = undefined
154
+ await h.host.dispose()
155
+ h.config.dispose()
156
+ h.events.close()
157
+ await h.kv.close()
158
+ await rm(h.root, { recursive: true, force: true })
159
+ })
160
+
161
+ describe("PluginHost 加载", () => {
162
+ it("加载一个正常插件,状态与计数都对得上", async () => {
163
+ const h = await makeHarness()
164
+ await writePlugin(
165
+ h,
166
+ "ok",
167
+ `${BRAND}export default {
168
+ name: "ok",
169
+ version: "1.2.3",
170
+ description: "正常插件",
171
+ setup(ctx) {
172
+ ctx.command("#ping").action(() => undefined)
173
+ globalThis.__yzngMarks.push("ok:setup")
174
+ },
175
+ [brand]: true
176
+ }`
177
+ )
178
+
179
+ const report = await h.host.loadAll()
180
+
181
+ expect(report.loaded).toEqual(["ok"])
182
+ expect(report.failed).toEqual([])
183
+ expect(h.host.size).toBe(1)
184
+ expect(marks()).toEqual(["ok:setup"])
185
+
186
+ const state = h.host.get("ok")
187
+ expect(state).toMatchObject({
188
+ name: "ok",
189
+ version: "1.2.3",
190
+ description: "正常插件",
191
+ status: "loaded",
192
+ commands: 1,
193
+ builtin: false
194
+ })
195
+ expect(h.recorded.commands).toHaveLength(1)
196
+ })
197
+
198
+ it("`homepage` 从 package.json 带出,供面板画「访问仓库」", async () => {
199
+ const h = await makeHarness()
200
+ await writePlugin(h, "withhome", `export default { name: "withhome", setup() {} }`, {
201
+ name: "withhome",
202
+ version: "1.0.0",
203
+ homepage: "https://example.com/repo"
204
+ })
205
+
206
+ await h.host.loadAll()
207
+
208
+ expect(h.host.get("withhome")?.homepage).toBe("https://example.com/repo")
209
+ })
210
+
211
+ it("`definePlugin` 里写的 homepage 优先于 package.json", async () => {
212
+ const h = await makeHarness()
213
+ await writePlugin(
214
+ h,
215
+ "bothhome",
216
+ `export default { name: "bothhome", homepage: "https://from-define", setup() {} }`,
217
+ { name: "bothhome", homepage: "https://from-manifest" }
218
+ )
219
+
220
+ await h.host.loadAll()
221
+
222
+ expect(h.host.get("bothhome")?.homepage).toBe("https://from-define")
223
+ })
224
+
225
+ it("**取不到主页时该字段不出现** —— 按钮在而点了没反应,比按钮不在更糟", async () => {
226
+ const h = await makeHarness()
227
+ await writePlugin(h, "nohome", `export default { name: "nohome", setup() {} }`, { name: "nohome" })
228
+
229
+ await h.host.loadAll()
230
+
231
+ const state = h.host.get("nohome")
232
+ expect(state).not.toHaveProperty("homepage")
233
+ })
234
+
235
+ it("没走 definePlugin 的手写定义也接受,但留一条 debug 提示", async () => {
236
+ const h = await makeHarness()
237
+ await writePlugin(h, "loose", `export default { name: "loose", setup() {} }`)
238
+
239
+ const report = await h.host.loadAll()
240
+
241
+ expect(report.loaded).toEqual(["loose"])
242
+ expect(h.logger.lines.some(l => l.includes("definePlugin"))).toBe(true)
243
+ })
244
+
245
+ it("未在 definePlugin 中声明版本时,日志、状态与 ctx 三处一致地取 package.json 的版本", async () => {
246
+ const h = await makeHarness()
247
+ await writePlugin(
248
+ h,
249
+ "panel",
250
+ `${BRAND}export default {
251
+ name: "panel",
252
+ setup(ctx) { globalThis.__yzngMarks.push("ctx:" + ctx.version) },
253
+ [brand]: true
254
+ }`,
255
+ { name: "@scope/panel-plugin", version: "2.3.4", main: "index.js" }
256
+ )
257
+
258
+ await h.host.loadAll()
259
+
260
+ // 三处必须给出同一个版本号。曾经日志只看 definition.version 而另两处回落到
261
+ // package.json:未重复声明版本的插件在日志里是 0.0.0、在面板里是 2.3.4
262
+ expect(h.host.get("panel")?.version).toBe("2.3.4")
263
+ expect(marks()).toEqual(["ctx:2.3.4"])
264
+ expect(h.logger.lines.some(l => l.includes("插件 panel@2.3.4 已加载"))).toBe(true)
265
+ })
266
+ it("插件名以定义里的 name 为准,与目录名无关", async () => {
267
+ const h = await makeHarness()
268
+ // 目录名故意与插件名不同:用户 clone 出来的目录名很随意(带 -main 后缀是常态)
269
+ //
270
+ // 注:这里不用中文目录名 —— vitest 的模块运行器不会对 file:// URL 做百分号解码,
271
+ // 非 ASCII 目录在测试环境里必然 "Does the file exist?"。纯 Node 下无此问题,
272
+ // 别为这个去改 host.ts。
273
+ await writePlugin(h, "some-plugin-main", `export default { name: "real-name", setup() {} }`)
274
+
275
+ const report = await h.host.loadAll()
276
+ expect(report.loaded).toEqual(["real-name"])
277
+ expect(h.host.get("real-name")).toBeDefined()
278
+ })
279
+
280
+ it("被禁用的插件不加载", async () => {
281
+ const h = await makeHarness({ disabled: ["nope"] })
282
+ await writePlugin(h, "nope", `export default { name: "nope", setup() {} }`)
283
+
284
+ const report = await h.host.loadAll()
285
+ expect(report.loaded).toEqual([])
286
+ expect(h.host.size).toBe(0)
287
+ })
288
+
289
+ it("重复 loadAll 不会把同一个插件装两遍", async () => {
290
+ const h = await makeHarness()
291
+ await writePlugin(
292
+ h,
293
+ "ok",
294
+ `export default { name: "ok", setup(ctx) { ctx.command("#a") } }`
295
+ )
296
+
297
+ await h.host.loadAll()
298
+ await h.host.loadAll()
299
+
300
+ expect(h.host.size).toBe(1)
301
+ expect(h.recorded.commands).toHaveLength(1)
302
+ expect(h.logger.lines.some(l => l.includes("忽略重复加载"))).toBe(true)
303
+ })
304
+ })
305
+
306
+ describe("PluginHost 失败隔离", () => {
307
+ it("一个插件 setup 抛错,其余插件照常加载", async () => {
308
+ const h = await makeHarness()
309
+ await writePlugin(
310
+ h,
311
+ "boom",
312
+ `export default {
313
+ name: "boom",
314
+ setup(ctx) {
315
+ ctx.command("#boom")
316
+ throw new Error("插件自身执行失败")
317
+ }
318
+ }`
319
+ )
320
+ await writePlugin(h, "ok", `export default { name: "ok", setup(ctx) { ctx.command("#ok") } }`)
321
+
322
+ const report = await h.host.loadAll()
323
+
324
+ expect(report.loaded).toEqual(["ok"])
325
+ expect(report.failed.map(f => f.name)).toEqual(["boom"])
326
+ expect(report.failed[0]?.reason).toContain("插件自身执行失败")
327
+ expect(h.host.get("boom")).toMatchObject({ status: "error" })
328
+
329
+ // 关键:失败的插件在 setup 中已完成的命令注册必须被回收,
330
+ // 否则路由里留着一条"点了没反应"的命令
331
+ expect(h.recorded.commands.map(c => c.patterns[0])).toEqual(["#ok"])
332
+ })
333
+
334
+ it("setup 超时会被判失败,且已完成的注册一并回收", async () => {
335
+ const h = await makeHarness({ setupTimeout: 120 })
336
+ await writePlugin(
337
+ h,
338
+ "slow",
339
+ `export default {
340
+ name: "slow",
341
+ setup(ctx) {
342
+ ctx.command("#slow")
343
+ return new Promise(() => {})
344
+ }
345
+ }`
346
+ )
347
+
348
+ const report = await h.host.loadAll()
349
+
350
+ expect(report.loaded).toEqual([])
351
+ expect(report.failed[0]?.name).toBe("slow")
352
+ expect(report.failed[0]?.reason).toContain("超时")
353
+ expect(h.recorded.commands).toEqual([])
354
+ expect(h.logger.lines.some(l => l.includes("app/ready"))).toBe(true)
355
+ })
356
+
357
+ it("语法写错的插件只记错误,不影响启动", async () => {
358
+ const h = await makeHarness()
359
+ await writePlugin(h, "broken", `export default { name: "broken", setup() { <<< } }`)
360
+ await writePlugin(h, "ok", `export default { name: "ok", setup() {} }`)
361
+
362
+ const report = await h.host.loadAll()
363
+
364
+ expect(report.loaded).toEqual(["ok"])
365
+ expect(report.failed.map(f => f.name)).toEqual(["broken"])
366
+ expect(h.logger.lines.some(l => l.includes("broken") && l.includes("导入出错"))).toBe(true)
367
+ })
368
+
369
+ it("没有默认导出时,提示怎么写才对", async () => {
370
+ const h = await makeHarness()
371
+ await writePlugin(h, "nodefault", `export const something = 1`)
372
+
373
+ const report = await h.host.loadAll()
374
+ expect(report.failed[0]?.reason).toContain("export default definePlugin")
375
+ })
376
+
377
+ it("默认导出不像插件时也给出可照做的提示", async () => {
378
+ const h = await makeHarness()
379
+ await writePlugin(h, "weird", `export default 42`)
380
+
381
+ const report = await h.host.loadAll()
382
+ expect(report.failed[0]?.reason).toContain("definePlugin({ name, setup })")
383
+ })
384
+
385
+ it("依赖没安装的插件被跳过,原因说清怎么办", async () => {
386
+ const h = await makeHarness()
387
+ await writePlugin(
388
+ h,
389
+ "needs",
390
+ `export default { name: "needs", dependencies: ["not-installed"], setup() {} }`
391
+ )
392
+
393
+ const report = await h.host.loadAll()
394
+
395
+ expect(report.loaded).toEqual([])
396
+ expect(report.failed[0]?.reason).toContain("not-installed")
397
+ expect(h.host.get("needs")).toMatchObject({ status: "error" })
398
+ })
399
+
400
+ it("依赖装了但加载失败时,依赖方一并跳过(不留半残状态)", async () => {
401
+ const h = await makeHarness()
402
+ await writePlugin(h, "base", `export default { name: "base", setup() { throw new Error("底座执行失败") } }`)
403
+ await writePlugin(
404
+ h,
405
+ "child",
406
+ `export default { name: "child", dependencies: ["base"], setup(ctx) { ctx.command("#child") } }`
407
+ )
408
+
409
+ const report = await h.host.loadAll()
410
+
411
+ expect(report.loaded).toEqual([])
412
+ expect(h.host.get("child")?.error).toContain("依赖的插件未成功加载")
413
+ expect(h.host.get("child")?.error).toContain("base")
414
+ expect(h.recorded.commands).toEqual([])
415
+ })
416
+ })
417
+
418
+ describe("PluginHost 加载顺序", () => {
419
+ it("依赖先于依赖方 setup,即使依赖方优先级更高", async () => {
420
+ const h = await makeHarness()
421
+ // user 的 priority 更小(更想早加载),但它依赖 base —— 依赖必须赢
422
+ await writePlugin(
423
+ h,
424
+ "base",
425
+ `export default { name: "base", priority: 200, setup() { globalThis.__yzngMarks.push("base") } }`
426
+ )
427
+ await writePlugin(
428
+ h,
429
+ "user",
430
+ `export default { name: "user", priority: 1, dependencies: ["base"], setup() { globalThis.__yzngMarks.push("user") } }`
431
+ )
432
+
433
+ await h.host.loadAll()
434
+ expect(marks()).toEqual(["base", "user"])
435
+ })
436
+
437
+ it("无依赖时按 priority 排,与文件名字典序无关", async () => {
438
+ const h = await makeHarness()
439
+ await writePlugin(
440
+ h,
441
+ "a-late",
442
+ `export default { name: "a-late", priority: 200, setup() { globalThis.__yzngMarks.push("a-late") } }`
443
+ )
444
+ await writePlugin(
445
+ h,
446
+ "z-early",
447
+ `export default { name: "z-early", priority: 1, setup() { globalThis.__yzngMarks.push("z-early") } }`
448
+ )
449
+
450
+ await h.host.loadAll()
451
+ // 按 priority 排而非文件名,否则会是 ["a-late", "z-early"]
452
+ expect(marks()).toEqual(["z-early", "a-late"])
453
+ })
454
+ })
455
+
456
+ describe("PluginHost 配置", () => {
457
+ it("声明 configSchema 的插件拿到真配置文件与默认值", async () => {
458
+ const h = await makeHarness()
459
+ await writePlugin(
460
+ h,
461
+ "cfg",
462
+ `${BRAND}const s = globalThis.__yzngS
463
+ export default {
464
+ name: "cfg",
465
+ configSchema: s.object({ cookie: s.string().default("空"), retry: s.number().default(3) }),
466
+ setup(ctx) { globalThis.__yzngConfig = ctx.config.get() },
467
+ [brand]: true
468
+ }`
469
+ )
470
+
471
+ await h.host.loadAll()
472
+
473
+ expect(shared().__yzngConfig).toEqual({ cookie: "空", retry: 3 })
474
+ expect(h.config.get("cfg")).toBeDefined()
475
+ // 面板据此决定是否给这个插件展示配置入口
476
+ expect(h.host.get("cfg")?.configured).toBe(true)
477
+ })
478
+
479
+ it("没声明 configSchema 时 get() 给空对象,写操作报错并说明办法", async () => {
480
+ const h = await makeHarness()
481
+ await writePlugin(
482
+ h,
483
+ "nocfg",
484
+ `export default {
485
+ name: "nocfg",
486
+ setup(ctx) {
487
+ globalThis.__yzngConfig = ctx.config.get()
488
+ globalThis.__yzngMarks.push("get-ok")
489
+ ctx.config.patch({ a: 1 }).catch(err => globalThis.__yzngMarks.push(err.message))
490
+ }
491
+ }`
492
+ )
493
+
494
+ await h.host.loadAll()
495
+ await new Promise(resolve => setTimeout(resolve, 10))
496
+
497
+ expect(shared().__yzngConfig).toEqual({})
498
+ expect(marks()[0]).toBe("get-ok")
499
+ expect(marks()[1]).toContain("configSchema")
500
+ expect(h.host.get("nocfg")?.configured).toBe(false)
501
+ })
502
+
503
+ it("configSchema 不是 s.object(...) 的产物时忽略并告警", async () => {
504
+ const h = await makeHarness()
505
+ await writePlugin(
506
+ h,
507
+ "badcfg",
508
+ `export default { name: "badcfg", configSchema: { type: "object" }, setup() {} }`
509
+ )
510
+
511
+ const report = await h.host.loadAll()
512
+ expect(report.loaded).toEqual(["badcfg"])
513
+ expect(h.logger.lines.some(l => l.includes("badcfg") && l.includes("s.object"))).toBe(true)
514
+ })
515
+ })
516
+
517
+ describe("PluginHost 卸载", () => {
518
+ /** 一个把各种能力都用上的插件 */
519
+ const FULL = `export default {
520
+ name: "full",
521
+ setup(ctx) {
522
+ ctx.command("#a")
523
+ ctx.middleware(async (e, next) => next())
524
+ ctx.cron("0 0 8 * * *", () => {})
525
+ ctx.on("app/ready", () => {})
526
+ ctx.provide("full.api", { x: 1 })
527
+ return () => globalThis.__yzngMarks.push("cleanup")
528
+ }
529
+ }`
530
+
531
+ it("卸载后各子系统里一条不剩,setup 返回的清理函数也被调用", async () => {
532
+ const h = await makeHarness()
533
+ await writePlugin(h, "full", FULL)
534
+ await h.host.loadAll()
535
+
536
+ expect(await h.host.unload("full")).toBe(true)
537
+
538
+ expect(h.host.size).toBe(0)
539
+ expect(h.host.get("full")).toBeUndefined()
540
+ expect(h.recorded.commands).toEqual([])
541
+ expect(h.recorded.middlewares).toEqual([])
542
+ expect(h.recorded.tasks).toEqual([])
543
+ expect(h.services.size).toBe(0)
544
+ expect(h.events.count("app/ready")).toBe(0)
545
+ expect(marks()).toContain("cleanup")
546
+ })
547
+
548
+ it("卸载未加载的插件返回 false", async () => {
549
+ const h = await makeHarness()
550
+ expect(await h.host.unload("ghost")).toBe(false)
551
+ })
552
+
553
+ it("绕过 ctx 直接注册的服务在卸载时被兜底清扫并告警", async () => {
554
+ const h = await makeHarness()
555
+ await writePlugin(h, "ok", `export default { name: "ok", setup() {} }`)
556
+ await h.host.loadAll()
557
+
558
+ // 模拟越界写法:插件不知从哪拿到注册表,绕过 ctx 自己注册
559
+ h.services.provide("sneaky", 1, "ok")
560
+ await h.host.unload("ok")
561
+
562
+ expect(h.services.has("sneaky")).toBe(false)
563
+ expect(h.logger.lines.some(l => l.includes("未经 ctx 注册"))).toBe(true)
564
+ })
565
+
566
+ it("dispose 按逆加载顺序卸载,依赖方先走", async () => {
567
+ const h = await makeHarness()
568
+ await writePlugin(
569
+ h,
570
+ "base",
571
+ `export default { name: "base", setup() { return () => globalThis.__yzngMarks.push("base-off") } }`
572
+ )
573
+ await writePlugin(
574
+ h,
575
+ "user",
576
+ `export default { name: "user", dependencies: ["base"], setup() { return () => globalThis.__yzngMarks.push("user-off") } }`
577
+ )
578
+ await h.host.loadAll()
579
+
580
+ expect(await h.host.dispose()).toBe(2)
581
+ expect(marks()).toEqual(["user-off", "base-off"])
582
+ expect(h.host.list()).toEqual([])
583
+ })
584
+
585
+ it("卸载会广播 plugin/unloaded", async () => {
586
+ const h = await makeHarness()
587
+ await writePlugin(h, "ok", `export default { name: "ok", setup() {} }`)
588
+ await h.host.loadAll()
589
+
590
+ h.events.on("plugin/unloaded", name => void marks().push(`event:${name}`), "test")
591
+ await h.host.unload("ok")
592
+ await new Promise(resolve => setTimeout(resolve, 10))
593
+
594
+ expect(marks()).toContain("event:ok")
595
+ })
596
+ })
597
+
598
+ describe("PluginHost 热重载", () => {
599
+ it("重载会重新读磁盘上的代码", async () => {
600
+ const h = await makeHarness()
601
+ await writePlugin(h, "hot", `export default { name: "hot", setup(ctx) { ctx.command("#v1") } }`)
602
+ await h.host.loadAll()
603
+ expect(h.recorded.commands.map(c => c.patterns[0])).toEqual(["#v1"])
604
+
605
+ await writePlugin(h, "hot", `export default { name: "hot", setup(ctx) { ctx.command("#v2") } }`)
606
+ expect(await h.host.reload("hot")).toBe(true)
607
+
608
+ expect(h.recorded.commands.map(c => c.patterns[0])).toEqual(["#v2"])
609
+ // 旧模块留在 Node 的 ESM 缓存里出不来,这条提示必须给
610
+ expect(h.logger.lines.some(l => l.includes("累积内存"))).toBe(true)
611
+ })
612
+
613
+ it("声明了配置的插件可以反复重载(配置声明要跟着解除)", async () => {
614
+ const h = await makeHarness()
615
+ await writePlugin(
616
+ h,
617
+ "cfg",
618
+ `${BRAND}const s = globalThis.__yzngS
619
+ export default {
620
+ name: "cfg",
621
+ configSchema: s.object({ a: s.number().default(1) }),
622
+ setup() {},
623
+ [brand]: true
624
+ }`
625
+ )
626
+ await h.host.loadAll()
627
+
628
+ // 不解除声明的话,这里会以"配置 cfg 已被声明"失败
629
+ expect(await h.host.reload("cfg")).toBe(true)
630
+ expect(await h.host.reload("cfg")).toBe(true)
631
+ expect(h.host.get("cfg")).toMatchObject({ status: "loaded" })
632
+ })
633
+
634
+ it("重载未知插件返回 false 并告警", async () => {
635
+ const h = await makeHarness()
636
+ expect(await h.host.reload("ghost")).toBe(false)
637
+ expect(h.logger.lines.some(l => l.includes("没有名为 ghost"))).toBe(true)
638
+ })
639
+
640
+ it("重载后新代码坏掉时,插件停在 error 状态而不是半残", async () => {
641
+ const h = await makeHarness()
642
+ await writePlugin(h, "hot", `export default { name: "hot", setup(ctx) { ctx.command("#v1") } }`)
643
+ await h.host.loadAll()
644
+
645
+ await writePlugin(h, "hot", `export default { name: "hot", setup() { throw new Error("改坏了") } }`)
646
+ expect(await h.host.reload("hot")).toBe(false)
647
+
648
+ expect(h.host.size).toBe(0)
649
+ expect(h.host.get("hot")).toMatchObject({ status: "error" })
650
+ expect(h.recorded.commands).toEqual([])
651
+ })
652
+ })
653
+
654
+ describe("PluginHost 并发与视图", () => {
655
+ it("并发的加载与卸载被串行化,不会互相打断", async () => {
656
+ const h = await makeHarness()
657
+ await writePlugin(h, "ok", `export default { name: "ok", setup(ctx) { ctx.command("#a") } }`)
658
+
659
+ // 不 await 第一个就发第二个:串行链保证 unload 排在 loadAll 之后
660
+ const loading = h.host.loadAll()
661
+ const unloading = h.host.unload("ok")
662
+ const [report, removed] = await Promise.all([loading, unloading])
663
+
664
+ expect(report.loaded).toEqual(["ok"])
665
+ expect(removed).toBe(true)
666
+ expect(h.host.size).toBe(0)
667
+ expect(h.recorded.commands).toEqual([])
668
+ })
669
+
670
+ it("list 按名字排序,失败的插件也在列表里", async () => {
671
+ const h = await makeHarness()
672
+ await writePlugin(h, "zoo", `export default { name: "zoo", setup() {} }`)
673
+ await writePlugin(h, "bad", `export default { name: "bad", setup() { throw new Error("x") } }`)
674
+ await h.host.loadAll()
675
+
676
+ expect(h.host.list().map(p => `${p.name}:${p.status}`)).toEqual(["bad:error", "zoo:loaded"])
677
+ })
678
+
679
+ it("createPluginsView 把插件状态与命令/任务清单拼在一起", async () => {
680
+ const h = await makeHarness()
681
+ await writePlugin(h, "ok", `export default { name: "ok", setup() {} }`)
682
+ await h.host.loadAll()
683
+
684
+ const view = createPluginsView(h.host, {
685
+ commands: () => [{ plugin: "ok", pattern: "#a" }] as never,
686
+ tasks: () => [],
687
+ middlewares: () => []
688
+ })
689
+
690
+ expect(view.list()).toHaveLength(1)
691
+ expect(view.get("ok")).toMatchObject({ name: "ok" })
692
+ expect(view.get("ghost")).toBeUndefined()
693
+ expect(view.commands()).toHaveLength(1)
694
+ expect(view.tasks()).toEqual([])
695
+ })
696
+ })