@koishi-ce/plugin-config 1.1.4 → 1.1.6

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.
@@ -0,0 +1,105 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2019-present Shigma and Koishijs contributors.
3
+ // Copyright (c) 2026-present Koishi-CE contributors.
4
+
5
+ import {
6
+ afterAll,
7
+ beforeAll,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from "bun:test";
12
+ import {
13
+ App,
14
+ type Dict,
15
+ type Plugin,
16
+ } from "@koishi-ce/koishi";
17
+ import * as configPlugin from "@koishi-ce/plugin-config";
18
+ import {
19
+ app,
20
+ itQuiet,
21
+ readSent,
22
+ socket,
23
+ startApp,
24
+ stopApp,
25
+ TestConsole,
26
+ tick,
27
+ } from "./helpers.ts";
28
+
29
+ beforeAll(startApp);
30
+ afterAll(stopApp);
31
+
32
+ describe("@koishi-ce/plugin-config", () => {
33
+ itQuiet(
34
+ ["app"],
35
+ "无可写 loader 时仅告警并跳过装配",
36
+ async () => {
37
+ const bare = new App();
38
+ bare.plugin(
39
+ TestConsole as unknown as Plugin.Constructor<App>,
40
+ );
41
+ bare.plugin(configPlugin);
42
+ await bare.start();
43
+ expect(
44
+ bare.get("console.services.config"),
45
+ ).toBeUndefined();
46
+ expect(
47
+ bare.get("console.services.packages"),
48
+ ).toBeUndefined();
49
+ expect(
50
+ bare.get("console.services.services"),
51
+ ).toBeUndefined();
52
+ await bare.stop();
53
+ },
54
+ );
55
+
56
+ it("挂载 packages / services / config 三个数据服务", () => {
57
+ expect(
58
+ app.get("console.services.packages"),
59
+ ).toBeTruthy();
60
+ expect(
61
+ app.get("console.services.services"),
62
+ ).toBeTruthy();
63
+ expect(app.get("console.services.config")).toBeTruthy();
64
+ });
65
+
66
+ describe("ServiceProvider", () => {
67
+ it("上报各服务的提供者上下文并在服务变动时刷新", async () => {
68
+ const provider = app.get(
69
+ "console.services.services",
70
+ ) as {
71
+ get(): Promise<Dict<number>>;
72
+ };
73
+ const data = await provider.get();
74
+ expect(data["console"]).toBeGreaterThanOrEqual(0);
75
+ // 服务注册(set 会广播 internal/service)→ refresh → 客户端收到 services 数据
76
+ socket.sent.length = 0;
77
+ app.set(
78
+ "probe.svc" as never,
79
+ { marker: true } as never,
80
+ );
81
+ await tick(30);
82
+ const messages = readSent().filter(
83
+ (msg) =>
84
+ msg.type === "data" &&
85
+ msg.body.key === "services",
86
+ );
87
+ expect(messages.length).toBeGreaterThan(0);
88
+ });
89
+
90
+ it("provide() 注册的服务同样上报(loader / watcher 形态)", async () => {
91
+ const provider = app.get(
92
+ "console.services.services",
93
+ ) as {
94
+ get(): Promise<Dict<number>>;
95
+ };
96
+ // cordis 3.18 的 ctx.provide() 不给值定义自有 "ctx" 属性
97
+ // (只有 tracker 符号,经 traceable 代理的属性访问可达),
98
+ // descriptor 查询落空时须以属性访问兜底,否则配置页
99
+ // 恒显示「必需服务未加载」
100
+ app.provide("probe.plain", { marker: 1 });
101
+ const data = await provider.get();
102
+ expect(data["probe.plain"]).toBeGreaterThanOrEqual(0);
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,270 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2019-present Shigma and Koishijs contributors.
3
+ // Copyright (c) 2026-present Koishi-CE contributors.
4
+
5
+ import {
6
+ afterAll,
7
+ beforeAll,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from "bun:test";
12
+ import type { Plugin } from "@koishi-ce/koishi";
13
+ import {
14
+ app,
15
+ client,
16
+ flushWrites,
17
+ itQuiet,
18
+ loader,
19
+ readSent,
20
+ startApp,
21
+ stopApp,
22
+ writer,
23
+ } from "./helpers.ts";
24
+
25
+ beforeAll(startApp);
26
+ afterAll(stopApp);
27
+
28
+ describe("@koishi-ce/plugin-config", () => {
29
+ describe("ConfigWriter.get()", () => {
30
+ it("过滤未加载项,分组递归展开,$ 与 ~ 键原样保留", async () => {
31
+ const result = await writer().get();
32
+ const plugins = result.plugins as Record<
33
+ string,
34
+ unknown
35
+ >;
36
+ // $if 为假的键不出现;$ 内部键与 ~ 停用键保留
37
+ expect("gone:xyz" in plugins).toBe(false);
38
+ expect(plugins["$sfolded"]).toBe(true);
39
+ expect(plugins["~disabled:q"]).toEqual({ a: 1 });
40
+ expect(plugins["keep:abc"]).toEqual({ v: 1 });
41
+ expect(plugins["group:g1"]).toEqual({
42
+ "./plugins/webui/auth": {},
43
+ "./missing-pkg": {},
44
+ "inner:two": { t: 2 },
45
+ "mov:me": { m: 1 },
46
+ });
47
+ });
48
+ });
49
+
50
+ describe("ConfigWriter 事件", () => {
51
+ it("manager/meta 更新元数据键(含 null 删除)", async () => {
52
+ const listener =
53
+ app.console.listeners["manager/meta"];
54
+ await listener?.callback.call(
55
+ client as never,
56
+ "abc",
57
+ {
58
+ $label: "L",
59
+ $collapsed: null,
60
+ },
61
+ );
62
+ await flushWrites();
63
+ const plugins = loader.config.plugins as Record<
64
+ string,
65
+ Record<string, unknown>
66
+ >;
67
+ expect(plugins["keep:abc"]?.["$label"]).toBe("L");
68
+ expect(
69
+ "$collapsed" in (plugins["keep:abc"] ?? {}),
70
+ ).toBe(false);
71
+ // 元数据键置于配置开头
72
+ expect(
73
+ Object.keys(plugins["keep:abc"] ?? {})[0],
74
+ ).toBe("$label");
75
+ // 分组内插件的 meta(按 ident 递归定位)
76
+ await listener?.callback.call(
77
+ client as never,
78
+ "two",
79
+ {
80
+ $label: "G",
81
+ },
82
+ );
83
+ await flushWrites();
84
+ const group = plugins["group:g1"] as Record<
85
+ string,
86
+ Record<string, unknown>
87
+ >;
88
+ expect(group["inner:two"]?.["$label"]).toBe("G");
89
+ });
90
+
91
+ it("manager/reload 更新插件配置并保持键位置", async () => {
92
+ const listener =
93
+ app.console.listeners["manager/reload"];
94
+ await listener?.callback.call(
95
+ client as never,
96
+ "",
97
+ "keep:abc",
98
+ {
99
+ v: 2,
100
+ },
101
+ );
102
+ await flushWrites();
103
+ const plugins = loader.config.plugins as Record<
104
+ string,
105
+ unknown
106
+ >;
107
+ expect(plugins["keep:abc"]).toEqual({ v: 2 });
108
+ const dummy = loader.data["keep"] as Plugin;
109
+ expect(app.registry.get(dummy)?.config).toEqual({
110
+ v: 2,
111
+ });
112
+ // 运行期更新触发回写
113
+ expect(loader.writes.length).toBeGreaterThan(0);
114
+ });
115
+
116
+ it("manager/unload 停用插件(原位改 ~ 与按位置插入)", async () => {
117
+ const listener =
118
+ app.console.listeners["manager/unload"];
119
+ // 原位重命名
120
+ await listener?.callback.call(
121
+ client as never,
122
+ "",
123
+ "keep:abc",
124
+ {
125
+ v: 3,
126
+ },
127
+ );
128
+ await flushWrites();
129
+ const plugins = loader.config.plugins as Record<
130
+ string,
131
+ unknown
132
+ >;
133
+ expect("keep:abc" in plugins).toBe(false);
134
+ expect(plugins["~keep:abc"]).toEqual({ v: 3 });
135
+ const dummy = loader.data["keep"] as Plugin;
136
+ expect(app.registry.get(dummy)).toBeUndefined();
137
+
138
+ // 先补一个可停用键,再按 index 插入到指定位置
139
+ const reload =
140
+ app.console.listeners["manager/reload"];
141
+ await reload?.callback.call(
142
+ client as never,
143
+ "",
144
+ "back:two",
145
+ {
146
+ b: 1,
147
+ },
148
+ );
149
+ await flushWrites();
150
+ await listener?.callback.call(
151
+ client as never,
152
+ "",
153
+ "back:two",
154
+ {},
155
+ 1,
156
+ );
157
+ await flushWrites();
158
+ const keys = Object.keys(
159
+ loader.config.plugins as object,
160
+ );
161
+ expect(keys.indexOf("~back:two")).toBe(1);
162
+ });
163
+
164
+ it("manager/remove 彻底移除插件(含 ~ 停用态键)", async () => {
165
+ const listener =
166
+ app.console.listeners["manager/remove"];
167
+ await listener?.callback.call(
168
+ client as never,
169
+ "",
170
+ "~keep:abc",
171
+ );
172
+ await flushWrites();
173
+ const plugins = loader.config.plugins as Record<
174
+ string,
175
+ unknown
176
+ >;
177
+ expect("keep:abc" in plugins).toBe(false);
178
+ expect("~keep:abc" in plugins).toBe(false);
179
+ await listener?.callback.call(
180
+ client as never,
181
+ "",
182
+ "pos:one",
183
+ );
184
+ await flushWrites();
185
+ expect("pos:one" in plugins).toBe(false);
186
+ });
187
+
188
+ it("manager/teleport 跨分组迁移与同分组重排", async () => {
189
+ const listener =
190
+ app.console.listeners["manager/teleport"];
191
+ const plugins = loader.config.plugins as Record<
192
+ string,
193
+ unknown
194
+ >;
195
+ // 跨分组:group:g1 → 根(index 0)
196
+ await listener?.callback.call(
197
+ client as never,
198
+ "g1",
199
+ "mov:me",
200
+ "",
201
+ 0,
202
+ );
203
+ await flushWrites();
204
+ expect(plugins["mov:me"]).toEqual({ m: 1 });
205
+ const group = plugins["group:g1"] as Record<
206
+ string,
207
+ unknown
208
+ >;
209
+ expect("mov:me" in group).toBe(false);
210
+ // 运行时 fork 已改挂根作用域
211
+ const dummy = loader.data["mov"] as Plugin;
212
+ const fork = app.registry.get(dummy)?.children[0];
213
+ expect(fork?.parent.scope.uid).toBe(
214
+ loader.entry.scope.uid,
215
+ );
216
+
217
+ // 同分组:仅重排键位置
218
+ await listener?.callback.call(
219
+ client as never,
220
+ "",
221
+ "mov:me",
222
+ "",
223
+ 0,
224
+ );
225
+ await flushWrites();
226
+ expect(Object.keys(plugins)[0]).toBe("mov:me");
227
+ });
228
+
229
+ itQuiet(
230
+ ["loader"],
231
+ "manager/* 失败统一回执 failed",
232
+ async () => {
233
+ const listener =
234
+ app.console.listeners["manager/teleport"];
235
+ await expect(
236
+ listener?.callback.call(
237
+ client as never,
238
+ "no-such-group",
239
+ "k",
240
+ "",
241
+ 0,
242
+ ),
243
+ ).rejects.toThrow("failed");
244
+ },
245
+ );
246
+
247
+ it("manager/app-reload 替换全局配置并整进程重载", async () => {
248
+ const listener =
249
+ app.console.listeners["manager/app-reload"];
250
+ const writesBefore = loader.writes.length;
251
+ await listener?.callback.call(client as never, {
252
+ prefix: ["."],
253
+ });
254
+ await flushWrites();
255
+ expect(loader.config.prefix).toEqual(["."]);
256
+ // plugins 部分被保留
257
+ expect(loader.config.plugins).toBeTruthy();
258
+ expect(loader.writes.length).toBeGreaterThan(
259
+ writesBefore,
260
+ );
261
+ expect(loader.fullReloadCount).toBe(1);
262
+ // 非静默写盘触发 config 事件 → config 服务刷新推送
263
+ const messages = readSent().filter(
264
+ (msg) =>
265
+ msg.type === "data" && msg.body.key === "config",
266
+ );
267
+ expect(messages.length).toBeGreaterThan(0);
268
+ });
269
+ });
270
+ });