@koishi-ce/plugin-market 1.1.3 → 1.2.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 +2 -2
- package/dist/style.css +1 -1
- package/lib/index.d.ts +58 -17
- package/lib/index.mjs +192 -58
- package/package.json +6 -6
- package/src/node/__tests__/commands.test.ts +349 -0
- package/src/node/__tests__/helpers.ts +380 -0
- package/src/node/__tests__/provider.test.ts +340 -0
- package/src/node/__tests__/retry.test.ts +169 -0
- package/src/node/dependencies/service.ts +118 -0
- package/src/node/dependencies/snapshot.test.ts +176 -0
- package/src/node/dependencies/snapshot.ts +92 -0
- package/src/node/dependencies/types.ts +39 -0
- package/src/node/deps.ts +1 -14
- package/src/node/index.ts +7 -8
- package/src/node/installer/index.ts +38 -20
- package/src/node/installer/installer.test.ts +282 -0
- package/src/node/installer/manifest.ts +15 -0
- package/src/index.test.ts +0 -1242
|
@@ -0,0 +1,349 @@
|
|
|
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 { mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type { Plugin } from "@koishi-ce/koishi";
|
|
15
|
+
import {
|
|
16
|
+
App,
|
|
17
|
+
app,
|
|
18
|
+
client,
|
|
19
|
+
FakeConsole,
|
|
20
|
+
type FakeLoader,
|
|
21
|
+
http,
|
|
22
|
+
itQuiet,
|
|
23
|
+
Logger,
|
|
24
|
+
market,
|
|
25
|
+
registryData,
|
|
26
|
+
registryServer,
|
|
27
|
+
setNextExitCode,
|
|
28
|
+
setSearchResponse,
|
|
29
|
+
spawnCalls,
|
|
30
|
+
startApp,
|
|
31
|
+
stopApp,
|
|
32
|
+
type TestApp,
|
|
33
|
+
tmp,
|
|
34
|
+
} from "./helpers.ts";
|
|
35
|
+
|
|
36
|
+
beforeAll(startApp);
|
|
37
|
+
afterAll(stopApp);
|
|
38
|
+
|
|
39
|
+
describe("market 聊天指令", () => {
|
|
40
|
+
itQuiet(
|
|
41
|
+
"plugin.install 缺参与未找到的报错路径",
|
|
42
|
+
async () => {
|
|
43
|
+
const missing = await client.receive(
|
|
44
|
+
"plugin.install",
|
|
45
|
+
);
|
|
46
|
+
expect(missing[0]).toContain("请输入插件名。");
|
|
47
|
+
const notFound = await client.receive(
|
|
48
|
+
"plugin.install absent-pkg",
|
|
49
|
+
);
|
|
50
|
+
expect(notFound[0]).toContain("未找到该插件。");
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
it("plugin.install 已安装时提示重复", async () => {
|
|
55
|
+
const replies = await client.receive(
|
|
56
|
+
"plugin.install demo",
|
|
57
|
+
);
|
|
58
|
+
expect(replies[0]).toContain("该插件已安装。");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
itQuiet(
|
|
62
|
+
"plugin.install 安装新插件并写入依赖",
|
|
63
|
+
async () => {
|
|
64
|
+
setNextExitCode(0);
|
|
65
|
+
spawnCalls.length = 0;
|
|
66
|
+
const replies = await client.receive(
|
|
67
|
+
"plugin.install newpkg",
|
|
68
|
+
);
|
|
69
|
+
expect(replies[0]).toContain("安装成功!");
|
|
70
|
+
expect(spawnCalls.length).toBe(1);
|
|
71
|
+
const manifest = JSON.parse(
|
|
72
|
+
await Bun.file(join(tmp, "package.json")).text(),
|
|
73
|
+
) as { dependencies: Record<string, string> };
|
|
74
|
+
expect(
|
|
75
|
+
manifest.dependencies["koishi-plugin-newpkg"],
|
|
76
|
+
).toBe("1.0.0");
|
|
77
|
+
// 重启消息在安装完成后复位(Loader 与桩形状不同,经 unknown 二段式断言)
|
|
78
|
+
expect(
|
|
79
|
+
(app.loader as unknown as FakeLoader).envData[
|
|
80
|
+
"message"
|
|
81
|
+
],
|
|
82
|
+
).toBeNull();
|
|
83
|
+
},
|
|
84
|
+
15000,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
itQuiet(
|
|
88
|
+
"plugin.uninstall 卸载依赖并从清单移除",
|
|
89
|
+
async () => {
|
|
90
|
+
setNextExitCode(0);
|
|
91
|
+
spawnCalls.length = 0;
|
|
92
|
+
const replies = await client.receive(
|
|
93
|
+
"plugin.uninstall newpkg",
|
|
94
|
+
);
|
|
95
|
+
expect(replies[0]).toContain("卸载成功!");
|
|
96
|
+
const manifest = JSON.parse(
|
|
97
|
+
await Bun.file(join(tmp, "package.json")).text(),
|
|
98
|
+
) as { dependencies: Record<string, string> };
|
|
99
|
+
expect(
|
|
100
|
+
manifest.dependencies["koishi-plugin-newpkg"],
|
|
101
|
+
).toBeUndefined();
|
|
102
|
+
},
|
|
103
|
+
15000,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
it("plugin.uninstall 未安装时提示", async () => {
|
|
107
|
+
const replies = await client.receive(
|
|
108
|
+
"plugin.uninstall absent-pkg",
|
|
109
|
+
);
|
|
110
|
+
expect(replies[0]).toContain("该插件未安装。");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("plugin.upgrade 无可升级项时提示已最新", async () => {
|
|
114
|
+
const replies = await client.receive("plugin.upgrade");
|
|
115
|
+
expect(replies[0]).toContain("所有插件已是最新版本。");
|
|
116
|
+
}, 10000);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("market 进阶链路", () => {
|
|
120
|
+
itQuiet("宿主配置不可写时不加载安装器", async () => {
|
|
121
|
+
// apply 的「仅告警并跳过」正是被测行为,静默 app 域避免预期告警刷屏
|
|
122
|
+
const levels = Logger.levels as Record<string, number>;
|
|
123
|
+
levels["app"] = 0;
|
|
124
|
+
try {
|
|
125
|
+
const appNoLoader = new App();
|
|
126
|
+
appNoLoader.plugin(http);
|
|
127
|
+
appNoLoader.plugin(
|
|
128
|
+
FakeConsole as unknown as Plugin.Constructor<TestApp>,
|
|
129
|
+
);
|
|
130
|
+
appNoLoader.plugin(market, {
|
|
131
|
+
registry: {
|
|
132
|
+
endpoint: `http://127.0.0.1:${registryServer.port}/`,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
await appNoLoader.start();
|
|
136
|
+
// apply 在 loader 缺席时仅告警并提前返回
|
|
137
|
+
expect(appNoLoader.installer).toBeUndefined();
|
|
138
|
+
await appNoLoader.stop();
|
|
139
|
+
} finally {
|
|
140
|
+
delete levels["app"];
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
itQuiet(
|
|
145
|
+
"浏览器 market/install 监听器执行安装并刷新服务",
|
|
146
|
+
async () => {
|
|
147
|
+
setNextExitCode(0);
|
|
148
|
+
const listener =
|
|
149
|
+
app.console.listeners["market/install"];
|
|
150
|
+
expect(listener).toBeDefined();
|
|
151
|
+
const code = (await listener?.callback.call(
|
|
152
|
+
{} as never,
|
|
153
|
+
{ "koishi-plugin-newpkg": "1.0.0" },
|
|
154
|
+
true,
|
|
155
|
+
)) as number;
|
|
156
|
+
expect(code).toBe(0);
|
|
157
|
+
},
|
|
158
|
+
15000,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
itQuiet(
|
|
162
|
+
"浏览器 market/registry 监听器批量查询包元数据",
|
|
163
|
+
async () => {
|
|
164
|
+
const listener =
|
|
165
|
+
app.console.listeners["market/registry"];
|
|
166
|
+
expect(listener).toBeDefined();
|
|
167
|
+
const meta = (await listener?.callback.call(
|
|
168
|
+
{} as never,
|
|
169
|
+
["koishi-plugin-demo", "koishi-plugin-missing"],
|
|
170
|
+
)) as Record<string, unknown>;
|
|
171
|
+
expect(
|
|
172
|
+
Object.keys(meta["koishi-plugin-demo"] ?? {}),
|
|
173
|
+
).toContain("2.0.0");
|
|
174
|
+
expect(meta["koishi-plugin-missing"]).toEqual({});
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
it("搜索结果中不兼容的包被跳过(analyze onSkipped/ignored)", async () => {
|
|
179
|
+
const svc = app.get("console.services.market");
|
|
180
|
+
// ghost 有 registry 条目,但版本声明的 koishi peer 与 4.x 不相交
|
|
181
|
+
registryData["koishi-plugin-ghost"] = {
|
|
182
|
+
versions: {
|
|
183
|
+
"1.0.0": {
|
|
184
|
+
version: "1.0.0",
|
|
185
|
+
peerDependencies: { koishi: "^5.0.0" },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
time: { "1.0.0": "2024-01-01T00:00:00Z" },
|
|
189
|
+
};
|
|
190
|
+
setSearchResponse({
|
|
191
|
+
objects: [
|
|
192
|
+
{
|
|
193
|
+
package: {
|
|
194
|
+
name: "koishi-plugin-ghost",
|
|
195
|
+
version: "1.0.0",
|
|
196
|
+
date: "2024-01-01T00:00:00Z",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
total: 1,
|
|
201
|
+
});
|
|
202
|
+
await svc?.start(true);
|
|
203
|
+
// collect 对 analyze 为即发即忘,等待逐包分析完成
|
|
204
|
+
await new Promise((resolve) =>
|
|
205
|
+
setTimeout(resolve, 300),
|
|
206
|
+
);
|
|
207
|
+
const payload = await svc?.get();
|
|
208
|
+
// 无兼容版本:对象标记 ignored,不进入数据缓存。
|
|
209
|
+
// progress 恒为 0:Scanner 以 defineProperty 定义 progress(不可写),
|
|
210
|
+
// analyze 收尾的自增在严格模式下抛错且被即发即忘吞掉(上游行为)。
|
|
211
|
+
expect(payload?.data).toEqual({});
|
|
212
|
+
expect(payload?.failed).toBe(0);
|
|
213
|
+
expect(payload?.total).toBe(1);
|
|
214
|
+
expect(payload?.progress).toBe(0);
|
|
215
|
+
delete registryData["koishi-plugin-ghost"];
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
itQuiet(
|
|
219
|
+
"搜索结果中被限流的包经重试后计入 failed(onFailure)",
|
|
220
|
+
async () => {
|
|
221
|
+
const svc = app.get("console.services.market");
|
|
222
|
+
setSearchResponse({
|
|
223
|
+
objects: [
|
|
224
|
+
{
|
|
225
|
+
package: {
|
|
226
|
+
name: "koishi-plugin-ratelimited",
|
|
227
|
+
version: "1.0.0",
|
|
228
|
+
date: "2024-01-01T00:00:00Z",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
total: 1,
|
|
233
|
+
});
|
|
234
|
+
await svc?.start(true);
|
|
235
|
+
// 等待限流重试(Retry-After 10ms × 3 次)与即发即忘的 analyze。
|
|
236
|
+
// 注意不能经 get() 断言:super.start() 会清空 _task,get() 触发的
|
|
237
|
+
// 二次 collect 会把 failed 重置(即发即忘的 analyze 尚未完成)。
|
|
238
|
+
await new Promise((resolve) =>
|
|
239
|
+
setTimeout(resolve, 400),
|
|
240
|
+
);
|
|
241
|
+
// 不可达/被限流的包名进入 failed 列表(上一个用例中 registry
|
|
242
|
+
// 条目已删除的 ghost 包经 404 路径同样落入此处)
|
|
243
|
+
const provider = svc as unknown as {
|
|
244
|
+
failed: string[];
|
|
245
|
+
};
|
|
246
|
+
expect(
|
|
247
|
+
provider.failed.some((name) =>
|
|
248
|
+
name.startsWith("koishi-plugin-"),
|
|
249
|
+
),
|
|
250
|
+
).toBe(true);
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
itQuiet(
|
|
255
|
+
"plugin.upgrade 检出可升级项并输出确认提示",
|
|
256
|
+
async () => {
|
|
257
|
+
// 本地放置旧版安装,使 resolved 有值且低于远端 latest
|
|
258
|
+
mkdirSync(
|
|
259
|
+
join(tmp, "node_modules", "koishi-plugin-demo"),
|
|
260
|
+
{
|
|
261
|
+
recursive: true,
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
writeFileSync(
|
|
265
|
+
join(
|
|
266
|
+
tmp,
|
|
267
|
+
"node_modules",
|
|
268
|
+
"koishi-plugin-demo",
|
|
269
|
+
"package.json",
|
|
270
|
+
),
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
name: "koishi-plugin-demo",
|
|
273
|
+
version: "1.0.0",
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
app.installer.refresh();
|
|
277
|
+
|
|
278
|
+
setNextExitCode(0);
|
|
279
|
+
// 发出升级指令(异步等待确认),再以 Y 回复确认。
|
|
280
|
+
// 注:mock 环境下指令 ctx 对 loader 服务的可见性受 cordis
|
|
281
|
+
// isolate 语义限制(见仓库测试任务记录),确认后的安装段
|
|
282
|
+
// 由 market/install 监听器用例覆盖。
|
|
283
|
+
const question = client.receive(
|
|
284
|
+
"plugin.upgrade demo",
|
|
285
|
+
);
|
|
286
|
+
await new Promise((resolve) =>
|
|
287
|
+
setTimeout(resolve, 200),
|
|
288
|
+
);
|
|
289
|
+
await client.receive("Y");
|
|
290
|
+
const replies = await question;
|
|
291
|
+
const output = replies.join("\n");
|
|
292
|
+
expect(output).toContain("koishi-plugin-demo");
|
|
293
|
+
expect(output).toContain("1.0.0 -> 2.0.0");
|
|
294
|
+
},
|
|
295
|
+
20000,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
itQuiet(
|
|
299
|
+
"plugin.upgrade 对本地畸形版本静默跳过(非法 semver catch)",
|
|
300
|
+
async () => {
|
|
301
|
+
// registry 侧提供合法 latest;本地安装产物的 version 是畸形串,
|
|
302
|
+
// request(清单声明)合法故不标记 invalid,gt(latest, resolved)
|
|
303
|
+
// 解析失败进入 catch 分支:该包被过滤,视为无可升级项
|
|
304
|
+
registryData["koishi-plugin-weird"] = {
|
|
305
|
+
versions: {
|
|
306
|
+
"2.0.0": {
|
|
307
|
+
version: "2.0.0",
|
|
308
|
+
peerDependencies: { koishi: "^4.17.0" },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
mkdirSync(
|
|
313
|
+
join(tmp, "node_modules", "koishi-plugin-weird"),
|
|
314
|
+
{
|
|
315
|
+
recursive: true,
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
writeFileSync(
|
|
319
|
+
join(
|
|
320
|
+
tmp,
|
|
321
|
+
"node_modules",
|
|
322
|
+
"koishi-plugin-weird",
|
|
323
|
+
"package.json",
|
|
324
|
+
),
|
|
325
|
+
JSON.stringify({
|
|
326
|
+
name: "koishi-plugin-weird",
|
|
327
|
+
version: "not.a.version",
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
setNextExitCode(0);
|
|
331
|
+
// 经 install 注入清单声明(顺带刷新 Installer 的 manifest 快照)
|
|
332
|
+
const code = await app.installer.install({
|
|
333
|
+
"koishi-plugin-weird": "1.0.0",
|
|
334
|
+
});
|
|
335
|
+
expect(code).toBe(0);
|
|
336
|
+
const deps = await app.installer.getDeps();
|
|
337
|
+
expect(deps["koishi-plugin-weird"]?.resolved).toBe(
|
|
338
|
+
"not.a.version",
|
|
339
|
+
);
|
|
340
|
+
const replies = await client.receive(
|
|
341
|
+
"plugin.upgrade weird",
|
|
342
|
+
);
|
|
343
|
+
expect(replies[0]).toContain(
|
|
344
|
+
"所有插件已是最新版本。",
|
|
345
|
+
);
|
|
346
|
+
},
|
|
347
|
+
20000,
|
|
348
|
+
);
|
|
349
|
+
});
|