@koishi-ce/plugin-market 1.0.8 → 1.0.10
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 +1 -1
- package/dist/style.css +1 -1
- package/lib/index.d.ts +8 -2
- package/lib/index.mjs +36 -37
- package/package.json +9 -10
- package/src/__tests__/index.test.ts +584 -286
- package/src/browser/index.ts +6 -1
- package/src/browser/market.ts +6 -2
- package/src/node/deps.ts +7 -2
- package/src/node/index.ts +74 -22
- package/src/node/installer.ts +114 -84
- package/src/node/market.ts +42 -21
- package/src/node/proc.ts +31 -0
- package/src/shared/index.ts +27 -12
|
@@ -2,28 +2,40 @@
|
|
|
2
2
|
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
3
|
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
beforeAll,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
mock,
|
|
12
|
+
} from "bun:test";
|
|
13
|
+
import {
|
|
14
|
+
mkdirSync,
|
|
15
|
+
mkdtempSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
7
19
|
import { tmpdir } from "node:os";
|
|
8
20
|
import { join } from "node:path";
|
|
9
21
|
import memory from "@koishijs/plugin-database-memory";
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* market 插件测试:
|
|
13
|
-
* - 子进程(
|
|
25
|
+
* - 子进程(bun 安装)经 mock.module 拦截本地 proc.ts(spawnBun 封装),不落盘不联网安装;
|
|
14
26
|
* - registry 网络请求由进程内 Bun.serve 提供(registry 协议的最小 JSON);
|
|
15
27
|
* - 宿主环境(loader / cwd)使用 FakeLoader 与临时目录 + chdir,
|
|
16
28
|
* Installer 的 override 写盘只会作用于临时 package.json。
|
|
17
29
|
*/
|
|
18
30
|
|
|
19
|
-
/**
|
|
31
|
+
/** 已发生的子进程调用(参数列表)。 */
|
|
20
32
|
const spawnCalls: string[][] = [];
|
|
21
33
|
/** 控制下一次子进程的退出码与触发事件。 */
|
|
22
34
|
let nextExitCode = 0;
|
|
23
35
|
let nextSpawnError = false;
|
|
24
36
|
|
|
25
|
-
const
|
|
26
|
-
spawnCalls.push(
|
|
37
|
+
const spawnBunMock = (_args: string[], _cwd: string) => {
|
|
38
|
+
spawnCalls.push(_args);
|
|
27
39
|
return {
|
|
28
40
|
on(event: string, cb: (code?: number) => void) {
|
|
29
41
|
if (nextSpawnError) {
|
|
@@ -35,24 +47,25 @@ const execaMock = (name: string, args: string[]) => {
|
|
|
35
47
|
},
|
|
36
48
|
stderr: {
|
|
37
49
|
on(event: string, cb: (data: Buffer) => void) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
// 同步派发:异步派发会晚于用例的日志静默窗口,导致转发告警漏出刷屏
|
|
51
|
+
if (event === "data")
|
|
52
|
+
cb(Buffer.from("stderr line\n"));
|
|
41
53
|
return this;
|
|
42
54
|
},
|
|
43
55
|
},
|
|
44
56
|
stdout: {
|
|
45
57
|
on(event: string, cb: (data: Buffer) => void) {
|
|
46
|
-
if (event === "data")
|
|
47
|
-
|
|
48
|
-
}
|
|
58
|
+
if (event === "data")
|
|
59
|
+
cb(Buffer.from("stdout line\n"));
|
|
49
60
|
return this;
|
|
50
61
|
},
|
|
51
62
|
},
|
|
52
63
|
};
|
|
53
64
|
};
|
|
54
65
|
|
|
55
|
-
mock.module("
|
|
66
|
+
mock.module("../node/proc.ts", () => ({
|
|
67
|
+
spawnBun: spawnBunMock,
|
|
68
|
+
}));
|
|
56
69
|
|
|
57
70
|
import type { Entry } from "@koishi-ce/console";
|
|
58
71
|
// 均为 type-only 导入:编译期擦除,不干扰 mock.module 先于插件加载的时序
|
|
@@ -60,18 +73,29 @@ import type { Plugin } from "@koishi-ce/koishi";
|
|
|
60
73
|
import type { RemotePackage } from "@koishi-ce/registry";
|
|
61
74
|
|
|
62
75
|
const { Console } = await import("@koishi-ce/console");
|
|
63
|
-
const { App, Service } = await import(
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
const { App, Logger, Service } = await import(
|
|
77
|
+
"@koishi-ce/koishi"
|
|
78
|
+
);
|
|
79
|
+
const http = (await import("@koishi-ce/plugin-http"))
|
|
80
|
+
.default;
|
|
81
|
+
const { isResidentInCache } = await import(
|
|
82
|
+
"@koishi-ce/registry"
|
|
83
|
+
);
|
|
66
84
|
const market = await import("../node/index.ts");
|
|
67
|
-
const { default: Installer } = await import(
|
|
68
|
-
|
|
85
|
+
const { default: Installer } = await import(
|
|
86
|
+
"../node/installer.ts"
|
|
87
|
+
);
|
|
88
|
+
const mockPlugin = (await import("@koishi-ce/plugin-mock"))
|
|
89
|
+
.default;
|
|
69
90
|
// 加载包入口占位文件(纯 re-export,无独立逻辑),保证 src 全量被加载
|
|
70
91
|
await import("../index.ts");
|
|
71
92
|
|
|
72
93
|
/** 控制台服务桩:仅实现入口登记所需的最小面。 */
|
|
73
94
|
class FakeConsole extends Console {
|
|
74
|
-
protected resolveEntry(
|
|
95
|
+
protected resolveEntry(
|
|
96
|
+
_files: Entry.Files,
|
|
97
|
+
_key: string,
|
|
98
|
+
): string[] {
|
|
75
99
|
return [];
|
|
76
100
|
}
|
|
77
101
|
}
|
|
@@ -84,7 +108,9 @@ class FakeLoader extends Service {
|
|
|
84
108
|
return ["group:entry", "plugins"];
|
|
85
109
|
}
|
|
86
110
|
fullReload() {}
|
|
87
|
-
constructor(
|
|
111
|
+
constructor(
|
|
112
|
+
ctx: ConstructorParameters<typeof Service>[0],
|
|
113
|
+
) {
|
|
88
114
|
super(ctx, "loader", true);
|
|
89
115
|
}
|
|
90
116
|
}
|
|
@@ -104,8 +130,37 @@ const registryData: {
|
|
|
104
130
|
};
|
|
105
131
|
} = {};
|
|
106
132
|
|
|
107
|
-
/** 搜索接口响应(/-/v1/search,collect
|
|
108
|
-
let searchResponse: {
|
|
133
|
+
/** 搜索接口响应(/-/v1/search,collect 阶段消费)。初始给空结果,避免启动期空转 404。 */
|
|
134
|
+
let searchResponse: {
|
|
135
|
+
objects: unknown[];
|
|
136
|
+
total: number;
|
|
137
|
+
} | null = {
|
|
138
|
+
objects: [],
|
|
139
|
+
total: 0,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** 声明预期触发 market 域告警(404 / 限流等桩失败路径)的用例:执行期间静默该域。 */
|
|
143
|
+
const itQuiet = (
|
|
144
|
+
name: string,
|
|
145
|
+
fn: () => Promise<void> | void,
|
|
146
|
+
timeout?: number,
|
|
147
|
+
) =>
|
|
148
|
+
it(
|
|
149
|
+
name,
|
|
150
|
+
async () => {
|
|
151
|
+
const levels = Logger.levels as Record<
|
|
152
|
+
string,
|
|
153
|
+
number
|
|
154
|
+
>;
|
|
155
|
+
levels["market"] = 0;
|
|
156
|
+
try {
|
|
157
|
+
await fn();
|
|
158
|
+
} finally {
|
|
159
|
+
delete levels["market"];
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
timeout,
|
|
163
|
+
);
|
|
109
164
|
|
|
110
165
|
const registryServer = Bun.serve({
|
|
111
166
|
port: 0,
|
|
@@ -119,7 +174,9 @@ const registryServer = Bun.serve({
|
|
|
119
174
|
return Response.json(searchResponse);
|
|
120
175
|
}
|
|
121
176
|
// 模拟 registry 限流:恒定 429 + 极短的 Retry-After,驱动重试后失败
|
|
122
|
-
if (
|
|
177
|
+
if (
|
|
178
|
+
url.pathname.includes("koishi-plugin-ratelimited")
|
|
179
|
+
) {
|
|
123
180
|
return new Response("rate limited", {
|
|
124
181
|
status: 429,
|
|
125
182
|
headers: { "Retry-After": "0.01" },
|
|
@@ -137,18 +194,44 @@ const registryServer = Bun.serve({
|
|
|
137
194
|
// 预置一个兼容插件包:最新版 2.0.0,旧版 1.0.0 均声明 koishi ^4 peer
|
|
138
195
|
registryData["koishi-plugin-demo"] = {
|
|
139
196
|
versions: {
|
|
140
|
-
"1.0.0": {
|
|
141
|
-
|
|
197
|
+
"1.0.0": {
|
|
198
|
+
version: "1.0.0",
|
|
199
|
+
peerDependencies: { koishi: "^4.17.0" },
|
|
200
|
+
},
|
|
201
|
+
"2.0.0": {
|
|
202
|
+
version: "2.0.0",
|
|
203
|
+
peerDependencies: { koishi: "^4.17.0" },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
time: {
|
|
207
|
+
"1.0.0": "2024-01-01T00:00:00Z",
|
|
208
|
+
"2.0.0": "2024-06-01T00:00:00Z",
|
|
142
209
|
},
|
|
143
|
-
time: { "1.0.0": "2024-01-01T00:00:00Z", "2.0.0": "2024-06-01T00:00:00Z" },
|
|
144
210
|
};
|
|
145
211
|
// 预置一个未安装的插件包(plugin.install 安装路径使用)
|
|
146
212
|
registryData["koishi-plugin-newpkg"] = {
|
|
147
213
|
versions: {
|
|
148
|
-
"1.0.0": {
|
|
214
|
+
"1.0.0": {
|
|
215
|
+
version: "1.0.0",
|
|
216
|
+
peerDependencies: { koishi: "^4.17.0" },
|
|
217
|
+
},
|
|
149
218
|
},
|
|
150
219
|
time: { "1.0.0": "2024-01-01T00:00:00Z" },
|
|
151
220
|
};
|
|
221
|
+
// 预置初始清单里三个非插件依赖的远端条目:Installer.refresh() 每轮都会对全部
|
|
222
|
+
// 清单依赖发起 registry 查询,缺桩会让每个用例都刷 404 告警
|
|
223
|
+
registryData["koishi"] = {
|
|
224
|
+
versions: { "4.18.0": { version: "4.18.0" } },
|
|
225
|
+
time: { "4.18.0": "2024-01-01T00:00:00Z" },
|
|
226
|
+
};
|
|
227
|
+
registryData["market-alias"] = {
|
|
228
|
+
versions: { "1.0.0": { version: "1.0.0" } },
|
|
229
|
+
time: { "1.0.0": "2024-01-01T00:00:00Z" },
|
|
230
|
+
};
|
|
231
|
+
registryData["bad-range"] = {
|
|
232
|
+
versions: { "1.0.0": { version: "1.0.0" } },
|
|
233
|
+
time: { "1.0.0": "2024-01-01T00:00:00Z" },
|
|
234
|
+
};
|
|
152
235
|
|
|
153
236
|
/** 临时宿主目录(Installer 的 cwd 与 override 写盘目标)。 */
|
|
154
237
|
const initialDependencies = {
|
|
@@ -166,7 +249,10 @@ const originalCwd = process.cwd();
|
|
|
166
249
|
writeFileSync(
|
|
167
250
|
join(tmp, "package.json"),
|
|
168
251
|
JSON.stringify(
|
|
169
|
-
{
|
|
252
|
+
{
|
|
253
|
+
name: "market-host",
|
|
254
|
+
dependencies: initialDependencies,
|
|
255
|
+
},
|
|
170
256
|
null,
|
|
171
257
|
"\t",
|
|
172
258
|
),
|
|
@@ -182,10 +268,14 @@ const app = new App();
|
|
|
182
268
|
app.plugin(memory as unknown as typeof memory.default);
|
|
183
269
|
app.plugin(http);
|
|
184
270
|
// Console 基类的 static inject 是 cordis 3 旧形态,与 Plugin.Constructor 期待类型不兼容,仅做类型层转型
|
|
185
|
-
app.plugin(
|
|
271
|
+
app.plugin(
|
|
272
|
+
FakeConsole as unknown as Plugin.Constructor<TestApp>,
|
|
273
|
+
);
|
|
186
274
|
app.plugin(FakeLoader);
|
|
187
275
|
app.plugin(market, {
|
|
188
|
-
registry: {
|
|
276
|
+
registry: {
|
|
277
|
+
endpoint: `http://127.0.0.1:${registryServer.port}/`,
|
|
278
|
+
},
|
|
189
279
|
});
|
|
190
280
|
app.plugin(mockPlugin);
|
|
191
281
|
|
|
@@ -207,21 +297,31 @@ afterAll(async () => {
|
|
|
207
297
|
|
|
208
298
|
describe("market 插件", () => {
|
|
209
299
|
it("注册三个数据服务与浏览器监听器", () => {
|
|
210
|
-
expect(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
expect(
|
|
214
|
-
|
|
300
|
+
expect(
|
|
301
|
+
app.get("console.services.market"),
|
|
302
|
+
).toBeDefined();
|
|
303
|
+
expect(
|
|
304
|
+
app.get("console.services.dependencies"),
|
|
305
|
+
).toBeDefined();
|
|
306
|
+
expect(
|
|
307
|
+
app.get("console.services.registry"),
|
|
308
|
+
).toBeDefined();
|
|
309
|
+
expect(
|
|
310
|
+
app.console.listeners["market/install"],
|
|
311
|
+
).toBeDefined();
|
|
312
|
+
expect(
|
|
313
|
+
app.console.listeners["market/registry"],
|
|
314
|
+
).toBeDefined();
|
|
215
315
|
});
|
|
216
316
|
|
|
217
317
|
it("resolveName 解析插件短名的候选全名", () => {
|
|
218
318
|
const installer = app.installer;
|
|
219
|
-
expect(
|
|
220
|
-
"@koishijs/plugin-echo",
|
|
221
|
-
]);
|
|
222
|
-
expect(
|
|
223
|
-
"koishi-plugin-echo",
|
|
224
|
-
]);
|
|
319
|
+
expect(
|
|
320
|
+
installer.resolveName("@koishijs/plugin-echo"),
|
|
321
|
+
).toEqual(["@koishijs/plugin-echo"]);
|
|
322
|
+
expect(
|
|
323
|
+
installer.resolveName("koishi-plugin-echo"),
|
|
324
|
+
).toEqual(["koishi-plugin-echo"]);
|
|
225
325
|
expect(installer.resolveName("@scope/echo")).toEqual([
|
|
226
326
|
"@scope/koishi-plugin-echo",
|
|
227
327
|
]);
|
|
@@ -234,27 +334,40 @@ describe("market 插件", () => {
|
|
|
234
334
|
it("getDeps 汇总本地依赖并带出远端最新版", async () => {
|
|
235
335
|
const deps = await app.installer.getDeps();
|
|
236
336
|
// 语义化区间去除前缀符号
|
|
237
|
-
expect(deps["koishi-plugin-demo"]?.request).toBe(
|
|
337
|
+
expect(deps["koishi-plugin-demo"]?.request).toBe(
|
|
338
|
+
"1.0.0",
|
|
339
|
+
);
|
|
238
340
|
// 远端最新版(本地 registry 预置 2.0.0)
|
|
239
|
-
expect(deps["koishi-plugin-demo"]?.latest).toBe(
|
|
341
|
+
expect(deps["koishi-plugin-demo"]?.latest).toBe(
|
|
342
|
+
"2.0.0",
|
|
343
|
+
);
|
|
240
344
|
// 非法 semver 标记 invalid
|
|
241
345
|
expect(deps["bad-range"]?.invalid).toBe(true);
|
|
242
346
|
});
|
|
243
347
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
348
|
+
itQuiet(
|
|
349
|
+
"findVersion 返回首个存在的候选包版本",
|
|
350
|
+
async () => {
|
|
351
|
+
const found = await app.installer.findVersion([
|
|
352
|
+
"@koishijs/plugin-none",
|
|
353
|
+
"koishi-plugin-demo",
|
|
354
|
+
]);
|
|
355
|
+
expect(found).toEqual({
|
|
356
|
+
"koishi-plugin-demo": "2.0.0",
|
|
357
|
+
});
|
|
358
|
+
// 全部不存在时返回 undefined
|
|
359
|
+
expect(
|
|
360
|
+
await app.installer.findVersion([
|
|
361
|
+
"@koishijs/plugin-none",
|
|
362
|
+
]),
|
|
363
|
+
).toBeUndefined();
|
|
364
|
+
},
|
|
365
|
+
);
|
|
255
366
|
|
|
256
|
-
|
|
257
|
-
const versions = await app.installer.getPackage(
|
|
367
|
+
itQuiet("getPackage 拉取失败时回退为空表", async () => {
|
|
368
|
+
const versions = await app.installer.getPackage(
|
|
369
|
+
"koishi-plugin-missing",
|
|
370
|
+
);
|
|
258
371
|
expect(versions).toEqual({});
|
|
259
372
|
});
|
|
260
373
|
|
|
@@ -267,60 +380,79 @@ describe("market 插件", () => {
|
|
|
267
380
|
} as unknown as RemotePackage,
|
|
268
381
|
]);
|
|
269
382
|
expect(
|
|
270
|
-
Object.keys(
|
|
383
|
+
Object.keys(
|
|
384
|
+
app.installer.fullCache["koishi-plugin-demo"] ?? {},
|
|
385
|
+
),
|
|
271
386
|
).toEqual(["3.0.0"]);
|
|
272
387
|
// 等待节流窗口
|
|
273
|
-
await new Promise((resolve) =>
|
|
388
|
+
await new Promise((resolve) =>
|
|
389
|
+
setTimeout(resolve, 600),
|
|
390
|
+
);
|
|
274
391
|
});
|
|
275
392
|
});
|
|
276
393
|
|
|
277
394
|
describe("Installer 安装链路", () => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
395
|
+
itQuiet(
|
|
396
|
+
"install 尊重护栏依赖并执行子进程安装",
|
|
397
|
+
async () => {
|
|
398
|
+
nextExitCode = 0;
|
|
399
|
+
spawnCalls.length = 0;
|
|
400
|
+
const code = await app.installer.install({
|
|
401
|
+
koishi: "2.0.0",
|
|
402
|
+
"market-alias": null,
|
|
403
|
+
"koishi-plugin-demo": "^1.0.0",
|
|
404
|
+
});
|
|
405
|
+
expect(code).toBe(0);
|
|
406
|
+
// 触发了包管理器安装(bun install --registry …)
|
|
407
|
+
expect(spawnCalls.length).toBe(1);
|
|
408
|
+
expect(spawnCalls[0]?.[0]).toBe("install");
|
|
409
|
+
// 重新读取临时 package.json:护栏项保持原样,新依赖加入
|
|
410
|
+
const manifest = JSON.parse(
|
|
411
|
+
await Bun.file(join(tmp, "package.json")).text(),
|
|
412
|
+
) as { dependencies: Record<string, string> };
|
|
413
|
+
expect(manifest.dependencies["koishi"]).toBe(
|
|
414
|
+
"workspace:*",
|
|
415
|
+
);
|
|
416
|
+
expect(manifest.dependencies["market-alias"]).toBe(
|
|
417
|
+
"npm:@koishi-ce/anything@^1.0.0",
|
|
418
|
+
);
|
|
419
|
+
expect(
|
|
420
|
+
manifest.dependencies["koishi-plugin-demo"],
|
|
421
|
+
).toBe("^1.0.0");
|
|
422
|
+
},
|
|
423
|
+
15000,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
itQuiet(
|
|
427
|
+
"install 强制时无视本地满足也要装",
|
|
428
|
+
async () => {
|
|
429
|
+
nextExitCode = 0;
|
|
430
|
+
spawnCalls.length = 0;
|
|
431
|
+
const code = await app.installer.install(
|
|
432
|
+
{ "koishi-plugin-demo": "^1.0.0" },
|
|
433
|
+
true,
|
|
434
|
+
);
|
|
435
|
+
expect(code).toBe(0);
|
|
436
|
+
expect(spawnCalls.length).toBe(1);
|
|
437
|
+
},
|
|
438
|
+
15000,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
itQuiet(
|
|
442
|
+
"子进程非零退出码向上传递",
|
|
443
|
+
async () => {
|
|
444
|
+
nextExitCode = 1;
|
|
445
|
+
spawnCalls.length = 0;
|
|
446
|
+
const code = await app.installer.install(
|
|
447
|
+
{ "koishi-plugin-demo": "^2.0.0" },
|
|
448
|
+
true,
|
|
449
|
+
);
|
|
450
|
+
expect(code).toBe(1);
|
|
451
|
+
},
|
|
452
|
+
15000,
|
|
453
|
+
);
|
|
322
454
|
|
|
323
|
-
|
|
455
|
+
itQuiet("子进程 spawn 失败返回 -1", async () => {
|
|
324
456
|
nextSpawnError = true;
|
|
325
457
|
nextExitCode = 0;
|
|
326
458
|
const code = await app.installer.exec(["install"]);
|
|
@@ -328,12 +460,15 @@ describe("Installer 安装链路", () => {
|
|
|
328
460
|
expect(code).toBe(-1);
|
|
329
461
|
});
|
|
330
462
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
463
|
+
itQuiet(
|
|
464
|
+
"exec 收集子进程 stdout / stderr 输出行",
|
|
465
|
+
async () => {
|
|
466
|
+
nextExitCode = 0;
|
|
467
|
+
nextSpawnError = false;
|
|
468
|
+
const code = await app.installer.exec(["install"]);
|
|
469
|
+
expect(code).toBe(0);
|
|
470
|
+
},
|
|
471
|
+
);
|
|
337
472
|
});
|
|
338
473
|
|
|
339
474
|
describe("isResidentInCache(装后重载判定)", () => {
|
|
@@ -343,9 +478,16 @@ describe("isResidentInCache(装后重载判定)", () => {
|
|
|
343
478
|
mkdirSync(pkgDir, { recursive: true });
|
|
344
479
|
writeFileSync(
|
|
345
480
|
join(pkgDir, "package.json"),
|
|
346
|
-
JSON.stringify({
|
|
481
|
+
JSON.stringify({
|
|
482
|
+
name,
|
|
483
|
+
version: "1.0.0",
|
|
484
|
+
main: "index.js",
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
writeFileSync(
|
|
488
|
+
join(pkgDir, "index.js"),
|
|
489
|
+
"module.exports = {}",
|
|
347
490
|
);
|
|
348
|
-
writeFileSync(join(pkgDir, "index.js"), "module.exports = {}");
|
|
349
491
|
return pkgDir;
|
|
350
492
|
}
|
|
351
493
|
|
|
@@ -353,16 +495,22 @@ describe("isResidentInCache(装后重载判定)", () => {
|
|
|
353
495
|
const pkgDir = placePkg("koishi-plugin-resident-check");
|
|
354
496
|
// 入口经 require 进入 require.cache,模拟旧版本驻留内存
|
|
355
497
|
require(join(pkgDir, "index.js"));
|
|
356
|
-
expect(
|
|
498
|
+
expect(
|
|
499
|
+
isResidentInCache("koishi-plugin-resident-check"),
|
|
500
|
+
).toBe(true);
|
|
357
501
|
});
|
|
358
502
|
|
|
359
503
|
it("已安装但无模块驻留内存时返回 false", () => {
|
|
360
504
|
placePkg("koishi-plugin-idle-check");
|
|
361
|
-
expect(
|
|
505
|
+
expect(
|
|
506
|
+
isResidentInCache("koishi-plugin-idle-check"),
|
|
507
|
+
).toBe(false);
|
|
362
508
|
});
|
|
363
509
|
|
|
364
510
|
it("包不存在时保守返回 true(宁可多重载不漏判)", () => {
|
|
365
|
-
expect(
|
|
511
|
+
expect(
|
|
512
|
+
isResidentInCache("koishi-plugin-absent-check"),
|
|
513
|
+
).toBe(true);
|
|
366
514
|
});
|
|
367
515
|
});
|
|
368
516
|
|
|
@@ -390,7 +538,9 @@ describe("registry 配置探测", () => {
|
|
|
390
538
|
app3.plugin(http);
|
|
391
539
|
app3.plugin(Installer, {});
|
|
392
540
|
await app3.start();
|
|
393
|
-
expect(app3.installer.endpoint).toBe(
|
|
541
|
+
expect(app3.installer.endpoint).toBe(
|
|
542
|
+
"http://registry.example.npm/",
|
|
543
|
+
);
|
|
394
544
|
await app3.stop();
|
|
395
545
|
rmSync(join(tmp, ".npmrc"), { force: true });
|
|
396
546
|
});
|
|
@@ -411,109 +561,178 @@ describe("MarketProvider 市场数据服务", () => {
|
|
|
411
561
|
keywords: ["koishi", "plugin", "Tool"],
|
|
412
562
|
},
|
|
413
563
|
},
|
|
414
|
-
{
|
|
564
|
+
{
|
|
565
|
+
package: {
|
|
566
|
+
name: "not-a-plugin",
|
|
567
|
+
date: "2024-01-01T00:00:00Z",
|
|
568
|
+
},
|
|
569
|
+
},
|
|
415
570
|
],
|
|
416
571
|
total: 1,
|
|
417
572
|
};
|
|
418
573
|
// start(true) 强制刷新市场数据(重新 collect)
|
|
419
574
|
await svc?.start(true);
|
|
420
575
|
// 等待节流窗口与逐包分析完成
|
|
421
|
-
await new Promise((resolve) =>
|
|
576
|
+
await new Promise((resolve) =>
|
|
577
|
+
setTimeout(resolve, 700),
|
|
578
|
+
);
|
|
422
579
|
const payload = await svc?.get();
|
|
423
580
|
expect(payload).toBeDefined();
|
|
424
581
|
// 非 plugin 条目被剔除,只保留 demo
|
|
425
|
-
expect(Object.keys(payload?.data ?? {})).toEqual([
|
|
582
|
+
expect(Object.keys(payload?.data ?? {})).toEqual([
|
|
583
|
+
"koishi-plugin-demo",
|
|
584
|
+
]);
|
|
426
585
|
expect(payload?.total).toBe(1);
|
|
427
586
|
expect(payload?.failed).toBe(0);
|
|
428
|
-
expect(payload?.registry).toBe(
|
|
587
|
+
expect(payload?.registry).toBe(
|
|
588
|
+
`http://127.0.0.1:${registryServer.port}/`,
|
|
589
|
+
);
|
|
429
590
|
});
|
|
430
591
|
|
|
431
592
|
it("依赖 / 注册表数据服务读取安装器缓存", async () => {
|
|
432
|
-
const dependencies = await app
|
|
433
|
-
|
|
434
|
-
|
|
593
|
+
const dependencies = await app
|
|
594
|
+
.get("console.services.dependencies")
|
|
595
|
+
?.get();
|
|
596
|
+
expect(
|
|
597
|
+
dependencies?.["koishi-plugin-demo"]?.request,
|
|
598
|
+
).toBeTruthy();
|
|
599
|
+
expect(
|
|
600
|
+
dependencies?.["koishi-plugin-demo"]?.latest,
|
|
601
|
+
).toBe("2.0.0");
|
|
435
602
|
|
|
436
|
-
const registry = await app
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
603
|
+
const registry = await app
|
|
604
|
+
.get("console.services.registry")
|
|
605
|
+
?.get();
|
|
606
|
+
expect(
|
|
607
|
+
Object.keys(registry?.["koishi-plugin-demo"] ?? {}),
|
|
608
|
+
).toContain("2.0.0");
|
|
440
609
|
});
|
|
441
610
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
611
|
+
itQuiet(
|
|
612
|
+
"搜索接口失败时 get 返回空数据与错误标记",
|
|
613
|
+
async () => {
|
|
614
|
+
const svc = app.get("console.services.market");
|
|
615
|
+
searchResponse = null;
|
|
616
|
+
// 强制重扫:collect 失败置 _error,get 返回空 payload
|
|
617
|
+
await svc?.start(true);
|
|
618
|
+
const payload = await svc?.get();
|
|
619
|
+
expect(payload).toEqual({
|
|
620
|
+
data: {},
|
|
621
|
+
failed: 0,
|
|
622
|
+
total: 0,
|
|
623
|
+
progress: 0,
|
|
624
|
+
});
|
|
625
|
+
searchResponse = {
|
|
626
|
+
objects: [],
|
|
627
|
+
total: 0,
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
);
|
|
454
631
|
|
|
455
632
|
it("控制台连接事件在数据过期时触发刷新", async () => {
|
|
456
633
|
const svc = app.get("console.services.market");
|
|
457
634
|
expect(svc).toBeDefined();
|
|
458
635
|
// 伪造一个在线客户端,使连接事件通过在线检查(broadcast 需可用的 socket)
|
|
459
|
-
const fakeClient = {
|
|
460
|
-
|
|
636
|
+
const fakeClient = {
|
|
637
|
+
id: "conn-1",
|
|
638
|
+
socket: { send() {} },
|
|
639
|
+
};
|
|
640
|
+
(app.console.clients as Record<string, unknown>)[
|
|
641
|
+
"conn-1"
|
|
642
|
+
] = fakeClient;
|
|
461
643
|
// 刚刷新过:12 小时窗口内直接返回,不重新收集
|
|
462
|
-
const timestamp = svc?.[
|
|
644
|
+
const timestamp = svc?.[
|
|
645
|
+
"_timestamp" as keyof typeof svc
|
|
646
|
+
] as number;
|
|
463
647
|
// console/connection 载荷声明为 Client,桩对象仅含在线检查所需的最小面,类型层断言穿透
|
|
464
648
|
app.emit("console/connection", fakeClient as never);
|
|
465
|
-
expect(
|
|
649
|
+
expect(
|
|
650
|
+
svc?.["_timestamp" as keyof typeof svc] as number,
|
|
651
|
+
).toBe(timestamp);
|
|
466
652
|
// 将时间戳回拨到窗口外,连接事件重新触发 start(异步监听,稍等)
|
|
467
653
|
(svc as Record<string, unknown>)["_timestamp"] = 0;
|
|
468
654
|
app.emit("console/connection", fakeClient as never);
|
|
469
655
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
470
656
|
expect(
|
|
471
|
-
(svc as Record<string, unknown>)[
|
|
657
|
+
(svc as Record<string, unknown>)[
|
|
658
|
+
"_timestamp"
|
|
659
|
+
] as number,
|
|
472
660
|
).toBeGreaterThan(0);
|
|
473
661
|
delete app.console.clients["conn-1"];
|
|
474
662
|
});
|
|
475
663
|
});
|
|
476
664
|
|
|
477
665
|
describe("market 聊天指令", () => {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
666
|
+
itQuiet(
|
|
667
|
+
"plugin.install 缺参与未找到的报错路径",
|
|
668
|
+
async () => {
|
|
669
|
+
const missing = await client.receive(
|
|
670
|
+
"plugin.install",
|
|
671
|
+
);
|
|
672
|
+
expect(missing[0]).toContain("请输入插件名。");
|
|
673
|
+
const notFound = await client.receive(
|
|
674
|
+
"plugin.install absent-pkg",
|
|
675
|
+
);
|
|
676
|
+
expect(notFound[0]).toContain("未找到该插件。");
|
|
677
|
+
},
|
|
678
|
+
);
|
|
484
679
|
|
|
485
680
|
it("plugin.install 已安装时提示重复", async () => {
|
|
486
|
-
const replies = await client.receive(
|
|
681
|
+
const replies = await client.receive(
|
|
682
|
+
"plugin.install demo",
|
|
683
|
+
);
|
|
487
684
|
expect(replies[0]).toContain("该插件已安装。");
|
|
488
685
|
});
|
|
489
686
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
687
|
+
itQuiet(
|
|
688
|
+
"plugin.install 安装新插件并写入依赖",
|
|
689
|
+
async () => {
|
|
690
|
+
nextExitCode = 0;
|
|
691
|
+
spawnCalls.length = 0;
|
|
692
|
+
const replies = await client.receive(
|
|
693
|
+
"plugin.install newpkg",
|
|
694
|
+
);
|
|
695
|
+
expect(replies[0]).toContain("安装成功!");
|
|
696
|
+
expect(spawnCalls.length).toBe(1);
|
|
697
|
+
const manifest = JSON.parse(
|
|
698
|
+
await Bun.file(join(tmp, "package.json")).text(),
|
|
699
|
+
) as { dependencies: Record<string, string> };
|
|
700
|
+
expect(
|
|
701
|
+
manifest.dependencies["koishi-plugin-newpkg"],
|
|
702
|
+
).toBe("1.0.0");
|
|
703
|
+
// 重启消息在安装完成后复位(Loader 与桩形状不同,经 unknown 二段式断言)
|
|
704
|
+
expect(
|
|
705
|
+
(app.loader as unknown as FakeLoader).envData[
|
|
706
|
+
"message"
|
|
707
|
+
],
|
|
708
|
+
).toBeNull();
|
|
709
|
+
},
|
|
710
|
+
15000,
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
itQuiet(
|
|
714
|
+
"plugin.uninstall 卸载依赖并从清单移除",
|
|
715
|
+
async () => {
|
|
716
|
+
nextExitCode = 0;
|
|
717
|
+
spawnCalls.length = 0;
|
|
718
|
+
const replies = await client.receive(
|
|
719
|
+
"plugin.uninstall newpkg",
|
|
720
|
+
);
|
|
721
|
+
expect(replies[0]).toContain("卸载成功!");
|
|
722
|
+
const manifest = JSON.parse(
|
|
723
|
+
await Bun.file(join(tmp, "package.json")).text(),
|
|
724
|
+
) as { dependencies: Record<string, string> };
|
|
725
|
+
expect(
|
|
726
|
+
manifest.dependencies["koishi-plugin-newpkg"],
|
|
727
|
+
).toBeUndefined();
|
|
728
|
+
},
|
|
729
|
+
15000,
|
|
730
|
+
);
|
|
514
731
|
|
|
515
732
|
it("plugin.uninstall 未安装时提示", async () => {
|
|
516
|
-
const replies = await client.receive(
|
|
733
|
+
const replies = await client.receive(
|
|
734
|
+
"plugin.uninstall absent-pkg",
|
|
735
|
+
);
|
|
517
736
|
expect(replies[0]).toContain("该插件未安装。");
|
|
518
737
|
});
|
|
519
738
|
|
|
@@ -524,48 +743,73 @@ describe("market 聊天指令", () => {
|
|
|
524
743
|
});
|
|
525
744
|
|
|
526
745
|
describe("market 进阶链路", () => {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
746
|
+
itQuiet("宿主配置不可写时不加载安装器", async () => {
|
|
747
|
+
// apply 的「仅告警并跳过」正是被测行为,静默 app 域避免预期告警刷屏
|
|
748
|
+
const levels = Logger.levels as Record<string, number>;
|
|
749
|
+
levels["app"] = 0;
|
|
750
|
+
try {
|
|
751
|
+
const appNoLoader = new App();
|
|
752
|
+
appNoLoader.plugin(http);
|
|
753
|
+
appNoLoader.plugin(
|
|
754
|
+
FakeConsole as unknown as Plugin.Constructor<TestApp>,
|
|
755
|
+
);
|
|
756
|
+
appNoLoader.plugin(market, {
|
|
757
|
+
registry: {
|
|
758
|
+
endpoint: `http://127.0.0.1:${registryServer.port}/`,
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
await appNoLoader.start();
|
|
762
|
+
// apply 在 loader 缺席时仅告警并提前返回
|
|
763
|
+
expect(appNoLoader.installer).toBeUndefined();
|
|
764
|
+
await appNoLoader.stop();
|
|
765
|
+
} finally {
|
|
766
|
+
delete levels["app"];
|
|
767
|
+
}
|
|
538
768
|
});
|
|
539
769
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
770
|
+
itQuiet(
|
|
771
|
+
"浏览器 market/install 监听器执行安装并刷新服务",
|
|
772
|
+
async () => {
|
|
773
|
+
nextExitCode = 0;
|
|
774
|
+
const listener =
|
|
775
|
+
app.console.listeners["market/install"];
|
|
776
|
+
expect(listener).toBeDefined();
|
|
777
|
+
const code = (await listener?.callback.call(
|
|
778
|
+
{} as never,
|
|
779
|
+
{ "koishi-plugin-newpkg": "1.0.0" },
|
|
780
|
+
true,
|
|
781
|
+
)) as number;
|
|
782
|
+
expect(code).toBe(0);
|
|
783
|
+
},
|
|
784
|
+
15000,
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
itQuiet(
|
|
788
|
+
"浏览器 market/registry 监听器批量查询包元数据",
|
|
789
|
+
async () => {
|
|
790
|
+
const listener =
|
|
791
|
+
app.console.listeners["market/registry"];
|
|
792
|
+
expect(listener).toBeDefined();
|
|
793
|
+
const meta = (await listener?.callback.call(
|
|
794
|
+
{} as never,
|
|
795
|
+
["koishi-plugin-demo", "koishi-plugin-missing"],
|
|
796
|
+
)) as Record<string, unknown>;
|
|
797
|
+
expect(
|
|
798
|
+
Object.keys(meta["koishi-plugin-demo"] ?? {}),
|
|
799
|
+
).toContain("2.0.0");
|
|
800
|
+
expect(meta["koishi-plugin-missing"]).toEqual({});
|
|
801
|
+
},
|
|
802
|
+
);
|
|
562
803
|
|
|
563
804
|
it("搜索结果中不兼容的包被跳过(analyze onSkipped/ignored)", async () => {
|
|
564
805
|
const svc = app.get("console.services.market");
|
|
565
806
|
// ghost 有 registry 条目,但版本声明的 koishi peer 与 4.x 不相交
|
|
566
807
|
registryData["koishi-plugin-ghost"] = {
|
|
567
808
|
versions: {
|
|
568
|
-
"1.0.0": {
|
|
809
|
+
"1.0.0": {
|
|
810
|
+
version: "1.0.0",
|
|
811
|
+
peerDependencies: { koishi: "^5.0.0" },
|
|
812
|
+
},
|
|
569
813
|
},
|
|
570
814
|
time: { "1.0.0": "2024-01-01T00:00:00Z" },
|
|
571
815
|
};
|
|
@@ -583,7 +827,9 @@ describe("market 进阶链路", () => {
|
|
|
583
827
|
};
|
|
584
828
|
await svc?.start(true);
|
|
585
829
|
// collect 对 analyze 为即发即忘,等待逐包分析完成
|
|
586
|
-
await new Promise((resolve) =>
|
|
830
|
+
await new Promise((resolve) =>
|
|
831
|
+
setTimeout(resolve, 300),
|
|
832
|
+
);
|
|
587
833
|
const payload = await svc?.get();
|
|
588
834
|
// 无兼容版本:对象标记 ignored,不进入数据缓存。
|
|
589
835
|
// progress 恒为 0:Scanner 以 defineProperty 定义 progress(不可写),
|
|
@@ -595,83 +841,135 @@ describe("market 进阶链路", () => {
|
|
|
595
841
|
delete registryData["koishi-plugin-ghost"];
|
|
596
842
|
});
|
|
597
843
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
844
|
+
itQuiet(
|
|
845
|
+
"搜索结果中被限流的包经重试后计入 failed(onFailure)",
|
|
846
|
+
async () => {
|
|
847
|
+
const svc = app.get("console.services.market");
|
|
848
|
+
searchResponse = {
|
|
849
|
+
objects: [
|
|
850
|
+
{
|
|
851
|
+
package: {
|
|
852
|
+
name: "koishi-plugin-ratelimited",
|
|
853
|
+
version: "1.0.0",
|
|
854
|
+
date: "2024-01-01T00:00:00Z",
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
total: 1,
|
|
859
|
+
};
|
|
860
|
+
await svc?.start(true);
|
|
861
|
+
// 等待限流重试(Retry-After 10ms × 3 次)与即发即忘的 analyze。
|
|
862
|
+
// 注意不能经 get() 断言:super.start() 会清空 _task,get() 触发的
|
|
863
|
+
// 二次 collect 会把 failed 重置(即发即忘的 analyze 尚未完成)。
|
|
864
|
+
await new Promise((resolve) =>
|
|
865
|
+
setTimeout(resolve, 400),
|
|
866
|
+
);
|
|
867
|
+
// 不可达/被限流的包名进入 failed 列表(上一个用例中 registry
|
|
868
|
+
// 条目已删除的 ghost 包经 404 路径同样落入此处)
|
|
869
|
+
const provider = svc as unknown as {
|
|
870
|
+
failed: string[];
|
|
871
|
+
};
|
|
872
|
+
expect(
|
|
873
|
+
provider.failed.some((name) =>
|
|
874
|
+
name.startsWith("koishi-plugin-"),
|
|
875
|
+
),
|
|
876
|
+
).toBe(true);
|
|
877
|
+
},
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
itQuiet(
|
|
881
|
+
"plugin.upgrade 检出可升级项并输出确认提示",
|
|
882
|
+
async () => {
|
|
883
|
+
// 本地放置旧版安装,使 resolved 有值且低于远端 latest
|
|
884
|
+
mkdirSync(
|
|
885
|
+
join(tmp, "node_modules", "koishi-plugin-demo"),
|
|
602
886
|
{
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
887
|
+
recursive: true,
|
|
888
|
+
},
|
|
889
|
+
);
|
|
890
|
+
writeFileSync(
|
|
891
|
+
join(
|
|
892
|
+
tmp,
|
|
893
|
+
"node_modules",
|
|
894
|
+
"koishi-plugin-demo",
|
|
895
|
+
"package.json",
|
|
896
|
+
),
|
|
897
|
+
JSON.stringify({
|
|
898
|
+
name: "koishi-plugin-demo",
|
|
899
|
+
version: "1.0.0",
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
app.installer.refresh();
|
|
903
|
+
|
|
904
|
+
nextExitCode = 0;
|
|
905
|
+
// 发出升级指令(异步等待确认),再以 Y 回复确认。
|
|
906
|
+
// 注:mock 环境下指令 ctx 对 loader 服务的可见性受 cordis
|
|
907
|
+
// isolate 语义限制(见仓库测试任务记录),确认后的安装段
|
|
908
|
+
// 由 market/install 监听器用例覆盖。
|
|
909
|
+
const question = client.receive(
|
|
910
|
+
"plugin.upgrade demo",
|
|
911
|
+
);
|
|
912
|
+
await new Promise((resolve) =>
|
|
913
|
+
setTimeout(resolve, 200),
|
|
914
|
+
);
|
|
915
|
+
await client.receive("Y");
|
|
916
|
+
const replies = await question;
|
|
917
|
+
const output = replies.join("\n");
|
|
918
|
+
expect(output).toContain("koishi-plugin-demo");
|
|
919
|
+
expect(output).toContain("1.0.0 -> 2.0.0");
|
|
920
|
+
},
|
|
921
|
+
20000,
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
itQuiet(
|
|
925
|
+
"plugin.upgrade 对本地畸形版本静默跳过(非法 semver catch)",
|
|
926
|
+
async () => {
|
|
927
|
+
// registry 侧提供合法 latest;本地安装产物的 version 是畸形串,
|
|
928
|
+
// request(清单声明)合法故不标记 invalid,gt(latest, resolved)
|
|
929
|
+
// 解析失败进入 catch 分支:该包被过滤,视为无可升级项
|
|
930
|
+
registryData["koishi-plugin-weird"] = {
|
|
931
|
+
versions: {
|
|
932
|
+
"2.0.0": {
|
|
933
|
+
version: "2.0.0",
|
|
934
|
+
peerDependencies: { koishi: "^4.17.0" },
|
|
607
935
|
},
|
|
608
936
|
},
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
expect(output).toContain("1.0.0 -> 2.0.0");
|
|
648
|
-
}, 20000);
|
|
649
|
-
|
|
650
|
-
it("plugin.upgrade 对本地畸形版本静默跳过(非法 semver catch)", async () => {
|
|
651
|
-
// registry 侧提供合法 latest;本地安装产物的 version 是畸形串,
|
|
652
|
-
// request(清单声明)合法故不标记 invalid,gt(latest, resolved)
|
|
653
|
-
// 解析失败进入 catch 分支:该包被过滤,视为无可升级项
|
|
654
|
-
registryData["koishi-plugin-weird"] = {
|
|
655
|
-
versions: {
|
|
656
|
-
"2.0.0": { version: "2.0.0", peerDependencies: { koishi: "^4.17.0" } },
|
|
657
|
-
},
|
|
658
|
-
};
|
|
659
|
-
mkdirSync(join(tmp, "node_modules", "koishi-plugin-weird"), {
|
|
660
|
-
recursive: true,
|
|
661
|
-
});
|
|
662
|
-
writeFileSync(
|
|
663
|
-
join(tmp, "node_modules", "koishi-plugin-weird", "package.json"),
|
|
664
|
-
JSON.stringify({ name: "koishi-plugin-weird", version: "not.a.version" }),
|
|
665
|
-
);
|
|
666
|
-
nextExitCode = 0;
|
|
667
|
-
// 经 install 注入清单声明(顺带刷新 Installer 的 manifest 快照)
|
|
668
|
-
const code = await app.installer.install({
|
|
669
|
-
"koishi-plugin-weird": "1.0.0",
|
|
670
|
-
});
|
|
671
|
-
expect(code).toBe(0);
|
|
672
|
-
const deps = await app.installer.getDeps();
|
|
673
|
-
expect(deps["koishi-plugin-weird"]?.resolved).toBe("not.a.version");
|
|
674
|
-
const replies = await client.receive("plugin.upgrade weird");
|
|
675
|
-
expect(replies[0]).toContain("所有插件已是最新版本。");
|
|
676
|
-
}, 20000);
|
|
937
|
+
};
|
|
938
|
+
mkdirSync(
|
|
939
|
+
join(tmp, "node_modules", "koishi-plugin-weird"),
|
|
940
|
+
{
|
|
941
|
+
recursive: true,
|
|
942
|
+
},
|
|
943
|
+
);
|
|
944
|
+
writeFileSync(
|
|
945
|
+
join(
|
|
946
|
+
tmp,
|
|
947
|
+
"node_modules",
|
|
948
|
+
"koishi-plugin-weird",
|
|
949
|
+
"package.json",
|
|
950
|
+
),
|
|
951
|
+
JSON.stringify({
|
|
952
|
+
name: "koishi-plugin-weird",
|
|
953
|
+
version: "not.a.version",
|
|
954
|
+
}),
|
|
955
|
+
);
|
|
956
|
+
nextExitCode = 0;
|
|
957
|
+
// 经 install 注入清单声明(顺带刷新 Installer 的 manifest 快照)
|
|
958
|
+
const code = await app.installer.install({
|
|
959
|
+
"koishi-plugin-weird": "1.0.0",
|
|
960
|
+
});
|
|
961
|
+
expect(code).toBe(0);
|
|
962
|
+
const deps = await app.installer.getDeps();
|
|
963
|
+
expect(deps["koishi-plugin-weird"]?.resolved).toBe(
|
|
964
|
+
"not.a.version",
|
|
965
|
+
);
|
|
966
|
+
const replies = await client.receive(
|
|
967
|
+
"plugin.upgrade weird",
|
|
968
|
+
);
|
|
969
|
+
expect(replies[0]).toContain(
|
|
970
|
+
"所有插件已是最新版本。",
|
|
971
|
+
);
|
|
972
|
+
},
|
|
973
|
+
20000,
|
|
974
|
+
);
|
|
677
975
|
});
|