




















@@ -0,0 +1,167 @@
1+import fs from "node:fs/promises";
2+import path from "node:path";
3+import { describe, expect, it, vi } from "vitest";
4+import { withTempDir } from "../test-helpers/temp-dir.js";
5+import { writePackageDistInventory } from "./package-dist-inventory.js";
6+import {
7+runGlobalPackageUpdateSteps,
8+type PackageUpdateStepResult,
9+} from "./package-update-steps.js";
10+import type { CommandRunner, ResolvedGlobalInstallTarget } from "./update-global.js";
11+12+async function writePackageRoot(packageRoot: string, version: string): Promise<void> {
13+await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true });
14+await fs.writeFile(
15+path.join(packageRoot, "package.json"),
16+JSON.stringify({ name: "openclaw", version }),
17+"utf8",
18+);
19+await fs.writeFile(path.join(packageRoot, "dist", "index.js"), "export {};\n", "utf8");
20+await writePackageDistInventory(packageRoot);
21+}
22+23+function createNpmTarget(globalRoot: string): ResolvedGlobalInstallTarget {
24+return {
25+manager: "npm",
26+command: "npm",
27+ globalRoot,
28+packageRoot: path.join(globalRoot, "openclaw"),
29+};
30+}
31+32+function createRootRunner(globalRoot: string): CommandRunner {
33+return async (argv) => {
34+if (argv.join(" ") === "npm root -g") {
35+return { stdout: `${globalRoot}\n`, stderr: "", code: 0 };
36+}
37+throw new Error(`unexpected command: ${argv.join(" ")}`);
38+};
39+}
40+41+describe("runGlobalPackageUpdateSteps", () => {
42+it("installs npm updates into a clean staged prefix before swapping the global package", async () => {
43+await withTempDir({ prefix: "openclaw-package-update-staged-" }, async (base) => {
44+const prefix = path.join(base, "prefix");
45+const globalRoot = path.join(prefix, "lib", "node_modules");
46+const packageRoot = path.join(globalRoot, "openclaw");
47+await writePackageRoot(packageRoot, "1.0.0");
48+await fs.mkdir(path.join(packageRoot, "dist", "extensions", "qa-channel"), {
49+recursive: true,
50+});
51+await fs.writeFile(
52+path.join(packageRoot, "dist", "extensions", "qa-channel", "runtime-api.js"),
53+"export {};\n",
54+"utf8",
55+);
56+57+const runStep = vi.fn(
58+async ({ name, argv, cwd, timeoutMs }): Promise<PackageUpdateStepResult> => {
59+expect(timeoutMs).toBe(1000);
60+if (name !== "global update") {
61+throw new Error(`unexpected step ${name}`);
62+}
63+const prefixIndex = argv.indexOf("--prefix");
64+expect(prefixIndex).toBeGreaterThan(0);
65+const stagePrefix = argv[prefixIndex + 1];
66+if (!stagePrefix) {
67+throw new Error("missing staged prefix");
68+}
69+await writePackageRoot(
70+path.join(stagePrefix, "lib", "node_modules", "openclaw"),
71+"2.0.0",
72+);
73+await fs.mkdir(path.join(stagePrefix, "bin"), { recursive: true });
74+await fs.symlink(
75+"../lib/node_modules/openclaw/dist/index.js",
76+path.join(stagePrefix, "bin", "openclaw"),
77+);
78+return {
79+ name,
80+command: argv.join(" "),
81+cwd: cwd ?? process.cwd(),
82+durationMs: 1,
83+exitCode: 0,
84+};
85+},
86+);
87+88+const result = await runGlobalPackageUpdateSteps({
89+installTarget: createNpmTarget(globalRoot),
90+installSpec: "openclaw@2.0.0",
91+packageName: "openclaw",
92+ packageRoot,
93+runCommand: createRootRunner(globalRoot),
94+ runStep,
95+timeoutMs: 1000,
96+});
97+98+expect(result.failedStep).toBeNull();
99+expect(result.verifiedPackageRoot).toBe(packageRoot);
100+expect(result.afterVersion).toBe("2.0.0");
101+expect(result.steps.map((step) => step.name)).toEqual([
102+"global update",
103+"global install swap",
104+]);
105+await expect(fs.readFile(path.join(packageRoot, "package.json"), "utf8")).resolves.toContain(
106+'"version":"2.0.0"',
107+);
108+await expect(
109+fs.access(path.join(packageRoot, "dist", "extensions", "qa-channel", "runtime-api.js")),
110+).rejects.toMatchObject({ code: "ENOENT" });
111+await expect(fs.readlink(path.join(prefix, "bin", "openclaw"))).resolves.toBe(
112+"../lib/node_modules/openclaw/dist/index.js",
113+);
114+});
115+});
116+117+it("does not run post-verify work when staged npm verification fails", async () => {
118+await withTempDir({ prefix: "openclaw-package-update-verify-" }, async (base) => {
119+const prefix = path.join(base, "prefix");
120+const globalRoot = path.join(prefix, "lib", "node_modules");
121+const packageRoot = path.join(globalRoot, "openclaw");
122+await writePackageRoot(packageRoot, "1.0.0");
123+const postVerifyStep = vi.fn();
124+125+const result = await runGlobalPackageUpdateSteps({
126+installTarget: createNpmTarget(globalRoot),
127+installSpec: "openclaw@2.0.0",
128+packageName: "openclaw",
129+ packageRoot,
130+runCommand: createRootRunner(globalRoot),
131+runStep: async ({ name, argv, cwd }) => {
132+const prefixIndex = argv.indexOf("--prefix");
133+const stagePrefix = argv[prefixIndex + 1];
134+if (!stagePrefix) {
135+throw new Error("missing staged prefix");
136+}
137+await writePackageRoot(
138+path.join(stagePrefix, "lib", "node_modules", "openclaw"),
139+"1.5.0",
140+);
141+return {
142+ name,
143+command: argv.join(" "),
144+cwd: cwd ?? process.cwd(),
145+durationMs: 1,
146+exitCode: 0,
147+};
148+},
149+timeoutMs: 1000,
150+ postVerifyStep,
151+});
152+153+expect(result.failedStep?.name).toBe("global install verify");
154+expect(result.steps.map((step) => step.name)).toEqual([
155+"global update",
156+"global install verify",
157+]);
158+expect(result.steps.at(-1)?.stderrTail).toContain(
159+"expected installed version 2.0.0, found 1.5.0",
160+);
161+expect(postVerifyStep).not.toHaveBeenCalled();
162+await expect(fs.readFile(path.join(packageRoot, "package.json"), "utf8")).resolves.toContain(
163+'"version":"1.0.0"',
164+);
165+});
166+});
167+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。