@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.
- package/dist/index.js +3 -3
- package/lib/index.d.ts +23 -12
- package/lib/index.mjs +18 -10
- package/package.json +4 -4
- package/src/__tests__/index.test.ts +391 -112
- package/src/browser/index.ts +10 -2
- package/src/node/index.ts +10 -2
- package/src/node/packages.ts +22 -7
- package/src/shared/packages.ts +74 -28
- package/src/shared/services.ts +17 -8
- package/src/shared/writer.ts +88 -27
|
@@ -11,16 +11,32 @@
|
|
|
11
11
|
* 与配置树的增删改、PackageProvider 的运行时解析缓存与按需刷新链路。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
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 {
|
|
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: {
|
|
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<
|
|
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(
|
|
49
|
-
|
|
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(
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
132
|
+
typeof files === "string" || Array.isArray(files)
|
|
133
|
+
? files
|
|
134
|
+
: files.prod;
|
|
79
135
|
return [String(list), key];
|
|
80
136
|
}
|
|
81
137
|
|
|
82
|
-
acceptClient(
|
|
138
|
+
acceptClient(
|
|
139
|
+
socket: Universal.WebSocket,
|
|
140
|
+
request: IncomingMessage,
|
|
141
|
+
): Client {
|
|
83
142
|
let accepted: Client | undefined;
|
|
84
|
-
const dispose = this.ctx.on(
|
|
85
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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"), {
|
|
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({
|
|
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(
|
|
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 = () =>
|
|
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(
|
|
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(
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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(
|
|
234
|
-
|
|
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<
|
|
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(
|
|
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([
|
|
401
|
+
expect(auth?.["paths"]).toEqual([
|
|
402
|
+
"./plugins/webui/auth",
|
|
403
|
+
]);
|
|
267
404
|
expect(auth?.["runtime"]).toBeTruthy();
|
|
268
405
|
// 本机 node_modules 扫描出的已装插件包(fixture 预置,见文件头说明)
|
|
269
|
-
expect(
|
|
406
|
+
expect(
|
|
407
|
+
data["@koishi-ce/plugin-fixture"],
|
|
408
|
+
).toBeTruthy();
|
|
270
409
|
});
|
|
271
410
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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) =>
|
|
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 =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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(
|
|
533
|
+
expect(
|
|
534
|
+
"$collapsed" in (plugins["keep:abc"] ?? {}),
|
|
535
|
+
).toBe(false);
|
|
345
536
|
// 元数据键置于配置开头
|
|
346
|
-
expect(
|
|
537
|
+
expect(
|
|
538
|
+
Object.keys(plugins["keep:abc"] ?? {})[0],
|
|
539
|
+
).toBe("$label");
|
|
347
540
|
// 分组内插件的 meta(按 ident 递归定位)
|
|
348
|
-
await listener?.callback.call(
|
|
349
|
-
|
|
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 =
|
|
361
|
-
|
|
362
|
-
|
|
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<
|
|
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({
|
|
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 =
|
|
582
|
+
const listener =
|
|
583
|
+
app.console.listeners["manager/unload"];
|
|
375
584
|
// 原位重命名
|
|
376
|
-
await listener?.callback.call(
|
|
377
|
-
|
|
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<
|
|
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 =
|
|
388
|
-
|
|
389
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
400
|
-
|
|
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<
|
|
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(
|
|
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 =
|
|
412
|
-
|
|
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(
|
|
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<
|
|
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(
|
|
678
|
+
expect(fork?.parent.scope.uid).toBe(
|
|
679
|
+
loader.entry.scope.uid,
|
|
680
|
+
);
|
|
423
681
|
|
|
424
682
|
// 同分组:仅重排键位置
|
|
425
|
-
await listener?.callback.call(
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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 =
|
|
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(
|
|
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) =>
|
|
729
|
+
(msg) =>
|
|
730
|
+
msg.type === "data" && msg.body.key === "config",
|
|
452
731
|
);
|
|
453
732
|
expect(messages.length).toBeGreaterThan(0);
|
|
454
733
|
});
|