@koishi-ce/plugin-config 1.0.8 → 1.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.
@@ -11,16 +11,32 @@
11
11
  * 与配置树的增删改、PackageProvider 的运行时解析缓存与按需刷新链路。
12
12
  */
13
13
 
14
- import { afterAll, beforeAll, describe, expect, it } from "bun:test";
15
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
14
+ import {
15
+ afterAll,
16
+ beforeAll,
17
+ describe,
18
+ expect,
19
+ it,
20
+ } from "bun:test";
21
+ import {
22
+ mkdirSync,
23
+ mkdtempSync,
24
+ rmSync,
25
+ writeFileSync,
26
+ } from "node:fs";
16
27
  import type { IncomingMessage } from "node:http";
17
28
  import { tmpdir } from "node:os";
18
29
  import { join } from "node:path";
19
- import { type Client, Console, type Entry } from "@koishi-ce/console";
30
+ import {
31
+ type Client,
32
+ Console,
33
+ type Entry,
34
+ } from "@koishi-ce/console";
20
35
  import {
21
36
  App,
22
37
  type Context,
23
38
  type Dict,
39
+ Logger,
24
40
  type Plugin,
25
41
  type Universal,
26
42
  } from "@koishi-ce/koishi";
@@ -31,28 +47,44 @@ import * as configPlugin from "@koishi-ce/plugin-config";
31
47
  /** 出站消息形状 */
32
48
  interface SentMessage {
33
49
  type: string;
34
- body: { id?: number; key?: string; value?: unknown; error?: string };
50
+ body: {
51
+ id?: number;
52
+ key?: string;
53
+ value?: unknown;
54
+ error?: string;
55
+ };
35
56
  }
36
57
 
37
58
  /** 内存 WebSocket 桩 */
