1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 createInitialDesktopUpdateState,
5 reduceDesktopUpdateStateOnCheckFailure,
6 reduceDesktopUpdateStateOnCheckStart,
7 reduceDesktopUpdateStateOnDownloadComplete,
8 reduceDesktopUpdateStateOnDownloadFailure,
9 reduceDesktopUpdateStateOnDownloadProgress,
10 reduceDesktopUpdateStateOnDownloadStart,
11 reduceDesktopUpdateStateOnInstallFailure,
12 reduceDesktopUpdateStateOnNoUpdate,
13 reduceDesktopUpdateStateOnUpdateAvailable,
14 resolveNotAppliedInstallVersion,
15} from "./updateMachine.ts";
16
17const runtimeInfo = {
18 hostArch: "x64",
19 appArch: "x64",
20 runningUnderArm64Translation: false,
21} as const;
22
23describe("updateMachine", () => {
24 it("clears transient errors when a check starts", () => {
25 const state = reduceDesktopUpdateStateOnCheckStart(
26 {
27 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
28 enabled: true,
29 status: "error",
30 message: "network",
31 errorContext: "check",
32 canRetry: true,
33 },
34 "2026-03-04T00:00:00.000Z",
35 );
36
37 expect(state.status).toBe("checking");
38 expect(state.message).toBeNull();
39 expect(state.errorContext).toBeNull();
40 expect(state.canRetry).toBe(false);
41 });
42
43 it("records a check failure without exposing an action", () => {
44 const state = reduceDesktopUpdateStateOnCheckFailure(
45 {
46 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
47 enabled: true,
48 status: "checking",
49 },
50 "network unavailable",
51 "2026-03-04T00:00:00.000Z",
52 );
53
54 expect(state.status).toBe("error");
55 expect(state.errorContext).toBe("check");
56 expect(state.canRetry).toBe(true);
57 });
58
59 it("preserves an already-downloaded update while checking the feed", () => {
60 const downloadedState = {
61 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
62 enabled: true,
63 status: "downloaded" as const,
64 availableVersion: "1.1.0",
65 downloadedVersion: "1.1.0",
66 releaseNotes: [{ version: "1.1.0", items: ["fix: queued update"], totalItems: 1 }],
67 omittedReleaseCount: 2,
68 downloadPercent: 100,
69 };
70 const checking = reduceDesktopUpdateStateOnCheckStart(
71 downloadedState,
72 "2026-03-04T00:00:00.000Z",
73 );
74 const failed = reduceDesktopUpdateStateOnCheckFailure(
75 checking,
76 "network unavailable",
77 "2026-03-04T00:00:01.000Z",
78 );
79
80 expect(checking.status).toBe("checking");
81 expect(checking.downloadedVersion).toBe("1.1.0");
82 expect(checking.releaseNotes).toEqual(downloadedState.releaseNotes);
83 expect(checking.omittedReleaseCount).toBe(2);
84 expect(failed.status).toBe("downloaded");
85 expect(failed.downloadedVersion).toBe("1.1.0");
86 expect(failed.releaseNotes).toEqual(downloadedState.releaseNotes);
87 expect(failed.omittedReleaseCount).toBe(2);
88 expect(failed.message).toBeNull();
89 });
90
91 it("keeps the installer when the feed still offers its version", () => {
92 const releaseNotes = [{ version: "1.1.0", items: ["fix: queued update"], totalItems: 1 }];
93 const state = reduceDesktopUpdateStateOnUpdateAvailable(
94 {
95 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
96 enabled: true,
97 status: "downloaded",
98 availableVersion: "1.1.0",
99 downloadedVersion: "1.1.0",
100 releaseNotes,
101 omittedReleaseCount: 2,
102 downloadPercent: 100,
103 },
104 "1.1.0",
105 "2026-03-04T00:00:00.000Z",
106 );
107
108 expect(state.status).toBe("downloaded");
109 expect(state.downloadedVersion).toBe("1.1.0");
110 expect(state.releaseNotes).toEqual(releaseNotes);
111 expect(state.omittedReleaseCount).toBe(2);
112 expect(state.downloadPercent).toBe(100);
113 });
114
115 it("preserves available version on download failure for retry", () => {
116 const state = reduceDesktopUpdateStateOnDownloadFailure(
117 {
118 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
119 enabled: true,
120 status: "downloading",
121 availableVersion: "1.1.0",
122 downloadPercent: 43,
123 },
124 "checksum mismatch",
125 );
126
127 expect(state.status).toBe("available");
128 expect(state.availableVersion).toBe("1.1.0");
129 expect(state.errorContext).toBe("download");
130 expect(state.canRetry).toBe(true);
131 });
132
133 it("transitions to downloaded and then preserves install retry state", () => {
134 const downloaded = reduceDesktopUpdateStateOnDownloadComplete(
135 {
136 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
137 enabled: true,
138 status: "downloading",
139 availableVersion: "1.1.0",
140 },
141 "1.1.0",
142 );
143 const failedInstall = reduceDesktopUpdateStateOnInstallFailure(
144 downloaded,
145 "backend shutdown timed out",
146 );
147
148 expect(downloaded.status).toBe("downloaded");
149 expect(downloaded.downloadedVersion).toBe("1.1.0");
150 expect(failedInstall.status).toBe("downloaded");
151 expect(failedInstall.errorContext).toBe("install");
152 expect(failedInstall.canRetry).toBe(true);
153 });
154
155 it("preserves a downloaded update when no update is available", () => {
156 const releaseNotes = [{ version: "1.1.0", items: ["fix: queued update"], totalItems: 1 }];
157 const state = reduceDesktopUpdateStateOnNoUpdate(
158 {
159 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
160 enabled: true,
161 status: "error",
162 availableVersion: "1.1.0",
163 downloadedVersion: "1.1.0",
164 releaseNotes,
165 omittedReleaseCount: 2,
166 message: "old failure",
167 errorContext: "download",
168 canRetry: true,
169 },
170 "2026-03-04T00:00:00.000Z",
171 );
172
173 expect(state.status).toBe("downloaded");
174 expect(state.availableVersion).toBe("1.1.0");
175 expect(state.downloadedVersion).toBe("1.1.0");
176 expect(state.releaseNotes).toBe(releaseNotes);
177 expect(state.omittedReleaseCount).toBe(2);
178 expect(state.downloadPercent).toBe(100);
179 expect(state.message).toBeNull();
180 expect(state.errorContext).toBeNull();
181 expect(state.canRetry).toBe(true);
182 });
183
184 it("clears stale available state when no update is available", () => {
185 const state = reduceDesktopUpdateStateOnNoUpdate(
186 {
187 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
188 enabled: true,
189 status: "error",
190 availableVersion: "1.1.0",
191 releaseNotes: [{ version: "1.1.0", items: ["fix: stale update"], totalItems: 1 }],
192 omittedReleaseCount: 2,
193 message: "old failure",
194 errorContext: "download",
195 canRetry: true,
196 },
197 "2026-03-04T00:00:00.000Z",
198 );
199
200 expect(state.status).toBe("up-to-date");
201 expect(state.availableVersion).toBeNull();
202 expect(state.downloadedVersion).toBeNull();
203 expect(state.releaseNotes).toEqual([]);
204 expect(state.omittedReleaseCount).toBe(0);
205 expect(state.message).toBeNull();
206 expect(state.errorContext).toBeNull();
207 });
208
209 it("tracks available, download start, and progress cleanly", () => {
210 const releaseNotes = [
211 {
212 version: "1.1.0",
213 items: ["feat: add update release notes"],
214 totalItems: 1,
215 },
216 ];
217 const available = reduceDesktopUpdateStateOnUpdateAvailable(
218 {
219 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "latest"),
220 enabled: true,
221 status: "checking",
222 },
223 "1.1.0",
224 "2026-03-04T00:00:00.000Z",
225 releaseNotes,
226 2,
227 );
228 const downloading = reduceDesktopUpdateStateOnDownloadStart(available);
229 const progress = reduceDesktopUpdateStateOnDownloadProgress(downloading, 55.5);
230
231 expect(available.status).toBe("available");
232 expect(available.channel).toBe("latest");
233 expect(available.releaseNotes).toBe(releaseNotes);
234 expect(available.omittedReleaseCount).toBe(2);
235 expect(downloading.releaseNotes).toBe(releaseNotes);
236 expect(downloading.status).toBe("downloading");
237 expect(downloading.downloadPercent).toBe(0);
238 expect(progress.downloadPercent).toBe(55.5);
239 expect(progress.errorContext).toBeNull();
240 });
241
242 it("clears release notes when checking again", () => {
243 const state = reduceDesktopUpdateStateOnCheckStart(
244 {
245 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo, "nightly"),
246 enabled: true,
247 status: "available",
248 availableVersion: "1.1.0-nightly.1",
249 releaseNotes: [{ version: "1.1.0-nightly.1", items: ["feat: old note"], totalItems: 1 }],
250 omittedReleaseCount: 2,
251 },
252 "2026-03-04T00:00:00.000Z",
253 );
254
255 expect(state.releaseNotes).toEqual([]);
256 expect(state.omittedReleaseCount).toBe(0);
257 });
258});
259
260describe("resolveNotAppliedInstallVersion", () => {
261 const record = { fromVersion: "1.2.3", targetVersion: "1.2.4" };
262
263 it("flags the target when the app came back on the version it installed from", () => {
264 expect(resolveNotAppliedInstallVersion(record, "1.2.3")).toBe("1.2.4");
265 });
266
267 it("stays quiet when the install applied or the app runs another version", () => {
268 expect(resolveNotAppliedInstallVersion(record, "1.2.4")).toBeNull();
269 expect(resolveNotAppliedInstallVersion(record, "1.2.5")).toBeNull();
270 });
271});