pi-webdav-backup 1.0.1 → 1.3.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/README.md +205 -23
- package/extensions/webdav-backup/apply.mjs +319 -0
- package/extensions/webdav-backup/args.mjs +27 -0
- package/extensions/webdav-backup/backup.mjs +167 -87
- package/extensions/webdav-backup/cli.mjs +475 -352
- package/extensions/webdav-backup/collect.mjs +104 -2
- package/extensions/webdav-backup/config.mjs +53 -7
- package/extensions/webdav-backup/dav-mock.mjs +166 -145
- package/extensions/webdav-backup/extras.mjs +420 -0
- package/extensions/webdav-backup/json.mjs +33 -0
- package/extensions/webdav-backup/packages.mjs +375 -0
- package/extensions/webdav-backup/restore.mjs +17 -0
- package/extensions/webdav-backup/test-apply.mjs +318 -0
- package/extensions/webdav-backup/test-empty-cmd.mjs +167 -0
- package/extensions/webdav-backup/test-extras.mjs +314 -0
- package/extensions/webdav-backup/test-json.mjs +89 -0
- package/extensions/webdav-backup/test-plugins.mjs +363 -0
- package/extensions/webdav-backup/test-restore.mjs +286 -219
- package/extensions/webdav-backup/webdav-backup.example.json +4 -9
- package/extensions/webdav-backup/webdav.mjs +258 -218
- package/extensions/webdav-backup.ts +1024 -766
- package/package.json +2 -2
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* test-plugins.mjs — 恢复后插件补装测试
|
|
4
|
+
*
|
|
5
|
+
* 验证:
|
|
6
|
+
* 1. 解析 settings.json 里的 npm / git / local 声明(含 @scope、@ref)
|
|
7
|
+
* 2. 体检:缺安装(missing-install)、缺依赖(missing-deps)、正常(ok)
|
|
8
|
+
* 3. npm 插件补装走 `npm install <spec>`(带版本号)
|
|
9
|
+
* 4. git 插件目录缺失 → clone;目录在但缺依赖 → 只装依赖
|
|
10
|
+
* 5. 依赖声明为空的 git 包不应被误判为缺依赖
|
|
11
|
+
* 6. 真实恢复到临时目录后,analyzePlugins 能发现插件未装齐
|
|
12
|
+
* 7. 修复不因单个包失败而中止;dry-run 不落盘
|
|
13
|
+
*
|
|
14
|
+
* 安装命令全部通过注入的 run 桩函数模拟(不联网、确定性);
|
|
15
|
+
* 仅第 6 项跑真实的备份/恢复往返。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
import { startDavServer } from "./dav-mock.mjs";
|
|
24
|
+
import { loadConfig } from "./config.mjs";
|
|
25
|
+
import { runBackup } from "./backup.mjs";
|
|
26
|
+
import { restoreBackup } from "./restore.mjs";
|
|
27
|
+
import { parsePackageSource, readDeclaredPackages, analyzePlugins, repairPlugins } from "./packages.mjs";
|
|
28
|
+
|
|
29
|
+
let pass = 0;
|
|
30
|
+
let fail = 0;
|
|
31
|
+
const lines = [];
|
|
32
|
+
const check = (n, c, d = "") => {
|
|
33
|
+
if (c) {
|
|
34
|
+
pass++;
|
|
35
|
+
lines.push(` ✅ ${n}`);
|
|
36
|
+
} else {
|
|
37
|
+
fail++;
|
|
38
|
+
lines.push(` ❌ ${n}${d ? ` — ${d}` : ""}`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 记录调用的 run 桩 */
|
|
43
|
+
function makeRunStub({ failOn = [] } = {}) {
|
|
44
|
+
const calls = [];
|
|
45
|
+
const run = (command, args, cwd) => {
|
|
46
|
+
calls.push({ command, args, cwd });
|
|
47
|
+
const key = `${command} ${args.join(" ")}`;
|
|
48
|
+
const hit = failOn.find((f) => key.includes(f));
|
|
49
|
+
if (hit) return { ok: false, error: `模拟失败:${hit}` };
|
|
50
|
+
// 模拟安装产物,便于后续断言
|
|
51
|
+
if (command === "npm") {
|
|
52
|
+
const nodeModules = join(cwd, "node_modules");
|
|
53
|
+
mkdirSync(nodeModules, { recursive: true });
|
|
54
|
+
writeFileSync(join(nodeModules, ".stub"), "1");
|
|
55
|
+
// `npm install <name>[@ver]` 会落地 node_modules/<name>,体检才能转 ok;
|
|
56
|
+
// 无 spec 的 `npm install`(装依赖)只建 node_modules
|
|
57
|
+
const spec = args.slice(1).find((a) => !a.startsWith("-") && a !== "install");
|
|
58
|
+
if (spec) {
|
|
59
|
+
const at = spec.lastIndexOf("@");
|
|
60
|
+
const name = at > 0 ? spec.slice(0, at) : spec;
|
|
61
|
+
const pkgDir = join(nodeModules, ...name.split("/"));
|
|
62
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
63
|
+
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name }));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (command === "git" && args[0] === "clone") {
|
|
67
|
+
const dest = args[args.length - 1];
|
|
68
|
+
mkdirSync(dest, { recursive: true });
|
|
69
|
+
writeFileSync(join(dest, "package.json"), JSON.stringify({ name: "stub" }));
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
};
|
|
73
|
+
return { run, calls };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
console.log("pi WebDAV 备份 — 插件补装测试\n");
|
|
78
|
+
|
|
79
|
+
// ── 1. 来源解析
|
|
80
|
+
check("解析 npm 无版本", JSON.stringify(parsePackageSource("npm:pi-footer")) ===
|
|
81
|
+
JSON.stringify({ type: "npm", source: "npm:pi-footer", name: "pi-footer", version: null }));
|
|
82
|
+
const npmVer = parsePackageSource("npm:pi-footer@1.2.3");
|
|
83
|
+
check("解析 npm 带版本", npmVer.name === "pi-footer" && npmVer.version === "1.2.3", JSON.stringify(npmVer));
|
|
84
|
+
const scoped = parsePackageSource("npm:@scope/pi-tools@2.0.0");
|
|
85
|
+
check("解析 @scope 包", scoped.name === "@scope/pi-tools" && scoped.version === "2.0.0", JSON.stringify(scoped));
|
|
86
|
+
const scopedNoVer = parsePackageSource("npm:@scope/pi-tools");
|
|
87
|
+
check("解析 @scope 无版本", scopedNoVer.name === "@scope/pi-tools" && scopedNoVer.version === null, JSON.stringify(scopedNoVer));
|
|
88
|
+
const git = parsePackageSource("git:github.com/ddldxiacan/pi-webdav-backup@v1.0.2");
|
|
89
|
+
check(
|
|
90
|
+
"解析 git 带 ref",
|
|
91
|
+
git.type === "git" && git.host === "github.com" && git.path === "ddldxiacan/pi-webdav-backup" &&
|
|
92
|
+
git.ref === "v1.0.2" && git.url === "https://github.com/ddldxiacan/pi-webdav-backup",
|
|
93
|
+
JSON.stringify(git),
|
|
94
|
+
);
|
|
95
|
+
const gitPlain = parsePackageSource("git:github.com/example/pi-tools");
|
|
96
|
+
check("解析 git 无 ref", gitPlain.ref === null, JSON.stringify(gitPlain));
|
|
97
|
+
// 与 pi 对齐的解析细节
|
|
98
|
+
const gitDot = parsePackageSource("git:github.com/example/pi-tools.git@v1");
|
|
99
|
+
check(
|
|
100
|
+
"git 去掉 .git 后缀(安装路径与 pi 一致)",
|
|
101
|
+
gitDot.path === "example/pi-tools" && gitDot.ref === "v1" && gitDot.host === "github.com",
|
|
102
|
+
JSON.stringify(gitDot),
|
|
103
|
+
);
|
|
104
|
+
const gitSlashRef = parsePackageSource("git:github.com/example/pi-tools@feature/x");
|
|
105
|
+
check("git ref 可含斜杠", gitSlashRef.ref === "feature/x" && gitSlashRef.path === "example/pi-tools", JSON.stringify(gitSlashRef));
|
|
106
|
+
const gitUrl = parsePackageSource("https://github.com/example/pi-tools@v1");
|
|
107
|
+
check(
|
|
108
|
+
"https URL 也识别为 git",
|
|
109
|
+
gitUrl.type === "git" && gitUrl.host === "github.com" && gitUrl.path === "example/pi-tools" && gitUrl.ref === "v1",
|
|
110
|
+
JSON.stringify(gitUrl),
|
|
111
|
+
);
|
|
112
|
+
const gitScp = parsePackageSource("git:git@github.com:example/pi-tools.git");
|
|
113
|
+
check(
|
|
114
|
+
"scp 形式识别为 git",
|
|
115
|
+
gitScp.type === "git" && gitScp.host === "github.com" && gitScp.path === "example/pi-tools",
|
|
116
|
+
JSON.stringify(gitScp),
|
|
117
|
+
);
|
|
118
|
+
check("解析 local", parsePackageSource("./pi-tools").type === "local");
|
|
119
|
+
check("空声明返回 null", parsePackageSource("") === null && parsePackageSource(null) === null);
|
|
120
|
+
|
|
121
|
+
// ── 2. settings.json 读取:跳过 enabled:false
|
|
122
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-plugins-test-"));
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(dir, "settings.json"),
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
packages: [
|
|
127
|
+
"npm:pi-footer",
|
|
128
|
+
{ source: "npm:pi-disabled", enabled: false },
|
|
129
|
+
"git:github.com/example/pi-tools@v1",
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
const declared = readDeclaredPackages(dir);
|
|
134
|
+
check("读取声明跳过 enabled:false", declared.length === 2, JSON.stringify(declared.map((d) => d.source)));
|
|
135
|
+
// 坏 JSON 不应抛异常
|
|
136
|
+
writeFileSync(join(dir, "settings.json"), "{ not json");
|
|
137
|
+
check("settings.json 损坏时安全返回空", readDeclaredPackages(dir).length === 0);
|
|
138
|
+
|
|
139
|
+
// ── 3. 体检:三类状态
|
|
140
|
+
const d2 = mkdtempSync(join(tmpdir(), "pi-plugins-test2-"));
|
|
141
|
+
writeFileSync(
|
|
142
|
+
join(d2, "settings.json"),
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
packages: [
|
|
145
|
+
"npm:missing-npm",
|
|
146
|
+
"npm:present-npm",
|
|
147
|
+
"git:github.com/example/missing-git",
|
|
148
|
+
"git:github.com/example/nodeps-git",
|
|
149
|
+
"git:github.com/example/deps-git",
|
|
150
|
+
"./local-pkg",
|
|
151
|
+
],
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
// present-npm:已装
|
|
155
|
+
mkdirSync(join(d2, "npm", "node_modules", "present-npm"), { recursive: true });
|
|
156
|
+
// nodeps-git:目录在、无 dependencies → 正常
|
|
157
|
+
mkdirSync(join(d2, "git", "github.com", "example", "nodeps-git"), { recursive: true });
|
|
158
|
+
writeFileSync(join(d2, "git", "github.com", "example", "nodeps-git", "package.json"), JSON.stringify({ name: "n" }));
|
|
159
|
+
// deps-git:目录在、有 dependencies、缺 node_modules → missing-deps
|
|
160
|
+
const depsGit = join(d2, "git", "github.com", "example", "deps-git");
|
|
161
|
+
mkdirSync(depsGit, { recursive: true });
|
|
162
|
+
writeFileSync(join(depsGit, "package.json"), JSON.stringify({ name: "d", dependencies: { a: "^1", b: "^1" } }));
|
|
163
|
+
|
|
164
|
+
const a = analyzePlugins(d2);
|
|
165
|
+
const bySource = Object.fromEntries(a.packages.map((p) => [p.source, p.status]));
|
|
166
|
+
check("npm 缺失 → missing-install", bySource["npm:missing-npm"] === "missing-install", JSON.stringify(bySource));
|
|
167
|
+
check("npm 已装 → ok", bySource["npm:present-npm"] === "ok", JSON.stringify(bySource));
|
|
168
|
+
check("git 目录缺失 → missing-install", bySource["git:github.com/example/missing-git"] === "missing-install", JSON.stringify(bySource));
|
|
169
|
+
check("git 无依赖 → ok(不误报)", bySource["git:github.com/example/nodeps-git"] === "ok", JSON.stringify(bySource));
|
|
170
|
+
check("git 有依赖但缺 node_modules → missing-deps", bySource["git:github.com/example/deps-git"] === "missing-deps", JSON.stringify(bySource));
|
|
171
|
+
check("local → skip", bySource["./local-pkg"] === "skip", JSON.stringify(bySource));
|
|
172
|
+
check("体检 ok=false(有 issue)", a.ok === false);
|
|
173
|
+
check("issue 数为 3", a.issues.length === 3, JSON.stringify(a.issues.map((i) => i.kind)));
|
|
174
|
+
|
|
175
|
+
// ── 4. 补装 npm:应执行 npm install <spec>(含版本)
|
|
176
|
+
const d3 = mkdtempSync(join(tmpdir(), "pi-plugins-test3-"));
|
|
177
|
+
writeFileSync(join(d3, "settings.json"), JSON.stringify({ packages: ["npm:pi-footer@1.2.3"] }));
|
|
178
|
+
{
|
|
179
|
+
const { run, calls } = makeRunStub();
|
|
180
|
+
const r = repairPlugins(d3, { run });
|
|
181
|
+
check("npm 补装成功", r.ok === true && r.repaired.length === 1, JSON.stringify(r));
|
|
182
|
+
const install = calls.find((c) => c.command === "npm");
|
|
183
|
+
check("npm 命令带版本号", install && install.args.join(" ") === "install pi-footer@1.2.3 --legacy-peer-deps --no-fund --no-audit", JSON.stringify(install));
|
|
184
|
+
check("npm 在 agentDir/npm 下执行", install && install.cwd === join(d3, "npm"), install?.cwd);
|
|
185
|
+
check("复检已通过", analyzePlugins(d3).ok === true);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── 5. git:目录缺失 → clone;目录在缺依赖 → 只 npm install
|
|
189
|
+
const d4 = mkdtempSync(join(tmpdir(), "pi-plugins-test4-"));
|
|
190
|
+
writeFileSync(
|
|
191
|
+
join(d4, "settings.json"),
|
|
192
|
+
JSON.stringify({ packages: ["git:github.com/example/missing-git@v1", "git:github.com/example/deps-git"] }),
|
|
193
|
+
);
|
|
194
|
+
const depsGit4 = join(d4, "git", "github.com", "example", "deps-git");
|
|
195
|
+
mkdirSync(depsGit4, { recursive: true });
|
|
196
|
+
writeFileSync(join(depsGit4, "package.json"), JSON.stringify({ name: "d", dependencies: { a: "^1" } }));
|
|
197
|
+
{
|
|
198
|
+
const { run, calls } = makeRunStub();
|
|
199
|
+
const r = repairPlugins(d4, { run });
|
|
200
|
+
const clone = calls.find((c) => c.command === "git");
|
|
201
|
+
check("git 缺失 → 执行 clone", !!clone && clone.args[0] === "clone", JSON.stringify(calls));
|
|
202
|
+
check("clone 带 --depth 1", clone && clone.args.includes("--depth") && clone.args.includes("1"), JSON.stringify(clone));
|
|
203
|
+
check("clone 带 ref(--branch)", clone && clone.args.includes("--branch") && clone.args.includes("v1"), JSON.stringify(clone));
|
|
204
|
+
check("clone 用 https url", clone && clone.args.some((x) => String(x).startsWith("https://github.com/example/missing-git")), JSON.stringify(clone));
|
|
205
|
+
// deps-git:应在其目录内跑 npm install(只装运行时依赖,同 pi)
|
|
206
|
+
const npmInGit = calls.find((c) => c.command === "npm" && c.cwd === depsGit4);
|
|
207
|
+
check("git 缺依赖 → 在包目录内 npm install", !!npmInGit, JSON.stringify(calls.map((c) => ({ c: c.command, cwd: c.cwd }))));
|
|
208
|
+
check("git 依赖用 --omit=dev", npmInGit && npmInGit.args.includes("--omit=dev"), JSON.stringify(npmInGit));
|
|
209
|
+
check("两个 git 包都算已修复", r.repaired.length === 2, JSON.stringify(r));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── 5b. git ref 用 commit SHA 时 --branch 不认,应回退为完整 clone + checkout
|
|
213
|
+
{
|
|
214
|
+
const d8 = mkdtempSync(join(tmpdir(), "pi-plugins-test8-"));
|
|
215
|
+
writeFileSync(join(d8, "settings.json"), JSON.stringify({ packages: ["git:github.com/example/sha-git@0123abc"] }));
|
|
216
|
+
const { run, calls } = makeRunStub({ failOn: ["--branch"] });
|
|
217
|
+
const r = repairPlugins(d8, { run });
|
|
218
|
+
check("SHA ref 回退后仍修复成功", r.ok === true && r.repaired.length === 1, JSON.stringify(r));
|
|
219
|
+
check("回退为完整 clone", calls.some((c) => c.command === "git" && c.args[0] === "clone" && !c.args.includes("--depth")), JSON.stringify(calls));
|
|
220
|
+
check("回退后执行 checkout", calls.some((c) => c.command === "git" && c.args[0] === "checkout" && c.args[1] === "0123abc"), JSON.stringify(calls));
|
|
221
|
+
try {
|
|
222
|
+
rmSync(d8, { recursive: true, force: true });
|
|
223
|
+
} catch {
|
|
224
|
+
/* ignore */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 5c. node_modules 在但缺了个别依赖(拷了一半)→ 仍应报 missing-deps
|
|
229
|
+
{
|
|
230
|
+
const d9 = mkdtempSync(join(tmpdir(), "pi-plugins-test9-"));
|
|
231
|
+
const pkgDir = join(d9, "git", "github.com", "example", "half-git");
|
|
232
|
+
mkdirSync(join(pkgDir, "node_modules", "a"), { recursive: true });
|
|
233
|
+
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name: "h", dependencies: { a: "^1", b: "^1" } }));
|
|
234
|
+
writeFileSync(join(d9, "settings.json"), JSON.stringify({ packages: ["git:github.com/example/half-git"] }));
|
|
235
|
+
const a9 = analyzePlugins(d9);
|
|
236
|
+
check("node_modules 半残也能查出来", a9.issues.length === 1 && a9.issues[0].kind === "missing-deps", JSON.stringify(a9.issues));
|
|
237
|
+
check("缺依赖点名到具体包", String(a9.issues[0].detail).includes("b"), JSON.stringify(a9.issues[0]));
|
|
238
|
+
const { run, calls } = makeRunStub();
|
|
239
|
+
const r9 = repairPlugins(d9, { run });
|
|
240
|
+
check("半残状态只补依赖不重新 clone", r9.ok === true && !calls.some((c) => c.command === "git"), JSON.stringify(calls));
|
|
241
|
+
try {
|
|
242
|
+
rmSync(d9, { recursive: true, force: true });
|
|
243
|
+
} catch {
|
|
244
|
+
/* ignore */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── 6. 单个包失败不影响其余;dry-run 不落盘
|
|
249
|
+
const d5 = mkdtempSync(join(tmpdir(), "pi-plugins-test5-"));
|
|
250
|
+
writeFileSync(join(d5, "settings.json"), JSON.stringify({ packages: ["npm:bad-pkg", "npm:good-pkg"] }));
|
|
251
|
+
{
|
|
252
|
+
const { run, calls } = makeRunStub({ failOn: ["bad-pkg"] });
|
|
253
|
+
const r = repairPlugins(d5, { run });
|
|
254
|
+
check("一个失败返回 ok=false", r.ok === false);
|
|
255
|
+
check("失败被记录", r.failed.length === 1 && r.failed[0].source === "npm:bad-pkg", JSON.stringify(r.failed));
|
|
256
|
+
check("失败不影响后续包", r.repaired.some((x) => x.source === "npm:good-pkg"), JSON.stringify(r.repaired));
|
|
257
|
+
check("两个包都尝试了", calls.filter((c) => c.command === "npm").length === 2, JSON.stringify(calls));
|
|
258
|
+
}
|
|
259
|
+
{
|
|
260
|
+
const d6 = mkdtempSync(join(tmpdir(), "pi-plugins-test6-"));
|
|
261
|
+
writeFileSync(join(d6, "settings.json"), JSON.stringify({ packages: ["npm:pi-footer"] }));
|
|
262
|
+
const { run, calls } = makeRunStub();
|
|
263
|
+
const r = repairPlugins(d6, { run, dryRun: true });
|
|
264
|
+
check("dry-run 不执行命令", calls.length === 0, JSON.stringify(calls));
|
|
265
|
+
check("dry-run 列入 skipped", r.skipped.length === 1, JSON.stringify(r.skipped));
|
|
266
|
+
check("dry-run 后仍未安装", analyzePlugins(d6).ok === false);
|
|
267
|
+
}
|
|
268
|
+
// 无需修复时:不执行任何命令
|
|
269
|
+
{
|
|
270
|
+
const d7 = mkdtempSync(join(tmpdir(), "pi-plugins-test7-"));
|
|
271
|
+
writeFileSync(join(d7, "settings.json"), JSON.stringify({ packages: [] }));
|
|
272
|
+
const { run, calls } = makeRunStub();
|
|
273
|
+
const r = repairPlugins(d7, { run });
|
|
274
|
+
check("无声明时不执行命令", calls.length === 0 && r.ok === true);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── 7. 真实往返:备份 → 恢复 → 体检发现插件未装齐
|
|
278
|
+
// 备份有意排除 node_modules,所以恢复出来的插件必然"缺安装",
|
|
279
|
+
// 这正是需要 repairPlugins 兜底的场景。
|
|
280
|
+
{
|
|
281
|
+
const dav = await startDavServer();
|
|
282
|
+
dav.dirs.add("/dav");
|
|
283
|
+
const agentDir = mkdtempSync(join(tmpdir(), "pi-plugins-e2e-"));
|
|
284
|
+
mkdirSync(join(agentDir, "npm"), { recursive: true });
|
|
285
|
+
writeFileSync(
|
|
286
|
+
join(agentDir, "settings.json"),
|
|
287
|
+
JSON.stringify({ packages: ["npm:pi-footer@1.2.3"], theme: "dark" }),
|
|
288
|
+
);
|
|
289
|
+
writeFileSync(
|
|
290
|
+
join(agentDir, "npm", "package.json"),
|
|
291
|
+
JSON.stringify({ name: "pi-extensions", private: true, dependencies: { "pi-footer": "^1.2.3" } }),
|
|
292
|
+
);
|
|
293
|
+
// 假装已安装(备份会排除 node_modules)
|
|
294
|
+
mkdirSync(join(agentDir, "npm", "node_modules", "pi-footer"), { recursive: true });
|
|
295
|
+
writeFileSync(join(agentDir, "npm", "node_modules", "pi-footer", "index.js"), "// plugin\n");
|
|
296
|
+
writeFileSync(
|
|
297
|
+
join(agentDir, "webdav-backup.json"),
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
remote: {
|
|
300
|
+
url: `http://127.0.0.1:${dav.port}/dav`,
|
|
301
|
+
username: "user",
|
|
302
|
+
password: "pass",
|
|
303
|
+
remoteDir: "bk",
|
|
304
|
+
remoteName: "pi",
|
|
305
|
+
},
|
|
306
|
+
encrypt: false,
|
|
307
|
+
includeSessions: false,
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
312
|
+
const config = loadConfig().config;
|
|
313
|
+
const bk = await runBackup(config, { log: () => {}, agentDir, stateFile: join(agentDir, "state.json") });
|
|
314
|
+
check("插件场景备份成功", bk.ok === true, bk.error);
|
|
315
|
+
|
|
316
|
+
const dest = join(agentDir, "restored");
|
|
317
|
+
const r = await restoreBackup(config, { agentDir, to: dest, log: () => {} });
|
|
318
|
+
check("插件场景恢复成功", r.ok === true, JSON.stringify(r));
|
|
319
|
+
check("恢复出 settings.json", existsSync(join(dest, "settings.json")));
|
|
320
|
+
check("恢复出 npm 声明文件", existsSync(join(dest, "npm", "package.json")));
|
|
321
|
+
check("恢复不含 node_modules(有意排除)", !existsSync(join(dest, "npm", "node_modules")));
|
|
322
|
+
|
|
323
|
+
const analysis = analyzePlugins(dest);
|
|
324
|
+
check("体检发现插件缺安装", analysis.ok === false && analysis.issues.length === 1, JSON.stringify(analysis.issues));
|
|
325
|
+
check(
|
|
326
|
+
"缺安装指向 npm:pi-footer@1.2.3",
|
|
327
|
+
analysis.issues[0]?.source === "npm:pi-footer@1.2.3",
|
|
328
|
+
JSON.stringify(analysis.issues[0]),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// 补装(注入桩,避免联网)
|
|
332
|
+
const { run, calls } = makeRunStub();
|
|
333
|
+
const repaired = repairPlugins(dest, { run });
|
|
334
|
+
check("恢复后补装成功", repaired.ok === true && repaired.repaired.length === 1, JSON.stringify(repaired));
|
|
335
|
+
check("补装命令正确", calls[0]?.args.join(" ") === "install pi-footer@1.2.3 --legacy-peer-deps --no-fund --no-audit", JSON.stringify(calls[0]));
|
|
336
|
+
check("补装后体检通过", analyzePlugins(dest).ok === true);
|
|
337
|
+
|
|
338
|
+
dav.server.close();
|
|
339
|
+
try {
|
|
340
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
341
|
+
} catch {
|
|
342
|
+
/* ignore */
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const d of [dir, d2, d3, d4, d5]) {
|
|
347
|
+
try {
|
|
348
|
+
rmSync(d, { recursive: true, force: true });
|
|
349
|
+
} catch {
|
|
350
|
+
/* ignore */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log(lines.join("\n"));
|
|
355
|
+
console.log(`\n结果:${pass} 通过,${fail} 失败`);
|
|
356
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main().catch((e) => {
|
|
360
|
+
console.log(lines.join("\n"));
|
|
361
|
+
console.error(`\n测试异常:${e?.stack ?? e}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|