feat(update): add core auto-updater and dry-run preview

This commit is contained in:
Peter Steinberger
2026-02-22 17:11:24 +01:00
parent 13690d406a
commit f442a3539f
15 changed files with 673 additions and 45 deletions

View File

@@ -35,6 +35,10 @@ vi.mock("../version.js", () => ({
VERSION: "1.0.0",
}));
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: vi.fn(),
}));
describe("update-startup", () => {
let suiteRoot = "";
let suiteCase = 0;
@@ -45,6 +49,7 @@ describe("update-startup", () => {
let checkUpdateStatus: (typeof import("./update-check.js"))["checkUpdateStatus"];
let resolveNpmChannelTag: (typeof import("./update-check.js"))["resolveNpmChannelTag"];
let runGatewayUpdateCheck: (typeof import("./update-startup.js"))["runGatewayUpdateCheck"];
let scheduleGatewayUpdateCheck: (typeof import("./update-startup.js"))["scheduleGatewayUpdateCheck"];
let getUpdateAvailable: (typeof import("./update-startup.js"))["getUpdateAvailable"];
let resetUpdateAvailableStateForTest: (typeof import("./update-startup.js"))["resetUpdateAvailableStateForTest"];
let loaded = false;
@@ -70,8 +75,12 @@ describe("update-startup", () => {
if (!loaded) {
({ resolveOpenClawPackageRoot } = await import("./openclaw-root.js"));
({ checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js"));
({ runGatewayUpdateCheck, getUpdateAvailable, resetUpdateAvailableStateForTest } =
await import("./update-startup.js"));
({
runGatewayUpdateCheck,
scheduleGatewayUpdateCheck,
getUpdateAvailable,
resetUpdateAvailableStateForTest,
} = await import("./update-startup.js"));
loaded = true;
}
vi.mocked(resolveOpenClawPackageRoot).mockClear();
@@ -238,4 +247,125 @@ describe("update-startup", () => {
expect(log.info).not.toHaveBeenCalled();
await expect(fs.stat(path.join(tempDir, "update-check.json"))).rejects.toThrow();
});
it("defers stable auto-update until rollout window is due", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "2.0.0",
});
const runAutoUpdate = vi.fn().mockResolvedValue({
ok: true,
code: 0,
});
await runGatewayUpdateCheck({
cfg: {
update: {
channel: "stable",
auto: {
enabled: true,
stableDelayHours: 6,
stableJitterHours: 12,
},
},
},
log: { info: vi.fn() },
isNixMode: false,
allowInTests: true,
runAutoUpdate,
});
expect(runAutoUpdate).not.toHaveBeenCalled();
vi.setSystemTime(new Date("2026-01-18T07:00:00Z"));
await runGatewayUpdateCheck({
cfg: {
update: {
channel: "stable",
auto: {
enabled: true,
stableDelayHours: 6,
stableJitterHours: 12,
},
},
},
log: { info: vi.fn() },
isNixMode: false,
allowInTests: true,
runAutoUpdate,
});
expect(runAutoUpdate).toHaveBeenCalledTimes(1);
expect(runAutoUpdate).toHaveBeenCalledWith({
channel: "stable",
timeoutMs: 45 * 60 * 1000,
});
});
it("runs beta auto-update checks hourly when enabled", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "beta",
version: "2.0.0-beta.1",
});
const runAutoUpdate = vi.fn().mockResolvedValue({
ok: true,
code: 0,
});
await runGatewayUpdateCheck({
cfg: {
update: {
channel: "beta",
auto: {
enabled: true,
betaCheckIntervalHours: 1,
},
},
},
log: { info: vi.fn() },
isNixMode: false,
allowInTests: true,
runAutoUpdate,
});
expect(runAutoUpdate).toHaveBeenCalledTimes(1);
expect(runAutoUpdate).toHaveBeenCalledWith({
channel: "beta",
timeoutMs: 45 * 60 * 1000,
});
});
it("scheduleGatewayUpdateCheck returns a cleanup function", async () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({
root: "/opt/openclaw",
installKind: "package",
packageManager: "npm",
} satisfies UpdateCheckResult);
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
tag: "latest",
version: "2.0.0",
});
const stop = scheduleGatewayUpdateCheck({
cfg: { update: { channel: "stable" } },
log: { info: vi.fn() },
isNixMode: false,
});
expect(typeof stop).toBe("function");
stop();
});
});