fix: harden windows native updates
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/types.openclaw.js";
|
||||
@@ -390,14 +391,13 @@ describe("update-cli", () => {
|
||||
},
|
||||
{
|
||||
name: "defaults to stable channel for package installs when unset",
|
||||
mode: "npm" as const,
|
||||
options: { yes: true },
|
||||
prepare: async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
},
|
||||
expectedChannel: "stable" as const,
|
||||
expectedTag: "latest",
|
||||
expectedChannel: undefined as "stable" | undefined,
|
||||
expectedTag: undefined as string | undefined,
|
||||
},
|
||||
{
|
||||
name: "uses stored beta channel when configured",
|
||||
@@ -414,14 +414,25 @@ describe("update-cli", () => {
|
||||
},
|
||||
])("$name", async ({ mode, options, prepare, expectedChannel, expectedTag }) => {
|
||||
await prepare();
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult({ mode }));
|
||||
if (mode) {
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult({ mode }));
|
||||
}
|
||||
|
||||
await updateCommand(options);
|
||||
|
||||
const call = expectUpdateCallChannel(expectedChannel);
|
||||
if (expectedTag !== undefined) {
|
||||
expect(call?.tag).toBe(expectedTag);
|
||||
if (expectedChannel !== undefined) {
|
||||
const call = expectUpdateCallChannel(expectedChannel);
|
||||
if (expectedTag !== undefined) {
|
||||
expect(call?.tag).toBe(expectedTag);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to latest when beta tag is older than release", async () => {
|
||||
@@ -436,32 +447,78 @@ describe("update-cli", () => {
|
||||
tag: "latest",
|
||||
version: "1.2.3-1",
|
||||
});
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(
|
||||
makeOkUpdateResult({
|
||||
mode: "npm",
|
||||
}),
|
||||
);
|
||||
|
||||
await updateCommand({});
|
||||
|
||||
const call = expectUpdateCallChannel("beta");
|
||||
expect(call?.tag).toBe("latest");
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("honors --tag override", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
|
||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir);
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(
|
||||
makeOkUpdateResult({
|
||||
mode: "npm",
|
||||
}),
|
||||
);
|
||||
mockPackageInstallStatus(tempDir);
|
||||
|
||||
await updateCommand({ tag: "next" });
|
||||
|
||||
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||
expect(call?.tag).toBe("next");
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
["npm", "i", "-g", "openclaw@next", "--no-fund", "--no-audit", "--loglevel=error"],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("prepends portable Git PATH for package updates on Windows", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
const localAppData = createCaseDir("openclaw-localappdata");
|
||||
const portableGitMingw = path.join(
|
||||
localAppData,
|
||||
"OpenClaw",
|
||||
"deps",
|
||||
"portable-git",
|
||||
"mingw64",
|
||||
"bin",
|
||||
);
|
||||
const portableGitUsr = path.join(
|
||||
localAppData,
|
||||
"OpenClaw",
|
||||
"deps",
|
||||
"portable-git",
|
||||
"usr",
|
||||
"bin",
|
||||
);
|
||||
await fs.mkdir(portableGitMingw, { recursive: true });
|
||||
await fs.mkdir(portableGitUsr, { recursive: true });
|
||||
mockPackageInstallStatus(tempDir);
|
||||
pathExists.mockImplementation(
|
||||
async (candidate: string) => candidate === portableGitMingw || candidate === portableGitUsr,
|
||||
);
|
||||
|
||||
await withEnvAsync({ LOCALAPPDATA: localAppData }, async () => {
|
||||
await updateCommand({ yes: true });
|
||||
});
|
||||
|
||||
platformSpy.mockRestore();
|
||||
|
||||
const updateCall = vi
|
||||
.mocked(runCommandWithTimeout)
|
||||
.mock.calls.find(
|
||||
(call) =>
|
||||
Array.isArray(call[0]) &&
|
||||
call[0][0] === "npm" &&
|
||||
call[0][1] === "i" &&
|
||||
call[0][2] === "-g",
|
||||
);
|
||||
const mergedPath = updateCall?.[1]?.env?.Path ?? updateCall?.[1]?.env?.PATH ?? "";
|
||||
expect(mergedPath.split(path.delimiter).slice(0, 2)).toEqual([
|
||||
portableGitMingw,
|
||||
portableGitUsr,
|
||||
]);
|
||||
expect(updateCall?.[1]?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||
expect(updateCall?.[1]?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||
});
|
||||
|
||||
it("updateCommand outputs JSON when --json is set", async () => {
|
||||
@@ -648,15 +705,15 @@ describe("update-cli", () => {
|
||||
name: "requires confirmation without --yes",
|
||||
options: {},
|
||||
shouldExit: true,
|
||||
shouldRunUpdate: false,
|
||||
shouldRunPackageUpdate: false,
|
||||
},
|
||||
{
|
||||
name: "allows downgrade with --yes",
|
||||
options: { yes: true },
|
||||
shouldExit: false,
|
||||
shouldRunUpdate: true,
|
||||
shouldRunPackageUpdate: true,
|
||||
},
|
||||
])("$name in non-interactive mode", async ({ options, shouldExit, shouldRunUpdate }) => {
|
||||
])("$name in non-interactive mode", async ({ options, shouldExit, shouldRunPackageUpdate }) => {
|
||||
await setupNonInteractiveDowngrade();
|
||||
await updateCommand(options);
|
||||
|
||||
@@ -667,7 +724,12 @@ describe("update-cli", () => {
|
||||
expect(vi.mocked(defaultRuntime.exit).mock.calls.some((call) => call[0] === 1)).toBe(
|
||||
shouldExit,
|
||||
);
|
||||
expect(vi.mocked(runGatewayUpdate).mock.calls.length > 0).toBe(shouldRunUpdate);
|
||||
expect(vi.mocked(runGatewayUpdate).mock.calls.length > 0).toBe(false);
|
||||
expect(
|
||||
vi
|
||||
.mocked(runCommandWithTimeout)
|
||||
.mock.calls.some((call) => Array.isArray(call[0]) && call[0][0] === "npm"),
|
||||
).toBe(shouldRunPackageUpdate);
|
||||
});
|
||||
|
||||
it("dry-run bypasses downgrade confirmation checks in non-interactive mode", async () => {
|
||||
|
||||
Reference in New Issue
Block a user