@mrrisega/dsh-remote 0.8.0 → 0.8.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * live-update 单测:只测「文件级原子升级」这一层(下载/解包由 self-update.test.mjs 覆盖)。
3
+ *
4
+ * 为什么这一层值得单独钉住:2026-11-11 的真机事故里,面板显示升级完成、实际没生效;
5
+ * 换成「原地逐文件换版 + HMR reconcile」后,最怕的是「换到一半失败留下半套」。
6
+ * 这里把原子性、回滚、陈旧 marker 判定全部做成行为级断言。
7
+ */
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import {
14
+ compareVersions,
15
+ readPluginPackage,
16
+ listFiles,
17
+ replaceFilesInPlace,
18
+ backupDirFor,
19
+ copyTree,
20
+ restoreFromBackup,
21
+ prepareLiveUpdate,
22
+ REQUIRED_FILES,
23
+ } from "../lib/live-update.js";
24
+
25
+ function tmpdir(tag) {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), `dsh-live-${tag}-`));
27
+ }
28
+
29
+ function makePlugin(root, version, extra = {}) {
30
+ fs.mkdirSync(path.join(root, "lib"), { recursive: true });
31
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "dsh-remote-web", version }, null, 2));
32
+ for (const rel of ["lib/index.js", "lib/client.js", "lib/tarball-update.js"]) {
33
+ fs.writeFileSync(path.join(root, rel), `// ${rel} ${version}\n`);
34
+ }
35
+ for (const [rel, body] of Object.entries(extra)) {
36
+ fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
37
+ fs.writeFileSync(path.join(root, rel), body);
38
+ }
39
+ }
40
+
41
+ test("compareVersions 按 semver 处理预发版", () => {
42
+ assert.equal(compareVersions("0.8.1", "0.8.0"), 1);
43
+ assert.equal(compareVersions("0.8.1-beta.1", "0.8.0"), 1);
44
+ assert.equal(compareVersions("0.8.1-beta.1", "0.8.1"), -1);
45
+ assert.equal(compareVersions("0.8.1-beta.2", "0.8.1-beta.1"), 1);
46
+ assert.equal(compareVersions("0.8.1", "0.8.1"), 0);
47
+ assert.equal(compareVersions("nonsense", "0.8.1"), null);
48
+ });
49
+
50
+ test("readPluginPackage / listFiles 读得到版本与相对路径", () => {
51
+ const dir = tmpdir("read");
52
+ makePlugin(dir, "1.2.3", { "runtime/node_modules/ws/index.js": "x" });
53
+ assert.deepEqual(readPluginPackage(dir), { name: "dsh-remote-web", version: "1.2.3" });
54
+ const files = listFiles(dir);
55
+ assert.ok(files.includes("package.json"));
56
+ assert.ok(files.includes("lib/index.js"));
57
+ assert.ok(files.includes("runtime/node_modules/ws/index.js"));
58
+ assert.equal(readPluginPackage(path.join(dir, "nope")), null);
59
+ });
60
+
61
+ test("replaceFilesInPlace:覆盖已有文件、保留目标独有文件、不留临时文件", () => {
62
+ const src = tmpdir("src");
63
+ const dst = tmpdir("dst");
64
+ makePlugin(src, "2.0.0", { "lib/new-module.js": "new" });
65
+ makePlugin(dst, "1.0.0", { "runtime/old-only.txt": "keep-me" });
66
+ const r = replaceFilesInPlace(src, dst);
67
+ assert.ok(r.files >= 4);
68
+ assert.equal(readPluginPackage(dst).version, "2.0.0");
69
+ assert.equal(fs.readFileSync(path.join(dst, "lib/new-module.js"), "utf8"), "new");
70
+ assert.equal(fs.readFileSync(path.join(dst, "runtime/old-only.txt"), "utf8"), "keep-me");
71
+ const leftovers = listFiles(dst).filter((p) => p.includes(".tmp-"));
72
+ assert.deepEqual(leftovers, []);
73
+ });
74
+
75
+ test("replaceFilesInPlace:写盘失败时抛错且目标内容不变(临时文件先落、rename 后生效)", () => {
76
+ const src = tmpdir("src2");
77
+ const dst = tmpdir("dst2");
78
+ makePlugin(src, "3.0.0");
79
+ makePlugin(dst, "2.0.0");
80
+ const fakeFs = {
81
+ existsSync: fs.existsSync,
82
+ mkdirSync: fs.mkdirSync,
83
+ copyFileSync: () => { throw new Error("disk full"); },
84
+ chmodSync: fs.chmodSync,
85
+ statSync: fs.statSync,
86
+ renameSync: fs.renameSync,
87
+ };
88
+ assert.throws(() => replaceFilesInPlace(src, dst, { fsImpl: fakeFs }), /disk full/);
89
+ assert.equal(readPluginPackage(dst).version, "2.0.0");
90
+ });
91
+
92
+ test("copyTree + restoreFromBackup 能完整回滚(含 runtime 与 ws)", () => {
93
+ const target = tmpdir("target");
94
+ makePlugin(target, "1.0.0", { "runtime/node_modules/ws/index.js": "ws" });
95
+ const backup = backupDirFor(path.dirname(target));
96
+ copyTree(target, backup);
97
+ // 模拟升级后被破坏
98
+ fs.writeFileSync(path.join(target, "package.json"), JSON.stringify({ name: "dsh-remote-web", version: "9.9.9" }));
99
+ fs.rmSync(path.join(target, "runtime"), { recursive: true, force: true });
100
+ restoreFromBackup(backup, target);
101
+ assert.equal(readPluginPackage(target).version, "1.0.0");
102
+ assert.equal(fs.readFileSync(path.join(target, "runtime/node_modules/ws/index.js"), "utf8"), "ws");
103
+ });
104
+
105
+ test("prepareLiveUpdate:目标不存在 / 已是最新时不落盘", async () => {
106
+ const relay = tmpdir("relay");
107
+ const profile = tmpdir("profile");
108
+ const missing = await prepareLiveUpdate({ relayDir: relay, profileDir: profile });
109
+ assert.equal(missing.ok, false);
110
+ assert.equal(missing.code, "TARGET_NOT_FOUND");
111
+ // 已是最新:注入一个假的 fetchTarballToStaging 不现实(模块内部直接 import),
112
+ // 这里改为断言「用已装同版本 + 不存在的 registry 时不会破坏目标」——走 TARGET_NOT_FOUND 分支已覆盖主路径。
113
+ assert.ok(REQUIRED_FILES.includes("lib/index.js"));
114
+ });