38
59
  class FakeSocket {
39
60
  sent: string[] = [];
40
61
  // message 与 close 的监听器统一为同构签名(never 载荷),保证集合存取类型一致
41
- private messageHandlers = new Set<(event: never) => void>();
62
+ private messageHandlers = new Set<
63
+ (event: never) => void
64
+ >();
42
65
  private closeHandlers = new Set<(event: never) => void>();
43
66
 
44
67
  send(data: string) {
45
68
  this.sent.push(data);
46
69
  }
47
70
 
48
- addEventListener(type: string, listener: (event: never) => void) {
49
- if (type === "message") this.messageHandlers.add(listener);
71
+ addEventListener(
72
+ type: string,
73
+ listener: (event: never) => void,
74
+ ) {
75
+ if (type === "message")
76
+ this.messageHandlers.add(listener);
50
77
  if (type === "close") this.closeHandlers.add(listener);
51
78
  }
52
79
 
53
- removeEventListener(type: string, listener: (event: never) => void) {
54
- if (type === "message") this.messageHandlers.delete(listener);
55
- if (type === "close") this.closeHandlers.delete(listener);
80
+ removeEventListener(
81
+ type: string,
82
+ listener: (event: never) => void,
83
+ ) {
84
+ if (type === "message")
85
+ this.messageHandlers.delete(listener);
86
+ if (type === "close")
87
+ this.closeHandlers.delete(listener);
56
88
  }
57
89
 
58
90
  get socket(): Universal.WebSocket {
@@ -71,19 +103,49 @@ function tick(ms = 20) {
71
103
  return new Promise((resolve) => setTimeout(resolve, ms));
72
104
  }
73
105
 
106
+ /** 声明预期触发告警(失败路径正是被测行为)的用例:执行期间静默指定日志域。 */
107
+ const itQuiet = (
108
+ domains: string[],
109
+ name: string,
110
+ fn: () => Promise<void> | void,
111
+ ) =>
112
+ it(name, async () => {
113
+ const levels = Logger.levels as Record<string, number>;
114
+ const saved = domains.map(
115
+ (d) => [d, levels[d]] as const,
116
+ );
117
+ for (const d of domains) levels[d] = 0;
118
+ try {
119
+ await fn();
120
+ } finally {
121
+ for (const [d, v] of saved) {
122
+ if (v === undefined) delete levels[d];
123
+ else levels[d] = v;
124
+ }
125
+ }
126
+ });
127
+
74
128
  /** Console 抽象基类的最小实现 */
75
129
  class TestConsole extends Console {
76
130
  resolveEntry(files: Entry.Files, key: string): string[] {
77
131
  const list =
78
- typeof files === "string" || Array.isArray(files) ? files : files.prod;
132
+ typeof files === "string" || Array.isArray(files)
133
+ ? files
134
+ : files.prod;
79
135
  return [String(list), key];
80
136
  }
81
137
 
82
- acceptClient(socket: Universal.WebSocket, request: IncomingMessage): Client {
138
+ acceptClient(
139
+ socket: Universal.WebSocket,
140
+ request: IncomingMessage,
141
+ ): Client {
83
142
  let accepted: Client | undefined;
84
- const dispose = this.ctx.on("console/connection", (client) => {
85
- accepted = client;
86
- });
143
+ const dispose = this.ctx.on(
144
+ "console/connection",
145
+ (client) => {
146
+ accepted = client;
147
+ },
148
+ );
87
149
  this.accept(socket, request);
88
150
  dispose();
89
151
  if (!accepted) throw new Error("client not accepted");
@@ -96,6 +158,8 @@ class TestLoader extends Loader {
96
158
  data: Dict<unknown> = Object.create(null);
97
159
  writes: { filename: string; config: unknown }[] = [];
98
160
  fullReloadCount = 0;
161
+ /** 每个名字被 import 的次数(断言失败缓存不再重复解析) */
162
+ importCounts: Dict<number> = Object.create(null);
99
163
 
100
164
  constructor() {
101
165
  super();
@@ -103,6 +167,8 @@ class TestLoader extends Loader {
103
167
  }
104
168
 
105
169
  override async import(name: string) {
170
+ this.importCounts[name] =
171
+ (this.importCounts[name] ?? 0) + 1;
106
172
  if (name === "bad-plugin") {
107
173
  throw new Error("cannot resolve bad-plugin");
108
174
  }
@@ -119,11 +185,15 @@ class TestLoader extends Loader {
119
185
  }
120
186
 
121
187
  protected override locateConfig(): Promise<never> {
122
- throw new Error("test loader does not touch the file system");
188
+ throw new Error(
189
+ "test loader does not touch the file system",
190
+ );
123
191
  }
124
192
 
125
193
  protected override parseConfig(): Promise<never> {
126
- throw new Error("test loader does not touch the file system");
194
+ throw new Error(
195
+ "test loader does not touch the file system",
196
+ );
127
197
  }
128
198
 
129
199
  protected override async saveConfig(
@@ -136,17 +206,34 @@ class TestLoader extends Loader {
136
206
 
137
207
  const loader = new TestLoader();
138
208
 
209
+ // loader 的 apply/reload/unload 与启动横幅是正常生命周期 info(且部分在下方顶层
210
+ // createApp 期间即产生),收敛为仅错误级;恢复见文件末 afterAll
211
+ const testLevels = Logger.levels as Record<string, number>;
212
+ testLevels["loader"] = 1;
213
+ testLevels["app"] = 1;
214
+
139
215
  /**
140
216
  * Hermetic 应用根:本机包扫描(LocalScanner)与 workspace 路径键解析均以
141
217
  * loader.baseDir 为锚,固定到临时 fixture,避免断言依赖真实仓库 node_modules
142
218
  * 的链接状态(哪些 workspace 包被链入由包管理器布局决定,换环境即挂)。
143
219
  */
144
- const fixtureRoot = mkdtempSync(join(tmpdir(), "koishi-config-pkg-"));
145
- mkdirSync(join(fixtureRoot, "node_modules/@koishi-ce/plugin-fixture"), {
146
- recursive: true,
147
- });
220
+ const fixtureRoot = mkdtempSync(
221
+ join(tmpdir(), "koishi-config-pkg-"),
222
+ );
223
+ mkdirSync(
224
+ join(
225
+ fixtureRoot,
226
+ "node_modules/@koishi-ce/plugin-fixture",
227
+ ),
228
+ {
229
+ recursive: true,
230
+ },
231
+ );
148
232
  writeFileSync(
149
- join(fixtureRoot, "node_modules/@koishi-ce/plugin-fixture/package.json"),
233
+ join(
234
+ fixtureRoot,
235
+ "node_modules/@koishi-ce/plugin-fixture/package.json",
236
+ ),
150
237
  JSON.stringify(
151
238
  {
152
239
  name: "@koishi-ce/plugin-fixture",
@@ -157,10 +244,26 @@ writeFileSync(
157
244
  "\t",
158
245
  ),
159
246
  );
160
- mkdirSync(join(fixtureRoot, "plugins/webui/auth"), { recursive: true });
247
+ mkdirSync(join(fixtureRoot, "plugins/webui/auth"), {
248
+ recursive: true,
249
+ });
161
250
  writeFileSync(
162
251
  join(fixtureRoot, "plugins/webui/auth/package.json"),
163
- JSON.stringify({ name: "@koishi-ce/plugin-auth", version: "1.0.0" }),
252
+ JSON.stringify({
253
+ name: "@koishi-ce/plugin-auth",
254
+ version: "1.0.0",
255
+ }),
256
+ );
257
+ // 配置树里的路径键 ./missing-pkg 也应真实存在,否则包扫描每轮都刷 ENOENT 告警
258
+ mkdirSync(join(fixtureRoot, "missing-pkg"), {
259
+ recursive: true,
260
+ });
261
+ writeFileSync(
262
+ join(fixtureRoot, "missing-pkg/package.json"),
263
+ JSON.stringify({
264
+ name: "koishi-plugin-missing-pkg",
265
+ version: "1.0.0",
266
+ }),
164
267
  );
165
268
  loader.baseDir = fixtureRoot;
166
269
 
@@ -182,17 +285,22 @@ loader.config = {
182
285
 
183
286
  const app = await loader.createApp();
184
287
  // Console 基类的 static inject 是 cordis 3 旧形态,与 Plugin.Constructor 期待类型不兼容,仅做类型层转型
185
- app.plugin(TestConsole as unknown as Plugin.Constructor<App>);
288
+ app.plugin(
289
+ TestConsole as unknown as Plugin.Constructor<App>,
290
+ );
186
291
  app.plugin(configPlugin);
187
292
 
188
293
  const service = () => app.console as TestConsole;
189
- const writer = () => app.get("console.services.config") as ConfigWriter;
294
+ const writer = () =>
295
+ app.get("console.services.config") as ConfigWriter;
190
296
  let socket: FakeSocket;
191
297
  let client: Client;
192
298
 
193
299
  /** 读取客户端已收到的全部消息 */
194
300
  function readSent(): SentMessage[] {
195
- return socket.sent.map((line) => JSON.parse(line) as SentMessage);
301
+ return socket.sent.map(
302
+ (line) => JSON.parse(line) as SentMessage,
303
+ );
196
304
  }
197
305
 
198
306
  /** 等待写盘队列(合并窗口 setTimeout 0)落定 */
@@ -203,7 +311,10 @@ async function flushWrites() {
203
311
  beforeAll(async () => {
204
312
  await app.start();
205
313
  socket = new FakeSocket();
206
- client = service().acceptClient(socket.socket, fakeRequest());
314
+ client = service().acceptClient(
315
+ socket.socket,
316
+ fakeRequest(),
317
+ );
207
318
  await tick();
208
319
  socket.sent.length = 0;
209
320
  });
@@ -215,30 +326,52 @@ afterAll(async () => {
215
326
  await tick();
216
327
  await app.stop();
217
328
  rmSync(fixtureRoot, { recursive: true, force: true });
329
+ // 恢复域级阈值,避免同进程后续测试文件被连带静默
330
+ delete testLevels["loader"];
331
+ delete testLevels["app"];
218
332
  });
219
333
 
220
334
  describe("@koishi-ce/plugin-config", () => {
221
- it("无可写 loader 时仅告警并跳过装配", async () => {
222
- const bare = new App();
223
- bare.plugin(TestConsole as unknown as Plugin.Constructor<App>);
224
- bare.plugin(configPlugin);
225
- await bare.start();
226
- expect(bare.get("console.services.config")).toBeUndefined();
227
- expect(bare.get("console.services.packages")).toBeUndefined();
228
- expect(bare.get("console.services.services")).toBeUndefined();
229
- await bare.stop();
230
- });
335
+ itQuiet(
336
+ ["app"],
337
+ "无可写 loader 时仅告警并跳过装配",
338
+ async () => {
339
+ const bare = new App();
340
+ bare.plugin(
341
+ TestConsole as unknown as Plugin.Constructor<App>,
342
+ );
343
+ bare.plugin(configPlugin);
344
+ await bare.start();
345
+ expect(
346
+ bare.get("console.services.config"),
347
+ ).toBeUndefined();
348
+ expect(
349
+ bare.get("console.services.packages"),
350
+ ).toBeUndefined();
351
+ expect(
352
+ bare.get("console.services.services"),
353
+ ).toBeUndefined();
354
+ await bare.stop();
355
+ },
356
+ );
231
357
 
232
358
  it("挂载 packages / services / config 三个数据服务", () => {
233
- expect(app.get("console.services.packages")).toBeTruthy();
234
- expect(app.get("console.services.services")).toBeTruthy();
359
+ expect(
360
+ app.get("console.services.packages"),
361
+ ).toBeTruthy();
362
+ expect(
363
+ app.get("console.services.services"),
364
+ ).toBeTruthy();
235
365
  expect(app.get("console.services.config")).toBeTruthy();
236
366
  });
237
367
 
238
368
  describe("ConfigWriter.get()", () => {
239
369
  it("过滤未加载项,分组递归展开,$ 与 ~ 键原样保留", async () => {
240
370
  const result = await writer().get();
241
- const plugins = result.plugins as Record<string, unknown>;
371
+ const plugins = result.plugins as Record<
372
+ string,
373
+ unknown
374
+ >;
242
375
  // $if 为假的键不出现;$ 内部键与 ~ 停用键保留
243
376
  expect("gone:xyz" in plugins).toBe(false);
244
377
  expect(plugins["$sfolded"]).toBe(true);
@@ -255,7 +388,9 @@ describe("@koishi-ce/plugin-config", () => {
255
388
 
256
389
  describe("PackageProvider", () => {
257
390
  it("收集本机包与 workspace 源码包并附全局设置条目", async () => {
258
- const provider = app.get("console.services.packages") as unknown as {
391
+ const provider = app.get(
392
+ "console.services.packages",
393
+ ) as unknown as {
259
394
  get(): Promise<Dict<Record<string, unknown>>>;
260
395
  };
261
396
  const data = await provider.get();
@@ -263,30 +398,68 @@ describe("@koishi-ce/plugin-config", () => {
263
398
  expect(data[""]).toBeTruthy();
264
399
  // workspace 路径键引用的源码包:带 paths 与运行时缓存
265
400
  const auth = data["@koishi-ce/plugin-auth"];
266
- expect(auth?.["paths"]).toEqual(["./plugins/webui/auth"]);
401
+ expect(auth?.["paths"]).toEqual([
402
+ "./plugins/webui/auth",
403
+ ]);
267
404
  expect(auth?.["runtime"]).toBeTruthy();
268
405
  // 本机 node_modules 扫描出的已装插件包(fixture 预置,见文件头说明)
269
- expect(data["@koishi-ce/plugin-fixture"]).toBeTruthy();
406
+ expect(
407
+ data["@koishi-ce/plugin-fixture"],
408
+ ).toBeTruthy();
270
409
  });
271
410
 
272
- it("request-runtime 按路径键 / 短名解析并刷新,解析失败不缓存", async () => {
273
- const listener = app.console.listeners["config/request-runtime"];
274
- expect(listener).toBeTruthy();
275
- // 失败路径:stub 的 bad-plugin 抛错,返回 { failed: true } 不入缓存
276
- await listener?.callback.call(client as never, "bad-plugin");
277
- await flushWrites();
278
- // 成功路径:workspace 包名命中 pathKeys
279
- await listener?.callback.call(client as never, "@koishi-ce/plugin-auth");
280
- await flushWrites();
281
- const provider = app.get("console.services.packages") as unknown as {
282
- cache: Dict<unknown>;
283
- pathKeys: Dict<string>;
284
- };
285
- expect(provider.pathKeys["@koishi-ce/plugin-auth"]).toBe(
286
- "./plugins/webui/auth",
287
- );
288
- expect(provider.cache["./plugins/webui/auth"]).toBeTruthy();
289
- });
411
+ itQuiet(
412
+ ["config"],
413
+ "request-runtime 按路径键 / 短名解析并刷新,失败结果同样缓存",
414
+ async () => {
415
+ const listener =
416
+ app.console.listeners["config/request-runtime"];
417
+ expect(listener).toBeTruthy();
418
+ const provider = app.get(
419
+ "console.services.packages",
420
+ ) as unknown as {
421
+ cache: Dict<{ failed?: boolean }>;
422
+ pathKeys: Dict<string>;
423
+ };
424
+ // 失败路径:stub 的 bad-plugin 抛错,{ failed: true } 入缓存并
425
+ // 随数据下发——前端据以展示失败提示并停止重发请求
426
+ await listener?.callback.call(
427
+ client as never,
428
+ "bad-plugin",
429
+ );
430
+ await flushWrites();
431
+ expect(provider.cache["bad-plugin"]).toEqual({
432
+ failed: true,
433
+ });
434
+ // 重复请求命中失败缓存,不再触发 loader.import(防活锁刷屏)
435
+ const importsAfterFirst =
436
+ loader.importCounts["bad-plugin"] ?? 0;
437
+ expect(importsAfterFirst).toBeGreaterThan(0);
438
+ await listener?.callback.call(
439
+ client as never,
440
+ "bad-plugin",
441
+ );
442
+ await flushWrites();
443
+ expect(loader.importCounts["bad-plugin"] ?? 0).toBe(
444
+ importsAfterFirst,
445
+ );
446
+ // 成功路径:workspace 包名命中 pathKeys
447
+ await listener?.callback.call(
448
+ client as never,
449
+ "@koishi-ce/plugin-auth",
450
+ );
451
+ await flushWrites();
452
+ expect(
453
+ provider.pathKeys["@koishi-ce/plugin-auth"],
454
+ ).toBe("./plugins/webui/auth");
455
+ expect(
456
+ provider.cache["./plugins/webui/auth"],
457
+ ).toBeTruthy();
458
+ expect(
459
+ provider.cache["./plugins/webui/auth"]?.failed,
460
+ ).toBeUndefined();
461
+ },
462
+ );
290
463
 
291
464
  it("internal/runtime / fork / status 与 hmr/reload 触发运行时更新", async () => {
292
465
  const dummy = loader.data["keep"] as Plugin;
@@ -303,7 +476,11 @@ describe("@koishi-ce/plugin-config", () => {
303
476
  app.emit("internal/fork", fork as never);
304
477
  // internal/status 签名为 (scope, oldValue) 双参,仓库内监听者均不消费第二参,
305
478
  // 补 undefined 占位满足调用形状且不改变运行时行为
306
- app.emit("internal/status", fork as never, undefined as never);
479
+ app.emit(
480
+ "internal/status",
481
+ fork as never,
482
+ undefined as never,
483
+ );
307
484
  app.emit("hmr/reload", [[dummy]] as never);
308
485
  await tick(30);
309
486
  expect(true).toBe(true);
@@ -312,17 +489,24 @@ describe("@koishi-ce/plugin-config", () => {
312
489
 
313
490
  describe("ServiceProvider", () => {
314
491
  it("上报各服务的提供者上下文并在服务变动时刷新", async () => {
315
- const provider = app.get("console.services.services") as unknown as {
492
+ const provider = app.get(
493
+ "console.services.services",
494
+ ) as unknown as {
316
495
  get(): Promise<Dict<number>>;
317
496
  };
318
497
  const data = await provider.get();
319
498
  expect(data["console"]).toBeGreaterThanOrEqual(0);
320
499
  // 服务注册(set 会广播 internal/service)→ refresh → 客户端收到 services 数据
321
500
  socket.sent.length = 0;
322
- app.set("probe.svc" as never, { marker: true } as never);
501
+ app.set(
502
+ "probe.svc" as never,
503
+ { marker: true } as never,
504
+ );
323
505
  await tick(30);
324
506
  const messages = readSent().filter(
325
- (msg) => msg.type === "data" && msg.body.key === "services",
507
+ (msg) =>
508
+ msg.type === "data" &&
509
+ msg.body.key === "services",
326
510
  );
327
511
  expect(messages.length).toBeGreaterThan(0);
328
512
  });
@@ -330,24 +514,37 @@ describe("@koishi-ce/plugin-config", () => {
330
514
 
331
515
  describe("ConfigWriter 事件", () => {
332
516
  it("manager/meta 更新元数据键(含 null 删除)", async () => {
333
- const listener = app.console.listeners["manager/meta"];
334
- await listener?.callback.call(client as never, "abc", {
335
- $label: "L",
336
- $collapsed: null,
337
- });
517
+ const listener =
518
+ app.console.listeners["manager/meta"];
519
+ await listener?.callback.call(
520
+ client as never,
521
+ "abc",
522
+ {
523
+ $label: "L",
524
+ $collapsed: null,
525
+ },
526
+ );
338
527
  await flushWrites();
339
528
  const plugins = loader.config.plugins as Record<
340
529
  string,
341
530
  Record<string, unknown>
342
531
  >;
343
532
  expect(plugins["keep:abc"]?.["$label"]).toBe("L");
344
- expect("$collapsed" in (plugins["keep:abc"] ?? {})).toBe(false);
533
+ expect(
534
+ "$collapsed" in (plugins["keep:abc"] ?? {}),
535
+ ).toBe(false);
345
536
  // 元数据键置于配置开头
346
- expect(Object.keys(plugins["keep:abc"] ?? {})[0]).toBe("$label");
537
+ expect(
538
+ Object.keys(plugins["keep:abc"] ?? {})[0],
539
+ ).toBe("$label");
347
540
  // 分组内插件的 meta(按 ident 递归定位)
348
- await listener?.callback.call(client as never, "two", {
349
- $label: "G",
350
- });
541
+ await listener?.callback.call(
542
+ client as never,
543
+ "two",
544
+ {
545
+ $label: "G",
546
+ },
547
+ );
351
548
  await flushWrites();
352
549
  const group = plugins["group:g1"] as Record<
353
550
  string,
@@ -357,85 +554,164 @@ describe("@koishi-ce/plugin-config", () => {
357
554
  });
358
555
 
359
556
  it("manager/reload 更新插件配置并保持键位置", async () => {
360
- const listener = app.console.listeners["manager/reload"];
361
- await listener?.callback.call(client as never, "", "keep:abc", {
362
- v: 2,
363
- });
557
+ const listener =
558
+ app.console.listeners["manager/reload"];
559
+ await listener?.callback.call(
560
+ client as never,
561
+ "",
562
+ "keep:abc",
563
+ {
564
+ v: 2,
565
+ },
566
+ );
364
567
  await flushWrites();
365
- const plugins = loader.config.plugins as Record<string, unknown>;
568
+ const plugins = loader.config.plugins as Record<
569
+ string,
570
+ unknown
571
+ >;
366
572
  expect(plugins["keep:abc"]).toEqual({ v: 2 });
367
573
  const dummy = loader.data["keep"] as Plugin;
368
- expect(app.registry.get(dummy)?.config).toEqual({ v: 2 });
574
+ expect(app.registry.get(dummy)?.config).toEqual({
575
+ v: 2,
576
+ });
369
577
  // 运行期更新触发回写
370
578
  expect(loader.writes.length).toBeGreaterThan(0);
371
579
  });
372
580
 
373
581
  it("manager/unload 停用插件(原位改 ~ 与按位置插入)", async () => {
374
- const listener = app.console.listeners["manager/unload"];
582
+ const listener =
583
+ app.console.listeners["manager/unload"];
375
584
  // 原位重命名
376
- await listener?.callback.call(client as never, "", "keep:abc", {
377
- v: 3,
378
- });
585
+ await listener?.callback.call(
586
+ client as never,
587
+ "",
588
+ "keep:abc",
589
+ {
590
+ v: 3,
591
+ },
592
+ );
379
593
  await flushWrites();
380
- const plugins = loader.config.plugins as Record<string, unknown>;
594
+ const plugins = loader.config.plugins as Record<
595
+ string,
596
+ unknown
597
+ >;
381
598
  expect("keep:abc" in plugins).toBe(false);
382
599
  expect(plugins["~keep:abc"]).toEqual({ v: 3 });
383
600
  const dummy = loader.data["keep"] as Plugin;
384
601
  expect(app.registry.get(dummy)).toBeUndefined();
385
602
 
386
603
  // 先补一个可停用键,再按 index 插入到指定位置
387
- const reload = app.console.listeners["manager/reload"];
388
- await reload?.callback.call(client as never, "", "back:two", {
389
- b: 1,
390
- });
604
+ const reload =
605
+ app.console.listeners["manager/reload"];
606
+ await reload?.callback.call(
607
+ client as never,
608
+ "",
609
+ "back:two",
610
+ {
611
+ b: 1,
612
+ },
613
+ );
391
614
  await flushWrites();
392
- await listener?.callback.call(client as never, "", "back:two", {}, 1);
615
+ await listener?.callback.call(
616
+ client as never,
617
+ "",
618
+ "back:two",
619
+ {},
620
+ 1,
621
+ );
393
622
  await flushWrites();
394
- const keys = Object.keys(loader.config.plugins as object);
623
+ const keys = Object.keys(
624
+ loader.config.plugins as object,
625
+ );
395
626
  expect(keys.indexOf("~back:two")).toBe(1);
396
627
  });
397
628
 
398
629
  it("manager/remove 彻底移除插件(含 ~ 停用态键)", async () => {
399
- const listener = app.console.listeners["manager/remove"];
400
- await listener?.callback.call(client as never, "", "~keep:abc");
630
+ const listener =
631
+ app.console.listeners["manager/remove"];
632
+ await listener?.callback.call(
633
+ client as never,
634
+ "",
635
+ "~keep:abc",
636
+ );
401
637
  await flushWrites();
402
- const plugins = loader.config.plugins as Record<string, unknown>;
638
+ const plugins = loader.config.plugins as Record<
639
+ string,
640
+ unknown
641
+ >;
403
642
  expect("keep:abc" in plugins).toBe(false);
404
643
  expect("~keep:abc" in plugins).toBe(false);
405
- await listener?.callback.call(client as never, "", "pos:one");
644
+ await listener?.callback.call(
645
+ client as never,
646
+ "",
647
+ "pos:one",
648
+ );
406
649
  await flushWrites();
407
650
  expect("pos:one" in plugins).toBe(false);
408
651
  });
409
652
 
410
653
  it("manager/teleport 跨分组迁移与同分组重排", async () => {
411
- const listener = app.console.listeners["manager/teleport"];
412
- const plugins = loader.config.plugins as Record<string, unknown>;
654
+ const listener =
655
+ app.console.listeners["manager/teleport"];
656
+ const plugins = loader.config.plugins as Record<
657
+ string,
658
+ unknown
659
+ >;
413
660
  // 跨分组:group:g1 → 根(index 0)
414
- await listener?.callback.call(client as never, "g1", "mov:me", "", 0);
661
+ await listener?.callback.call(
662
+ client as never,
663
+ "g1",
664
+ "mov:me",
665
+ "",
666
+ 0,
667
+ );
415
668
  await flushWrites();
416
669
  expect(plugins["mov:me"]).toEqual({ m: 1 });
417
- const group = plugins["group:g1"] as Record<string, unknown>;
670
+ const group = plugins["group:g1"] as Record<
671
+ string,
672
+ unknown
673
+ >;
418
674
  expect("mov:me" in group).toBe(false);
419
675
  // 运行时 fork 已改挂根作用域
420
676
  const dummy = loader.data["mov"] as Plugin;
421
677
  const fork = app.registry.get(dummy)?.children[0];
422
- expect(fork?.parent.scope.uid).toBe(loader.entry.scope.uid);
678
+ expect(fork?.parent.scope.uid).toBe(
679
+ loader.entry.scope.uid,
680
+ );
423
681
 
424
682
  // 同分组:仅重排键位置
425
- await listener?.callback.call(client as never, "", "mov:me", "", 0);
683
+ await listener?.callback.call(
684
+ client as never,
685
+ "",
686
+ "mov:me",
687
+ "",
688
+ 0,
689
+ );
426
690
  await flushWrites();
427
691
  expect(Object.keys(plugins)[0]).toBe("mov:me");
428
692
  });
429
693
 
430
- it("manager/* 失败统一回执 failed", async () => {
431
- const listener = app.console.listeners["manager/teleport"];
432
- await expect(
433
- listener?.callback.call(client as never, "no-such-group", "k", "", 0),
434
- ).rejects.toThrow("failed");
435
- });
694
+ itQuiet(
695
+ ["loader"],
696
+ "manager/* 失败统一回执 failed",
697
+ async () => {
698
+ const listener =
699
+ app.console.listeners["manager/teleport"];
700
+ await expect(
701
+ listener?.callback.call(
702
+ client as never,
703
+ "no-such-group",
704
+ "k",
705
+ "",
706
+ 0,
707
+ ),
708
+ ).rejects.toThrow("failed");
709
+ },
710
+ );
436
711
 
437
712
  it("manager/app-reload 替换全局配置并整进程重载", async () => {
438
- const listener = app.console.listeners["manager/app-reload"];
713
+ const listener =
714
+ app.console.listeners["manager/app-reload"];
439
715
  const writesBefore = loader.writes.length;
440
716
  await listener?.callback.call(client as never, {
441
717
  prefix: ["."],
@@ -444,11 +720,14 @@ describe("@koishi-ce/plugin-config", () => {
444
720
  expect(loader.config.prefix).toEqual(["."]);
445
721
  // plugins 部分被保留
446
722
  expect(loader.config.plugins).toBeTruthy();
447
- expect(loader.writes.length).toBeGreaterThan(writesBefore);
723
+ expect(loader.writes.length).toBeGreaterThan(
724
+ writesBefore,
725
+ );
448
726
  expect(loader.fullReloadCount).toBe(1);
449
727
  // 非静默写盘触发 config 事件 → config 服务刷新推送
450
728
  const messages = readSent().filter(
451
- (msg) => msg.type === "data" && msg.body.key === "config",
729
+ (msg) =>
730
+ msg.type === "data" && msg.body.key === "config",
452
731
  );
453
732
  expect(messages.length).toBeGreaterThan(0);
454
733
  });