1import { describe, expect, it } from "vite-plus/test";
2import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";
3
4import {
5 canCheckForUpdate,
6 getArm64IntelBuildWarningDescription,
7 getDesktopUpdateActionError,
8 getDesktopUpdateButtonTooltip,
9 getDesktopUpdateInstallConfirmationMessage,
10 getDesktopUpdateReleaseHistoryUrl,
11 getDesktopUpdateReleaseUrl,
12 isDesktopUpdateButtonDisabled,
13 resolveDesktopUpdateButtonAction,
14 shouldShowArm64IntelBuildWarning,
15 shouldToastDesktopUpdateActionResult,
16} from "./desktopUpdate.logic";
17
18const baseState: DesktopUpdateState = {
19 enabled: true,
20 status: "idle",
21 channel: "latest",
22 currentVersion: "1.0.0",
23 hostArch: "x64",
24 appArch: "x64",
25 runningUnderArm64Translation: false,
26 availableVersion: null,
27 downloadedVersion: null,
28 releaseNotes: [],
29 omittedReleaseCount: 0,
30 downloadPercent: null,
31 checkedAt: null,
32 message: null,
33 errorContext: null,
34 canRetry: false,
35};
36
37describe("desktop update button state", () => {
38 it("shows a download action when an update is available", () => {
39 const state: DesktopUpdateState = {
40 ...baseState,
41 status: "available",
42 availableVersion: "1.1.0",
43 };
44 expect(resolveDesktopUpdateButtonAction(state)).toBe("download");
45 });
46
47 it("keeps retry action available after a download error", () => {
48 const state: DesktopUpdateState = {
49 ...baseState,
50 status: "error",
51 availableVersion: "1.1.0",
52 message: "network timeout",
53 errorContext: "download",
54 canRetry: true,
55 };
56 expect(resolveDesktopUpdateButtonAction(state)).toBe("download");
57 expect(getDesktopUpdateButtonTooltip(state)).toContain("Click to retry");
58 });
59
60 it("says when the same version did not apply before", () => {
61 const state: DesktopUpdateState = {
62 ...baseState,
63 status: "downloaded",
64 availableVersion: "1.1.0",
65 downloadedVersion: "1.1.0",
66 notAppliedVersion: "1.1.0",
67 };
68 expect(getDesktopUpdateButtonTooltip(state)).toBe(
69 "Update 1.1.0 did not apply. Click to retry.",
70 );
71 expect(
72 getDesktopUpdateButtonTooltip({
73 ...state,
74 availableVersion: "1.2.0",
75 downloadedVersion: "1.2.0",
76 }),
77 ).toContain("1.2.0 downloaded");
78 });
79
80 it("keeps install action available after an install error", () => {
81 const state: DesktopUpdateState = {
82 ...baseState,
83 status: "error",
84 downloadedVersion: "1.1.0",
85 availableVersion: "1.1.0",
86 message: "shutdown timeout",
87 errorContext: "install",
88 canRetry: true,
89 };
90 expect(resolveDesktopUpdateButtonAction(state)).toBe("install");
91 expect(getDesktopUpdateButtonTooltip(state)).toContain("Click to retry");
92 });
93
94 it("keeps install action available after a background updater error", () => {
95 const state: DesktopUpdateState = {
96 ...baseState,
97 status: "error",
98 downloadedVersion: "1.1.0",
99 availableVersion: "1.1.0",
100 message: "background updater error",
101 errorContext: null,
102 canRetry: true,
103 };
104 expect(resolveDesktopUpdateButtonAction(state)).toBe("install");
105 expect(getDesktopUpdateButtonTooltip(state)).toContain("Click to restart and install");
106 });
107
108 it("prefers a newly available release over a stale downloaded version", () => {
109 const state: DesktopUpdateState = {
110 ...baseState,
111 status: "available",
112 availableVersion: "1.2.0",
113 downloadedVersion: "1.1.0",
114 };
115 expect(resolveDesktopUpdateButtonAction(state)).toBe("download");
116 });
117
118 it("hides the install action while checking for a newer release", () => {
119 const state: DesktopUpdateState = {
120 ...baseState,
121 status: "checking",
122 availableVersion: "1.1.0",
123 downloadedVersion: "1.1.0",
124 downloadPercent: 100,
125 };
126 expect(resolveDesktopUpdateButtonAction(state)).toBe("none");
127 });
128
129 it("has no action for non-actionable check errors", () => {
130 const state: DesktopUpdateState = {
131 ...baseState,
132 status: "error",
133 message: "network unavailable",
134 errorContext: "check",
135 canRetry: true,
136 };
137 expect(resolveDesktopUpdateButtonAction(state)).toBe("none");
138 });
139
140 it("disables the button while downloading", () => {
141 const state: DesktopUpdateState = {
142 ...baseState,
143 status: "downloading",
144 availableVersion: "1.1.0",
145 downloadPercent: 42.5,
146 };
147 expect(isDesktopUpdateButtonDisabled(state)).toBe(true);
148 expect(getDesktopUpdateButtonTooltip(state)).toContain("42%");
149 });
150});
151
152describe("getDesktopUpdateActionError", () => {
153 it("returns user-visible message for accepted failed attempts", () => {
154 const result: DesktopUpdateActionResult = {
155 accepted: true,
156 completed: false,
157 state: {
158 ...baseState,
159 status: "available",
160 availableVersion: "1.1.0",
161 message: "checksum mismatch",
162 errorContext: "download",
163 canRetry: true,
164 },
165 };
166 expect(getDesktopUpdateActionError(result)).toBe("checksum mismatch");
167 });
168
169 it("ignores messages for non-accepted attempts", () => {
170 const result: DesktopUpdateActionResult = {
171 accepted: false,
172 completed: false,
173 state: {
174 ...baseState,
175 status: "error",
176 message: "background failure",
177 errorContext: "check",
178 canRetry: false,
179 },
180 };
181 expect(getDesktopUpdateActionError(result)).toBeNull();
182 });
183
184 it("ignores messages for successful attempts", () => {
185 const result: DesktopUpdateActionResult = {
186 accepted: true,
187 completed: true,
188 state: {
189 ...baseState,
190 status: "downloaded",
191 downloadedVersion: "1.1.0",
192 availableVersion: "1.1.0",
193 message: null,
194 errorContext: null,
195 canRetry: true,
196 },
197 };
198 expect(getDesktopUpdateActionError(result)).toBeNull();
199 });
200});
201
202describe("desktop update UI helpers", () => {
203 it("builds the stable release URL for a downloaded version", () => {
204 expect(getDesktopUpdateReleaseUrl("0.0.30")).toBe(
205 "https://github.com/pingdotgg/t3code/releases/tag/v0.0.30",
206 );
207 });
208
209 it("builds the nightly release URL without dropping its version suffix", () => {
210 expect(getDesktopUpdateReleaseUrl("0.0.30-nightly.20260728.931")).toBe(
211 "https://github.com/pingdotgg/t3code/releases/tag/v0.0.30-nightly.20260728.931",
212 );
213 });
214
215 it("omits the release URL when the updater does not report a version", () => {
216 expect(getDesktopUpdateReleaseUrl(null)).toBeNull();
217 expect(getDesktopUpdateReleaseUrl(" ")).toBeNull();
218 });
219
220 it("builds the release history URL", () => {
221 expect(getDesktopUpdateReleaseHistoryUrl()).toBe(
222 "https://github.com/pingdotgg/t3code/releases",
223 );
224 });
225
226 it("toasts only for actionable updater errors", () => {
227 expect(
228 shouldToastDesktopUpdateActionResult({
229 accepted: true,
230 completed: false,
231 state: { ...baseState, message: "checksum mismatch" },
232 }),
233 ).toBe(true);
234 expect(
235 shouldToastDesktopUpdateActionResult({
236 accepted: true,
237 completed: false,
238 state: { ...baseState, message: null },
239 }),
240 ).toBe(false);
241 expect(
242 shouldToastDesktopUpdateActionResult({
243 accepted: true,
244 completed: true,
245 state: { ...baseState, message: "checksum mismatch" },
246 }),
247 ).toBe(false);
248 });
249
250 it("shows an Apple Silicon warning for Intel builds under Rosetta", () => {
251 const state: DesktopUpdateState = {
252 ...baseState,
253 hostArch: "arm64",
254 appArch: "x64",
255 runningUnderArm64Translation: true,
256 };
257
258 expect(shouldShowArm64IntelBuildWarning(state)).toBe(true);
259 expect(getArm64IntelBuildWarningDescription(state)).toContain("Apple Silicon");
260 expect(getArm64IntelBuildWarningDescription(state)).toContain("Intel build");
261 });
262
263 it("changes the warning copy when a native build update is ready to download", () => {
264 const state: DesktopUpdateState = {
265 ...baseState,
266 hostArch: "arm64",
267 appArch: "x64",
268 runningUnderArm64Translation: true,
269 status: "available",
270 availableVersion: "1.1.0",
271 };
272
273 expect(getArm64IntelBuildWarningDescription(state)).toContain("Download the available update");
274 });
275
276 it("includes the downloaded version in the install confirmation copy", () => {
277 expect(
278 getDesktopUpdateInstallConfirmationMessage({
279 availableVersion: "1.1.0",
280 downloadedVersion: "1.1.1",
281 }),
282 ).toContain("Install update 1.1.1 and restart The Knife?");
283 });
284
285 it("falls back to generic install confirmation copy when no version is available", () => {
286 expect(
287 getDesktopUpdateInstallConfirmationMessage({
288 availableVersion: null,
289 downloadedVersion: null,
290 }),
291 ).toContain("Install update and restart The Knife?");
292 });
293
294 it("keeps the same install confirmation copy across desktop platforms", () => {
295 expect(
296 getDesktopUpdateInstallConfirmationMessage({
297 availableVersion: "1.1.0",
298 downloadedVersion: "1.1.0",
299 }),
300 ).toBe(
301 "Install update 1.1.0 and restart The Knife?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.",
302 );
303 });
304});
305
306describe("canCheckForUpdate", () => {
307 it("returns false for null state", () => {
308 expect(canCheckForUpdate(null)).toBe(false);
309 });
310
311 it("returns false when updates are disabled", () => {
312 expect(canCheckForUpdate({ ...baseState, enabled: false, status: "disabled" })).toBe(false);
313 });
314
315 it("returns false while checking", () => {
316 expect(canCheckForUpdate({ ...baseState, status: "checking" })).toBe(false);
317 });
318
319 it("returns false while downloading", () => {
320 expect(canCheckForUpdate({ ...baseState, status: "downloading", downloadPercent: 50 })).toBe(
321 false,
322 );
323 });
324
325 it("returns true once an update has been downloaded so newer releases can be found", () => {
326 expect(
327 canCheckForUpdate({
328 ...baseState,
329 status: "downloaded",
330 availableVersion: "1.1.0",
331 downloadedVersion: "1.1.0",
332 }),
333 ).toBe(true);
334 });
335
336 it("returns true when idle", () => {
337 expect(canCheckForUpdate({ ...baseState, status: "idle" })).toBe(true);
338 });
339
340 it("returns true when up-to-date", () => {
341 expect(canCheckForUpdate({ ...baseState, status: "up-to-date" })).toBe(true);
342 });
343
344 it("returns true when an update is available", () => {
345 expect(
346 canCheckForUpdate({ ...baseState, status: "available", availableVersion: "1.1.0" }),
347 ).toBe(true);
348 });
349
350 it("returns true on error so the user can retry", () => {
351 expect(
352 canCheckForUpdate({
353 ...baseState,
354 status: "error",
355 errorContext: "check",
356 message: "network",
357 }),
358 ).toBe(true);
359 });
360});
361
362describe("getDesktopUpdateButtonTooltip", () => {
363 it("returns 'Up to date' for non-actionable states", () => {
364 expect(getDesktopUpdateButtonTooltip({ ...baseState, status: "idle" })).toBe("Up to date");
365 expect(getDesktopUpdateButtonTooltip({ ...baseState, status: "up-to-date" })).toBe(
366 "Up to date",
367 );
368 });
369});