@yunzai-ng/core 0.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/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:插件市场的索引解析、名称校验、安装与卸载测试
|
|
3
|
+
* 依赖方向:测试文件,依赖 plugin/market、plugin/tar、testing/fake
|
|
4
|
+
* 生命周期:每个用例一套临时目录与一个市场实例,afterEach 删除目录
|
|
5
|
+
* 注意事项:HTTP 由本文件内的替身提供,索引与归档都是现场构造的字节。安装路径不触及
|
|
6
|
+
* 真实网络:声明 `tarball` 来源的条目走归档,声明 `git` 来源的条目由
|
|
7
|
+
* `MarketDeps.git` 的替身接管,**故一律不起真实的 git 子进程**。
|
|
8
|
+
* 就地拉取那条路验的是**下发了哪些命令、以什么次序** —— 真实的 git 只能
|
|
9
|
+
* 告诉你「最后目录对了」,而次序错掉(先 reset 后 stash)同样能得到一个
|
|
10
|
+
* 对的目录,直到某天吃掉使用者的改动。故此处逐条比对参数数组。
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir, readFile, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
13
|
+
import { tmpdir } from "node:os"
|
|
14
|
+
import { join } from "node:path"
|
|
15
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
16
|
+
import type { HttpClient } from "@yunzai-ng/types"
|
|
17
|
+
import { isDirectory, isFile } from "../util/fs.js"
|
|
18
|
+
import { fakeLogger, type FakeLogger } from "../testing/fake.js"
|
|
19
|
+
import { buildTarGz } from "./tar.js"
|
|
20
|
+
import {
|
|
21
|
+
PluginMarket,
|
|
22
|
+
applyMirror,
|
|
23
|
+
assertPluginName,
|
|
24
|
+
compareVersion,
|
|
25
|
+
parseIndex,
|
|
26
|
+
tarballFromGit,
|
|
27
|
+
type GitRunner,
|
|
28
|
+
type MarketSettings
|
|
29
|
+
} from "./market.js"
|
|
30
|
+
|
|
31
|
+
/** 一个可编程的响应 */
|
|
32
|
+
interface StubReply {
|
|
33
|
+
/** JSON 响应体 */
|
|
34
|
+
json?: unknown
|
|
35
|
+
/** 字节响应体 */
|
|
36
|
+
bytes?: Uint8Array
|
|
37
|
+
/** 令该地址失败 */
|
|
38
|
+
error?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** HTTP 替身与其调用记录 */
|
|
42
|
+
interface StubHttp {
|
|
43
|
+
/** 客户端 */
|
|
44
|
+
http: HttpClient
|
|
45
|
+
/** 依次请求过的地址 */
|
|
46
|
+
calls: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 造一个只认识给定地址的 HTTP 客户端
|
|
51
|
+
* @param routes 地址到响应的映射
|
|
52
|
+
* @returns 客户端与调用记录
|
|
53
|
+
*/
|
|
54
|
+
function stubHttp(routes: Record<string, StubReply>): StubHttp {
|
|
55
|
+
const calls: string[] = []
|
|
56
|
+
/** 取一条路由 */
|
|
57
|
+
const pick = (url: string): StubReply => {
|
|
58
|
+
calls.push(url)
|
|
59
|
+
const reply = routes[url]
|
|
60
|
+
if (reply === undefined) throw new Error(`替身未配置该地址:${url}`)
|
|
61
|
+
if (reply.error !== undefined) throw new Error(reply.error)
|
|
62
|
+
return reply
|
|
63
|
+
}
|
|
64
|
+
const client = {
|
|
65
|
+
request: () => {
|
|
66
|
+
throw new Error("用例不应调用 request")
|
|
67
|
+
},
|
|
68
|
+
get: async (url: string) => pick(url).json,
|
|
69
|
+
post: () => {
|
|
70
|
+
throw new Error("用例不应调用 post")
|
|
71
|
+
},
|
|
72
|
+
buffer: async (url: string) => pick(url).bytes ?? new Uint8Array(),
|
|
73
|
+
download: () => {
|
|
74
|
+
throw new Error("用例不应调用 download")
|
|
75
|
+
},
|
|
76
|
+
extend: () => client
|
|
77
|
+
}
|
|
78
|
+
return { http: client as unknown as HttpClient, calls }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 一次 git 调用的全部入参 */
|
|
82
|
+
interface GitCall {
|
|
83
|
+
/** 命令参数 */
|
|
84
|
+
args: readonly string[]
|
|
85
|
+
/** 工作目录 */
|
|
86
|
+
cwd: string
|
|
87
|
+
/** 超时毫秒 */
|
|
88
|
+
timeout: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** git 替身的可编程行为 */
|
|
92
|
+
interface StubGitOptions {
|
|
93
|
+
/** `rev-parse HEAD` 依次返回的提交号,用尽后重复最后一个 */
|
|
94
|
+
heads?: readonly string[]
|
|
95
|
+
/** `status --porcelain` 的输出,非空即视为工作区有改动 */
|
|
96
|
+
status?: string
|
|
97
|
+
/** 令某个子命令失败 */
|
|
98
|
+
fail?: { cmd: string; message: string }
|
|
99
|
+
/** 某条命令执行后的副作用,用于模拟 reset 改写了工作区 */
|
|
100
|
+
effect?: (args: readonly string[]) => Promise<void> | void
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** git 替身与其调用记录 */
|
|
104
|
+
interface StubGit {
|
|
105
|
+
/** 注入 `MarketDeps.git` 的执行器 */
|
|
106
|
+
run: GitRunner
|
|
107
|
+
/** 依次收到的调用 */
|
|
108
|
+
calls: GitCall[]
|
|
109
|
+
/** 依次收到的子命令名,用于比对次序 */
|
|
110
|
+
cmds: () => string[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 造一个不起子进程的 git
|
|
115
|
+
*
|
|
116
|
+
* 只认识就地拉取用到的那几条命令。遇到未编程的命令**抛错而非返回空串** ——
|
|
117
|
+
* 静默返回会让「多下发了一条本不该有的命令」这类错误从用例里溜过去。
|
|
118
|
+
* @param options 可编程行为
|
|
119
|
+
* @returns 执行器与调用记录
|
|
120
|
+
*/
|
|
121
|
+
function stubGit(options: StubGitOptions = {}): StubGit {
|
|
122
|
+
const calls: GitCall[] = []
|
|
123
|
+
const heads = [...(options.heads ?? ["1111111aaa", "2222222bbb"])]
|
|
124
|
+
const run: GitRunner = async (args, cwd, timeout) => {
|
|
125
|
+
calls.push({ args: [...args], cwd, timeout })
|
|
126
|
+
const cmd = args[0]
|
|
127
|
+
if (options.fail !== undefined && options.fail.cmd === cmd) throw new Error(options.fail.message)
|
|
128
|
+
await options.effect?.(args)
|
|
129
|
+
if (cmd === "rev-parse") return `${heads.length > 1 ? heads.shift() : heads[0]}\n`
|
|
130
|
+
if (cmd === "status") return options.status ?? ""
|
|
131
|
+
if (cmd === "--version") return "git version 2.44.0\n"
|
|
132
|
+
if (cmd === "stash" || cmd === "fetch" || cmd === "reset" || cmd === "clone") return ""
|
|
133
|
+
throw new Error(`替身未编程该命令:git ${args.join(" ")}`)
|
|
134
|
+
}
|
|
135
|
+
return { run, calls, cmds: () => calls.map(item => item.args[0] ?? "") }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 一套市场及其周边 */
|
|
139
|
+
interface Harness {
|
|
140
|
+
/** 市场 */
|
|
141
|
+
market: PluginMarket
|
|
142
|
+
/** 插件目录 */
|
|
143
|
+
pluginsDir: string
|
|
144
|
+
/** 索引缓存文件 */
|
|
145
|
+
cacheFile: string
|
|
146
|
+
/** 日志 */
|
|
147
|
+
logger: FakeLogger
|
|
148
|
+
/** HTTP 调用记录 */
|
|
149
|
+
calls: string[]
|
|
150
|
+
/** 根临时目录 */
|
|
151
|
+
root: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let harness: Harness | undefined
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 起一套市场
|
|
158
|
+
* @param routes HTTP 替身路由
|
|
159
|
+
* @param settings 配置覆盖
|
|
160
|
+
* @param reuse 复用已有的临时目录(用于验证磁盘缓存跨实例生效)
|
|
161
|
+
* @param git git 执行器替身,不给则连接一个「本机没有 git」的桩
|
|
162
|
+
* @returns 市场与周边
|
|
163
|
+
*/
|
|
164
|
+
async function makeHarness(
|
|
165
|
+
routes: Record<string, StubReply>,
|
|
166
|
+
settings: Partial<MarketSettings> = {},
|
|
167
|
+
reuse?: Harness,
|
|
168
|
+
git?: GitRunner
|
|
169
|
+
): Promise<Harness> {
|
|
170
|
+
const root = reuse?.root ?? (await mkdtemp(join(tmpdir(), "yzng-market-")))
|
|
171
|
+
const pluginsDir = join(root, "plugins")
|
|
172
|
+
const tempDir = join(root, "temp")
|
|
173
|
+
const cacheFile = join(root, "cache", "market.json")
|
|
174
|
+
await mkdir(pluginsDir, { recursive: true })
|
|
175
|
+
const logger = reuse?.logger ?? fakeLogger()
|
|
176
|
+
const { http, calls } = stubHttp(routes)
|
|
177
|
+
const market = new PluginMarket({
|
|
178
|
+
http,
|
|
179
|
+
logger,
|
|
180
|
+
pluginsDir,
|
|
181
|
+
tempDir,
|
|
182
|
+
cacheFile,
|
|
183
|
+
coreVersion: "1.0.0",
|
|
184
|
+
settings: () => ({
|
|
185
|
+
sources: ["https://example.com/index.json"],
|
|
186
|
+
mirror: "",
|
|
187
|
+
cacheTtl: 60_000,
|
|
188
|
+
timeout: 5000,
|
|
189
|
+
...settings
|
|
190
|
+
}),
|
|
191
|
+
/*
|
|
192
|
+
* 不给替身的用例一律当作「本机没有 git」
|
|
193
|
+
*
|
|
194
|
+
* 缺省若落到真实的 git 上,同一份用例在装了 git 的机器上走克隆、在没装的机器上走
|
|
195
|
+
* 归档 —— 两条完全不同的代码路径,而用例里看不出走的是哪条。抛错即令 `#hasGit()`
|
|
196
|
+
* 记下 false,归档那条路由此成为确定的行为。
|
|
197
|
+
*/
|
|
198
|
+
git: git ?? (() => Promise.reject(new Error("用例未注入 git 替身")))
|
|
199
|
+
})
|
|
200
|
+
harness = { market, pluginsDir, cacheFile, logger, calls, root }
|
|
201
|
+
return harness
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
afterEach(async () => {
|
|
205
|
+
if (harness !== undefined) await rm(harness.root, { recursive: true, force: true })
|
|
206
|
+
harness = undefined
|
|
207
|
+
vi.restoreAllMocks()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe("assertPluginName", () => {
|
|
211
|
+
it("接受常规名称", () => {
|
|
212
|
+
expect(assertPluginName("mhy-game-plugin")).toBe("mhy-game-plugin")
|
|
213
|
+
expect(assertPluginName("adapter.napcat")).toBe("adapter.napcat")
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("拒绝路径分隔符、上级目录与保留名", () => {
|
|
217
|
+
for (const bad of ["../evil", "a/b", "a\\b", "..", ".hidden", "node_modules", "", "-leading"]) {
|
|
218
|
+
expect(() => assertPluginName(bad), bad).toThrow()
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("compareVersion", () => {
|
|
224
|
+
it("按数值段比较,位数不同也能比", () => {
|
|
225
|
+
expect(compareVersion("1.2.0", "1.10.0")).toBeLessThan(0)
|
|
226
|
+
expect(compareVersion("2.0", "1.9.9")).toBeGreaterThan(0)
|
|
227
|
+
expect(compareVersion("1.0.0", "1.0.0")).toBe(0)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("忽略预发布标识", () => {
|
|
231
|
+
expect(compareVersion("1.0.0-beta.1", "1.0.0")).toBe(0)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe("applyMirror", () => {
|
|
236
|
+
it("只对白名单主机套前缀", () => {
|
|
237
|
+
expect(applyMirror("https://github.com/a/b", "https://gh-proxy.org/")).toBe("https://gh-proxy.org/https://github.com/a/b")
|
|
238
|
+
expect(applyMirror("https://example.com/i.json", "https://gh-proxy.org/")).toBe("https://example.com/i.json")
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("前缀为空或地址不可解析时原样返回", () => {
|
|
242
|
+
expect(applyMirror("https://github.com/a/b", " ")).toBe("https://github.com/a/b")
|
|
243
|
+
expect(applyMirror("不是地址", "https://gh-proxy.org")).toBe("不是地址")
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
describe("tarballFromGit", () => {
|
|
248
|
+
it("由 GitHub 仓库地址推出 codeload 归档地址", () => {
|
|
249
|
+
expect(tarballFromGit("https://github.com/a/b.git", "main")).toBe("https://codeload.github.com/a/b/tar.gz/main")
|
|
250
|
+
expect(tarballFromGit("https://github.com/a/b")).toBe("https://codeload.github.com/a/b/tar.gz/HEAD")
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("非 GitHub 地址无法推导", () => {
|
|
254
|
+
expect(tarballFromGit("https://gitee.com/a/b.git")).toBeUndefined()
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe("parseIndex", () => {
|
|
259
|
+
it("接受 { plugins: [...] } 与顶层数组两种形状", () => {
|
|
260
|
+
const one = { name: "a", install: { type: "git", url: "https://github.com/a/a" } }
|
|
261
|
+
expect(parseIndex({ plugins: [one] }, "s")).toHaveLength(1)
|
|
262
|
+
expect(parseIndex([one], "s")).toHaveLength(1)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("补齐可选字段,标题缺省取插件名", () => {
|
|
266
|
+
const [entry] = parseIndex(
|
|
267
|
+
[{ name: "demo", install: { type: "tarball", url: "https://example.com/d.tar.gz" } }],
|
|
268
|
+
"s"
|
|
269
|
+
)
|
|
270
|
+
expect(entry).toMatchObject({ name: "demo", title: "demo", description: "", tags: [], official: false, source: "s" })
|
|
271
|
+
expect(entry?.author).toBeUndefined()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it("整条丢弃不合法的记录,而不是补默认值", () => {
|
|
275
|
+
const entries = parseIndex(
|
|
276
|
+
[
|
|
277
|
+
{ install: { type: "git", url: "https://github.com/a/a" } },
|
|
278
|
+
{ name: "../evil", install: { type: "git", url: "https://github.com/a/a" } },
|
|
279
|
+
{ name: "nourl", install: { type: "git" } },
|
|
280
|
+
{ name: "badtype", install: { type: "svn", url: "https://example.com/x" } },
|
|
281
|
+
{ name: "badproto", install: { type: "tarball", url: "file:///etc/passwd" } },
|
|
282
|
+
42,
|
|
283
|
+
{ name: "good", install: { type: "git", url: "https://github.com/a/a" } }
|
|
284
|
+
],
|
|
285
|
+
"s"
|
|
286
|
+
)
|
|
287
|
+
expect(entries.map(item => item.name)).toEqual(["good"])
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it("tags 中的非字符串项被剔除", () => {
|
|
291
|
+
const [entry] = parseIndex(
|
|
292
|
+
[{ name: "a", tags: ["工具", 1, null], install: { type: "git", url: "https://github.com/a/a" } }],
|
|
293
|
+
"s"
|
|
294
|
+
)
|
|
295
|
+
expect(entry?.tags).toEqual(["工具"])
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it("文档形状完全不符时抛错", () => {
|
|
299
|
+
expect(() => parseIndex({ items: [] }, "s")).toThrow("索引格式不符")
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
/** 一份最小索引 */
|
|
304
|
+
const INDEX = {
|
|
305
|
+
version: 1,
|
|
306
|
+
plugins: [
|
|
307
|
+
{
|
|
308
|
+
name: "demo",
|
|
309
|
+
title: "示例插件",
|
|
310
|
+
description: "用于测试",
|
|
311
|
+
version: "1.0.0",
|
|
312
|
+
tags: ["示例"],
|
|
313
|
+
official: true,
|
|
314
|
+
install: { type: "tarball", url: "https://example.com/demo.tar.gz" }
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
describe("PluginMarket 索引", () => {
|
|
320
|
+
it("列出条目并标注安装状态", async () => {
|
|
321
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: INDEX } })
|
|
322
|
+
|
|
323
|
+
const first = await h.market.list()
|
|
324
|
+
expect(first.plugins.map(item => item.name)).toEqual(["demo"])
|
|
325
|
+
expect(first.plugins[0]).toMatchObject({ title: "示例插件", official: true, installed: false })
|
|
326
|
+
expect(first.sources).toEqual([{ url: "https://example.com/index.json", ok: true, count: 1 }])
|
|
327
|
+
|
|
328
|
+
await mkdir(join(h.pluginsDir, "demo"), { recursive: true })
|
|
329
|
+
expect((await h.market.list()).plugins[0]?.installed).toBe(true)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("缓存未过期时不再发请求,force 才重新获取", async () => {
|
|
333
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: INDEX } })
|
|
334
|
+
|
|
335
|
+
await h.market.list()
|
|
336
|
+
await h.market.list()
|
|
337
|
+
expect(h.calls).toHaveLength(1)
|
|
338
|
+
|
|
339
|
+
const forced = await h.market.list(true)
|
|
340
|
+
expect(h.calls).toHaveLength(2)
|
|
341
|
+
expect(forced.cached).toBe(false)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it("多个索引按顺序合并,同名条目以靠前的源为准", async () => {
|
|
345
|
+
const h = await makeHarness(
|
|
346
|
+
{
|
|
347
|
+
"https://a.example.com/i.json": {
|
|
348
|
+
json: [{ name: "demo", title: "私有源", install: { type: "git", url: "https://github.com/a/a" } }]
|
|
349
|
+
},
|
|
350
|
+
"https://b.example.com/i.json": {
|
|
351
|
+
json: [
|
|
352
|
+
{ name: "demo", title: "官方源", install: { type: "git", url: "https://github.com/b/b" } },
|
|
353
|
+
{ name: "other", install: { type: "git", url: "https://github.com/b/c" } }
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{ sources: ["https://a.example.com/i.json", "https://b.example.com/i.json"] }
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
const snapshot = await h.market.list()
|
|
361
|
+
expect(snapshot.plugins.map(item => item.name)).toEqual(["demo", "other"])
|
|
362
|
+
expect(snapshot.plugins.find(item => item.name === "demo")?.title).toBe("私有源")
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it("单个索引失败时记录原因,其余索引照常贡献条目", async () => {
|
|
366
|
+
const h = await makeHarness(
|
|
367
|
+
{
|
|
368
|
+
"https://bad.example.com/i.json": { error: "连接被拒绝" },
|
|
369
|
+
"https://ok.example.com/i.json": { json: INDEX }
|
|
370
|
+
},
|
|
371
|
+
{ sources: ["https://bad.example.com/i.json", "https://ok.example.com/i.json"] }
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const snapshot = await h.market.list()
|
|
375
|
+
expect(snapshot.plugins).toHaveLength(1)
|
|
376
|
+
expect(snapshot.sources[0]).toMatchObject({ ok: false, error: "连接被拒绝", count: 0 })
|
|
377
|
+
expect(h.logger.lines.some(line => line.includes("索引获取失败"))).toBe(true)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it("全部索引不可达时沿用磁盘缓存", async () => {
|
|
381
|
+
const first = await makeHarness({ "https://example.com/index.json": { json: INDEX } })
|
|
382
|
+
await first.market.list()
|
|
383
|
+
expect(await isFile(first.cacheFile)).toBe(true)
|
|
384
|
+
|
|
385
|
+
// 新实例共用同一份缓存文件:模拟重启后断网打开面板
|
|
386
|
+
const second = await makeHarness({ "https://example.com/index.json": { error: "网络不可达" } }, { cacheTtl: 0 }, first)
|
|
387
|
+
const snapshot = await second.market.list()
|
|
388
|
+
|
|
389
|
+
expect(snapshot.plugins.map(item => item.name)).toEqual(["demo"])
|
|
390
|
+
expect(snapshot.sources[0]?.ok).toBe(false)
|
|
391
|
+
expect(second.logger.lines.some(line => line.includes("沿用上次缓存"))).toBe(true)
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
/** 归档地址 */
|
|
396
|
+
const TARBALL = "https://example.com/demo.tar.gz"
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 造一份带外层目录的插件归档
|
|
400
|
+
* @param manifest package.json 内容,undefined 表示不含该文件
|
|
401
|
+
* @param marker 写入 index.js 的标记内容
|
|
402
|
+
* @returns gzip 压缩的归档
|
|
403
|
+
*/
|
|
404
|
+
function pluginArchive(manifest: Record<string, unknown> | undefined, marker = "v1"): Uint8Array {
|
|
405
|
+
const files: { path: string; content: string }[] = [{ path: "demo-main/index.js", content: `export default "${marker}"` }]
|
|
406
|
+
if (manifest !== undefined) files.unshift({ path: "demo-main/package.json", content: JSON.stringify(manifest) })
|
|
407
|
+
return buildTarGz(files)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
describe("PluginMarket 安装", () => {
|
|
411
|
+
it("解包到插件目录,版本取自 package.json,并提示需要安装依赖", async () => {
|
|
412
|
+
const h = await makeHarness({
|
|
413
|
+
"https://example.com/index.json": { json: INDEX },
|
|
414
|
+
[TARBALL]: { bytes: pluginArchive({ name: "demo", version: "1.2.3", dependencies: { lodash: "^4" } }) }
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const result = await h.market.install("demo")
|
|
418
|
+
|
|
419
|
+
expect(result).toMatchObject({ name: "demo", via: "tarball", version: "1.2.3", needsDependencies: true })
|
|
420
|
+
/*
|
|
421
|
+
* 归档装出来的目录没有 `.git`,故此后的更新是整目录重下
|
|
422
|
+
*
|
|
423
|
+
* 这一条钉的是「装完就说清此后更新的代价」:归档那条路每次更新都要重装依赖,
|
|
424
|
+
* 而 git 那条路保住 `node_modules`。说反了会让使用者对着一个几十兆的重装等半天。
|
|
425
|
+
*/
|
|
426
|
+
expect(result.updatable).toBe("reinstall")
|
|
427
|
+
expect(result.dir).toBe(join(h.pluginsDir, "demo"))
|
|
428
|
+
expect(await readFile(join(h.pluginsDir, "demo", "index.js"), "utf8")).toBe('export default "v1"')
|
|
429
|
+
expect(await isFile(join(h.pluginsDir, "demo", "package.json"))).toBe(true)
|
|
430
|
+
expect(h.logger.lines.some(line => line.includes("声明了运行时依赖"))).toBe(true)
|
|
431
|
+
// 外层的 demo-main/ 必须被剥掉,否则入口文件深一层,宿主扫不到
|
|
432
|
+
expect(await isDirectory(join(h.pluginsDir, "demo", "demo-main"))).toBe(false)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it("没有 package.json 时版本退回索引声明,且不提示依赖", async () => {
|
|
436
|
+
const h = await makeHarness({
|
|
437
|
+
"https://example.com/index.json": { json: INDEX },
|
|
438
|
+
[TARBALL]: { bytes: pluginArchive(undefined) }
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const result = await h.market.install("demo")
|
|
442
|
+
expect(result).toMatchObject({ version: "1.0.0", needsDependencies: false })
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it("目标已存在时拒绝安装,update 才覆盖", async () => {
|
|
446
|
+
const h = await makeHarness({
|
|
447
|
+
"https://example.com/index.json": { json: INDEX },
|
|
448
|
+
[TARBALL]: { bytes: pluginArchive({ version: "2.0.0" }, "v2") }
|
|
449
|
+
})
|
|
450
|
+
await mkdir(join(h.pluginsDir, "demo"), { recursive: true })
|
|
451
|
+
await writeFile(join(h.pluginsDir, "demo", "旧文件.txt"), "旧内容", "utf8")
|
|
452
|
+
|
|
453
|
+
await expect(h.market.install("demo")).rejects.toThrow("已安装")
|
|
454
|
+
|
|
455
|
+
const result = await h.market.update("demo")
|
|
456
|
+
expect(result.version).toBe("2.0.0")
|
|
457
|
+
expect(await readFile(join(h.pluginsDir, "demo", "index.js"), "utf8")).toBe('export default "v2"')
|
|
458
|
+
// 覆盖是整体替换:旧版本残留的文件不应当留在新版本目录里
|
|
459
|
+
expect(await isFile(join(h.pluginsDir, "demo", "旧文件.txt"))).toBe(false)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it("非 GitHub 的纯 git 来源在本机没有 git 时明确报错,而不是静默失败", async () => {
|
|
463
|
+
/*
|
|
464
|
+
* GitHub 的 git 地址可换算出 codeload 归档,故缺 git 仍装得上;而其余托管站
|
|
465
|
+
* 换算不出来 —— 那时确实无路可走,须当场说清,且话里要点出「未安装 git」这个
|
|
466
|
+
* 真实原因。静默失败会让用户以为是网络问题,转而反复重试。
|
|
467
|
+
*/
|
|
468
|
+
const h = await makeHarness({
|
|
469
|
+
"https://example.com/index.json": {
|
|
470
|
+
json: { plugins: [{ name: "gitonly", install: { type: "git", url: "https://gitee.com/a/gitonly" } }] }
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
await expect(h.market.install("gitonly")).rejects.toThrow(/仅提供 git 来源.*未安装 git/)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it("内核版本不满足 minCore 时拒绝安装", async () => {
|
|
478
|
+
const h = await makeHarness({
|
|
479
|
+
"https://example.com/index.json": {
|
|
480
|
+
json: { plugins: [{ ...INDEX.plugins[0], minCore: "9.0.0" }] }
|
|
481
|
+
},
|
|
482
|
+
[TARBALL]: { bytes: pluginArchive({ version: "1.0.0" }) }
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
await expect(h.market.install("demo")).rejects.toThrow("要求内核版本不低于 9.0.0")
|
|
486
|
+
expect(await isDirectory(join(h.pluginsDir, "demo"))).toBe(false)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it("索引中没有的插件与不合法的名称都拒绝", async () => {
|
|
490
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: INDEX } })
|
|
491
|
+
|
|
492
|
+
await expect(h.market.install("ghost")).rejects.toThrow("没有名为 ghost")
|
|
493
|
+
await expect(h.market.install("../evil")).rejects.toThrow("不合法")
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it("取到的内容不像插件时拒绝,插件目录保持原状", async () => {
|
|
497
|
+
const h = await makeHarness({
|
|
498
|
+
"https://example.com/index.json": { json: INDEX },
|
|
499
|
+
[TARBALL]: { bytes: buildTarGz([{ path: "demo-main/README.md", content: "# 文档" }]) }
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await expect(h.market.install("demo")).rejects.toThrow("不像插件")
|
|
503
|
+
expect(await isDirectory(join(h.pluginsDir, "demo"))).toBe(false)
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it("下载失败时不留下半个插件目录", async () => {
|
|
507
|
+
const h = await makeHarness({
|
|
508
|
+
"https://example.com/index.json": { json: INDEX },
|
|
509
|
+
[TARBALL]: { error: "连接超时" }
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
await expect(h.market.install("demo")).rejects.toThrow("连接超时")
|
|
513
|
+
expect(await isDirectory(join(h.pluginsDir, "demo"))).toBe(false)
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
/** 一份声明 git 来源的索引 */
|
|
518
|
+
const GIT_INDEX = {
|
|
519
|
+
version: 1,
|
|
520
|
+
plugins: [
|
|
521
|
+
{
|
|
522
|
+
name: "demo",
|
|
523
|
+
title: "示例插件",
|
|
524
|
+
version: "1.0.0",
|
|
525
|
+
install: { type: "git", url: "https://github.com/demo/demo.git" }
|
|
526
|
+
}
|
|
527
|
+
]
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** git 归档退路的地址,由上面那个仓库地址换算而来 */
|
|
531
|
+
const CODELOAD = "https://codeload.github.com/demo/demo/tar.gz/HEAD"
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* 造一个「由 git 装来的」插件目录
|
|
535
|
+
*
|
|
536
|
+
* 关键在于 `.git` 与 `node_modules` 两处:前者是就地拉取的判据,后者是这一批要保住的
|
|
537
|
+
* 东西 —— 它由使用者自己 `pnpm install` 装出,重装一次插件就要在国内网络下重来一遍。
|
|
538
|
+
* @param pluginsDir 插件根目录
|
|
539
|
+
* @param version 现装版本
|
|
540
|
+
* @returns 插件目录路径
|
|
541
|
+
*/
|
|
542
|
+
async function makeGitRepo(pluginsDir: string, version = "1.0.0"): Promise<string> {
|
|
543
|
+
const dir = join(pluginsDir, "demo")
|
|
544
|
+
await mkdir(join(dir, ".git"), { recursive: true })
|
|
545
|
+
await mkdir(join(dir, "node_modules", "lodash"), { recursive: true })
|
|
546
|
+
await writeFile(join(dir, "package.json"), JSON.stringify({ name: "demo", version }), "utf8")
|
|
547
|
+
await writeFile(join(dir, "index.js"), 'export default "v1"', "utf8")
|
|
548
|
+
return dir
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
describe("PluginMarket 就地拉取", () => {
|
|
552
|
+
it("目录是 git 仓库时就地拉取,命令次序为 fetch 而后 reset,且不动 node_modules", async () => {
|
|
553
|
+
const git = stubGit({
|
|
554
|
+
effect: async args => {
|
|
555
|
+
// reset 之后工作区已是新版本:模拟远端把版本号提到了 1.1.0
|
|
556
|
+
if (args[0] === "reset") {
|
|
557
|
+
await writeFile(join(harness!.pluginsDir, "demo", "package.json"), JSON.stringify({ version: "1.1.0" }), "utf8")
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
562
|
+
const dir = await makeGitRepo(h.pluginsDir)
|
|
563
|
+
|
|
564
|
+
const result = await h.market.update("demo")
|
|
565
|
+
|
|
566
|
+
expect(result).toMatchObject({
|
|
567
|
+
name: "demo",
|
|
568
|
+
dir,
|
|
569
|
+
via: "pull",
|
|
570
|
+
version: "1.1.0",
|
|
571
|
+
fromVersion: "1.0.0",
|
|
572
|
+
changed: true
|
|
573
|
+
})
|
|
574
|
+
// 整条序列逐字钉住:多一条、少一条、次序错一处都算失败
|
|
575
|
+
expect(git.cmds()).toEqual(["--version", "rev-parse", "status", "fetch", "reset", "rev-parse"])
|
|
576
|
+
// 那份依赖是这一批的全部意义所在
|
|
577
|
+
expect(await isDirectory(join(dir, "node_modules", "lodash"))).toBe(true)
|
|
578
|
+
expect(h.logger.lines.some(line => line.includes("已就地更新至 1.1.0"))).toBe(true)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it("拉取用 fetch --depth 1 加 reset --hard FETCH_HEAD,绝不用 pull", async () => {
|
|
582
|
+
const git = stubGit()
|
|
583
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
584
|
+
await makeGitRepo(h.pluginsDir)
|
|
585
|
+
|
|
586
|
+
await h.market.update("demo")
|
|
587
|
+
|
|
588
|
+
expect(git.cmds()).not.toContain("pull")
|
|
589
|
+
const fetch = git.calls.find(item => item.args[0] === "fetch")
|
|
590
|
+
expect(fetch?.args).toEqual(["fetch", "--depth", "1", "https://github.com/demo/demo.git", "HEAD"])
|
|
591
|
+
expect(git.calls.find(item => item.args[0] === "reset")?.args).toEqual(["reset", "--hard", "FETCH_HEAD"])
|
|
592
|
+
// 含网络往返的那一条才给长超时,只读本地的几条不该跟着放宽
|
|
593
|
+
expect(fetch?.timeout).toBe(5 * 60 * 1000)
|
|
594
|
+
expect(git.calls.find(item => item.args[0] === "reset")?.timeout).toBe(10_000)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it("fetch 的地址每次由镜像前缀现算,不沿用目录里的 origin", async () => {
|
|
598
|
+
const git = stubGit()
|
|
599
|
+
const h = await makeHarness(
|
|
600
|
+
{ "https://example.com/index.json": { json: GIT_INDEX } },
|
|
601
|
+
{ mirror: "https://gh-proxy.org/" },
|
|
602
|
+
undefined,
|
|
603
|
+
git.run
|
|
604
|
+
)
|
|
605
|
+
await makeGitRepo(h.pluginsDir)
|
|
606
|
+
|
|
607
|
+
await h.market.update("demo")
|
|
608
|
+
|
|
609
|
+
expect(git.calls.find(item => item.args[0] === "fetch")?.args[3]).toBe(
|
|
610
|
+
"https://gh-proxy.org/https://github.com/demo/demo.git"
|
|
611
|
+
)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it("索引声明分支时按该分支拉取", async () => {
|
|
615
|
+
const git = stubGit()
|
|
616
|
+
const h = await makeHarness(
|
|
617
|
+
{
|
|
618
|
+
"https://example.com/index.json": {
|
|
619
|
+
json: { plugins: [{ ...GIT_INDEX.plugins[0], install: { type: "git", url: "https://github.com/demo/demo.git", branch: "dev" } }] }
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{},
|
|
623
|
+
undefined,
|
|
624
|
+
git.run
|
|
625
|
+
)
|
|
626
|
+
await makeGitRepo(h.pluginsDir)
|
|
627
|
+
|
|
628
|
+
await h.market.update("demo")
|
|
629
|
+
|
|
630
|
+
expect(git.calls.find(item => item.args[0] === "fetch")?.args[4]).toBe("dev")
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it("提交号没变时 changed 为假,日志说已是最新而非已更新", async () => {
|
|
634
|
+
const git = stubGit({ heads: ["1111111aaa"] })
|
|
635
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
636
|
+
await makeGitRepo(h.pluginsDir)
|
|
637
|
+
|
|
638
|
+
const result = await h.market.update("demo")
|
|
639
|
+
|
|
640
|
+
expect(result).toMatchObject({ via: "pull", changed: false, version: "1.0.0" })
|
|
641
|
+
expect(h.logger.lines.some(line => line.includes("已是最新版本"))).toBe(true)
|
|
642
|
+
expect(h.logger.lines.some(line => line.includes("已就地更新"))).toBe(false)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it("工作区有改动时先 stash 再 reset,且给出取回办法", async () => {
|
|
646
|
+
const git = stubGit({ status: " M index.js\n?? 我的笔记.txt\n" })
|
|
647
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
648
|
+
await makeGitRepo(h.pluginsDir)
|
|
649
|
+
|
|
650
|
+
await h.market.update("demo")
|
|
651
|
+
|
|
652
|
+
const cmds = git.cmds()
|
|
653
|
+
// 次序即安全性:reset --hard 会覆盖工作区,暂存必须发生在它之前
|
|
654
|
+
expect(cmds.indexOf("stash")).toBeLessThan(cmds.indexOf("reset"))
|
|
655
|
+
expect(cmds.indexOf("stash")).toBeLessThan(cmds.indexOf("fetch"))
|
|
656
|
+
const stash = git.calls.find(item => item.args[0] === "stash")
|
|
657
|
+
// 未跟踪的文件更可能是使用者自己放进去的,一并收走
|
|
658
|
+
expect(stash?.args.slice(0, 3)).toEqual(["stash", "push", "--include-untracked"])
|
|
659
|
+
expect(h.logger.lines.some(line => line.includes("git stash pop"))).toBe(true)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it("工作区干净时不 stash", async () => {
|
|
663
|
+
const git = stubGit({ status: "" })
|
|
664
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
665
|
+
await makeGitRepo(h.pluginsDir)
|
|
666
|
+
|
|
667
|
+
await h.market.update("demo")
|
|
668
|
+
|
|
669
|
+
expect(git.cmds()).not.toContain("stash")
|
|
670
|
+
expect(h.logger.lines.some(line => line.includes("已暂存"))).toBe(false)
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it("拉取失败时抛错,插件目录原样留下", async () => {
|
|
674
|
+
const git = stubGit({ fail: { cmd: "fetch", message: "无法连接到远端" } })
|
|
675
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
676
|
+
const dir = await makeGitRepo(h.pluginsDir)
|
|
677
|
+
|
|
678
|
+
await expect(h.market.update("demo")).rejects.toThrow("无法连接到远端")
|
|
679
|
+
// 退回重装会把一次可修复的网络失败变成一次目录删除
|
|
680
|
+
expect(await isFile(join(dir, "index.js"))).toBe(true)
|
|
681
|
+
expect(await isDirectory(join(dir, "node_modules"))).toBe(true)
|
|
682
|
+
expect(git.cmds()).not.toContain("reset")
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
it("minCore 不满足时拒绝,且不退回重装", async () => {
|
|
686
|
+
const git = stubGit()
|
|
687
|
+
const h = await makeHarness(
|
|
688
|
+
{ "https://example.com/index.json": { json: { plugins: [{ ...GIT_INDEX.plugins[0], minCore: "9.0.0" }] } } },
|
|
689
|
+
{},
|
|
690
|
+
undefined,
|
|
691
|
+
git.run
|
|
692
|
+
)
|
|
693
|
+
const dir = await makeGitRepo(h.pluginsDir)
|
|
694
|
+
|
|
695
|
+
await expect(h.market.update("demo")).rejects.toThrow("要求内核版本不低于 9.0.0")
|
|
696
|
+
expect(await isFile(join(dir, "index.js"))).toBe(true)
|
|
697
|
+
expect(git.cmds()).not.toContain("fetch")
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it("目录不是 git 仓库时退回重装,走 clone", async () => {
|
|
701
|
+
const git = stubGit({
|
|
702
|
+
effect: async args => {
|
|
703
|
+
if (args[0] !== "clone") return
|
|
704
|
+
const dest = args[args.length - 1]!
|
|
705
|
+
await mkdir(dest, { recursive: true })
|
|
706
|
+
await writeFile(join(dest, "package.json"), JSON.stringify({ version: "2.0.0" }), "utf8")
|
|
707
|
+
await writeFile(join(dest, "index.js"), 'export default "v2"', "utf8")
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: GIT_INDEX } }, {}, undefined, git.run)
|
|
711
|
+
// 早先由归档装来的目录:没有 .git,无从拉取
|
|
712
|
+
const dir = join(h.pluginsDir, "demo")
|
|
713
|
+
await mkdir(dir, { recursive: true })
|
|
714
|
+
await writeFile(join(dir, "index.js"), 'export default "v1"', "utf8")
|
|
715
|
+
|
|
716
|
+
const result = await h.market.update("demo")
|
|
717
|
+
|
|
718
|
+
expect(result).toMatchObject({ via: "git", version: "2.0.0" })
|
|
719
|
+
expect(result.fromVersion).toBeUndefined()
|
|
720
|
+
expect(result.changed).toBeUndefined()
|
|
721
|
+
expect(git.cmds()).toContain("clone")
|
|
722
|
+
expect(git.cmds()).not.toContain("fetch")
|
|
723
|
+
expect(await readFile(join(dir, "index.js"), "utf8")).toBe('export default "v2"')
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it("本机没有 git 时退回重装,走归档", async () => {
|
|
727
|
+
const git = stubGit({ fail: { cmd: "--version", message: "找不到 git" } })
|
|
728
|
+
const h = await makeHarness(
|
|
729
|
+
{
|
|
730
|
+
"https://example.com/index.json": { json: GIT_INDEX },
|
|
731
|
+
[CODELOAD]: { bytes: pluginArchive({ version: "2.0.0" }, "v2") }
|
|
732
|
+
},
|
|
733
|
+
{},
|
|
734
|
+
undefined,
|
|
735
|
+
git.run
|
|
736
|
+
)
|
|
737
|
+
await makeGitRepo(h.pluginsDir)
|
|
738
|
+
|
|
739
|
+
const result = await h.market.update("demo")
|
|
740
|
+
|
|
741
|
+
expect(result).toMatchObject({ via: "tarball", version: "2.0.0" })
|
|
742
|
+
expect(h.calls).toContain(CODELOAD)
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
it("索引声明的来源不是 git 时退回重装", async () => {
|
|
746
|
+
const git = stubGit()
|
|
747
|
+
const h = await makeHarness(
|
|
748
|
+
{
|
|
749
|
+
"https://example.com/index.json": { json: INDEX },
|
|
750
|
+
[TARBALL]: { bytes: pluginArchive({ version: "2.0.0" }, "v2") }
|
|
751
|
+
},
|
|
752
|
+
{},
|
|
753
|
+
undefined,
|
|
754
|
+
git.run
|
|
755
|
+
)
|
|
756
|
+
// 目录里有 .git(使用者自己 clone 的),但索引说这插件由归档分发
|
|
757
|
+
await makeGitRepo(h.pluginsDir)
|
|
758
|
+
|
|
759
|
+
const result = await h.market.update("demo")
|
|
760
|
+
|
|
761
|
+
expect(result).toMatchObject({ via: "tarball", version: "2.0.0" })
|
|
762
|
+
expect(git.cmds()).not.toContain("fetch")
|
|
763
|
+
})
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
describe("PluginMarket 卸载", () => {
|
|
767
|
+
it("删除插件目录,未安装时返回 false", async () => {
|
|
768
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: INDEX } })
|
|
769
|
+
await mkdir(join(h.pluginsDir, "demo", "lib"), { recursive: true })
|
|
770
|
+
await writeFile(join(h.pluginsDir, "demo", "index.js"), "1", "utf8")
|
|
771
|
+
|
|
772
|
+
expect(await h.market.remove("demo")).toBe(true)
|
|
773
|
+
expect(await isDirectory(join(h.pluginsDir, "demo"))).toBe(false)
|
|
774
|
+
expect(await h.market.remove("demo")).toBe(false)
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
it("越界名称在删除前即被拒绝", async () => {
|
|
778
|
+
const h = await makeHarness({ "https://example.com/index.json": { json: INDEX } })
|
|
779
|
+
await expect(h.market.remove("../..")).rejects.toThrow("不合法")
|
|
780
|
+
})
|
|
781
|
+
})
|