-
Notifications
You must be signed in to change notification settings - Fork 136
feat(update): add update settings and a background check #525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
My-Denia
wants to merge
2
commits into
getopenscreen:main
Choose a base branch
from
My-Denia:feat/update-settings
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { | ||
| planBackgroundUpdate, | ||
| runUnblockedDownloadAndInstall, | ||
| shouldQuitAndInstallAfterRestartPrompt, | ||
| shouldStartBackgroundUpdateTimer, | ||
| } from "./background-update"; | ||
|
|
||
| describe("background update policy", () => { | ||
| it("starts no timer on a Store / non-owning channel", () => { | ||
| expect(shouldStartBackgroundUpdateTimer({ isPackaged: true, ownsItsUpdates: false })).toBe( | ||
| false, | ||
| ); | ||
| expect(shouldStartBackgroundUpdateTimer({ isPackaged: false, ownsItsUpdates: true })).toBe( | ||
| false, | ||
| ); | ||
| expect(shouldStartBackgroundUpdateTimer({ isPackaged: true, ownsItsUpdates: true })).toBe(true); | ||
| }); | ||
|
|
||
| it("does not plan a current-version dialog on the background path", () => { | ||
| expect(planBackgroundUpdate({ outcome: { kind: "current" }, mode: "notify" })).toEqual({ | ||
| action: "none", | ||
| }); | ||
| expect( | ||
| planBackgroundUpdate({ outcome: { kind: "unsupported" }, mode: "download-and-install" }), | ||
| ).toEqual({ action: "none" }); | ||
| }); | ||
|
|
||
| it("plans notify / download / download-and-install from an available update", () => { | ||
| const outcome = { kind: "downloaded" as const, version: "1.10.0" }; | ||
| expect(planBackgroundUpdate({ outcome, mode: "notify" })).toEqual({ | ||
| action: "notify-available", | ||
| version: "1.10.0", | ||
| }); | ||
| expect(planBackgroundUpdate({ outcome, mode: "download" })).toEqual({ | ||
| action: "download", | ||
| version: "1.10.0", | ||
| }); | ||
| expect(planBackgroundUpdate({ outcome, mode: "download-and-install" })).toEqual({ | ||
| action: "download-and-install", | ||
| version: "1.10.0", | ||
| }); | ||
| }); | ||
|
|
||
| it("does not call quitAndInstall until Restart Now returns 0", async () => { | ||
| const install = vi.fn(); | ||
| const cancelled = await runUnblockedDownloadAndInstall({ | ||
| download: async () => ({ kind: "downloaded", version: "1.10.0" }), | ||
| blocked: () => null, | ||
| confirmRestart: async () => 1, | ||
| install, | ||
| }); | ||
| expect(cancelled).toEqual({ status: "cancelled" }); | ||
| expect(install).not.toHaveBeenCalled(); | ||
| expect(shouldQuitAndInstallAfterRestartPrompt(1)).toBe(false); | ||
|
|
||
| const installed = await runUnblockedDownloadAndInstall({ | ||
| download: async () => ({ kind: "downloaded", version: "1.10.0" }), | ||
| blocked: () => null, | ||
| confirmRestart: async () => 0, | ||
| install, | ||
| }); | ||
| expect(installed).toEqual({ status: "installed" }); | ||
| expect(install).toHaveBeenCalledTimes(1); | ||
| expect(shouldQuitAndInstallAfterRestartPrompt(0)).toBe(true); | ||
| }); | ||
|
|
||
| it("hands the download error back so the dialog can show its message", async () => { | ||
| const error = new Error("ECONNRESET mid-download"); | ||
| const failed = await runUnblockedDownloadAndInstall({ | ||
| download: async () => ({ kind: "failed", error }), | ||
| blocked: () => null, | ||
| confirmRestart: async () => 0, | ||
| install: vi.fn(), | ||
| }); | ||
| expect(failed).toEqual({ status: "failed", error }); | ||
| }); | ||
|
|
||
| it("never reaches the restart prompt without a downloaded update", async () => { | ||
| // downloadSelfUpdate cannot return these today, but the type admits | ||
| // them; a current/unsupported outcome must not prompt or install. | ||
| for (const kind of ["current", "unsupported"] as const) { | ||
| const confirmRestart = vi.fn(async () => 0); | ||
| const install = vi.fn(); | ||
| const result = await runUnblockedDownloadAndInstall({ | ||
| download: async () => ({ kind }), | ||
| blocked: () => null, | ||
| confirmRestart, | ||
| install, | ||
| }); | ||
| expect(result).toEqual({ status: "unavailable" }); | ||
| expect(confirmRestart).not.toHaveBeenCalled(); | ||
| expect(install).not.toHaveBeenCalled(); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import type { UpdateOutcome } from "./auto-updater"; | ||
|
|
||
| /** How far a background-discovered update may go on its own. No mode ever | ||
| * flips `autoInstallOnAppQuit`: install is always an explicit | ||
| * `quitAndInstall` behind a restart prompt, because `window-all-closed` | ||
| * quits this app and the HUD is a window — install-on-quit would fire a | ||
| * ~243 MB installer when the user merely closed the HUD. */ | ||
| export type UpdateMode = "notify" | "download" | "download-and-install"; | ||
|
|
||
| export const DEFAULT_UPDATE_MODE: UpdateMode = "notify"; | ||
| export const BACKGROUND_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000; | ||
|
|
||
| export function parseUpdateMode(raw: unknown): UpdateMode { | ||
| if (raw === "notify" || raw === "download" || raw === "download-and-install") return raw; | ||
| return DEFAULT_UPDATE_MODE; | ||
| } | ||
|
|
||
| export function shouldStartBackgroundUpdateTimer(input: { | ||
| isPackaged: boolean; | ||
| ownsItsUpdates: boolean; | ||
| }): boolean { | ||
| return input.isPackaged && input.ownsItsUpdates; | ||
| } | ||
|
|
||
| export type BackgroundUpdatePlan = | ||
| | { action: "none" } | ||
| | { action: "notify-available"; version: string } | ||
| | { action: "download"; version: string } | ||
| | { action: "download-and-install"; version: string }; | ||
|
|
||
| /** Background path: never plan a "you are current" dialog. */ | ||
| export function planBackgroundUpdate(input: { | ||
| outcome: UpdateOutcome; | ||
| mode: UpdateMode; | ||
| }): BackgroundUpdatePlan { | ||
| if (input.outcome.kind !== "downloaded") return { action: "none" }; | ||
| switch (input.mode) { | ||
| case "notify": | ||
| return { action: "notify-available", version: input.outcome.version }; | ||
| case "download": | ||
| return { action: "download", version: input.outcome.version }; | ||
| case "download-and-install": | ||
| return { action: "download-and-install", version: input.outcome.version }; | ||
| } | ||
| } | ||
|
|
||
| export function shouldQuitAndInstallAfterRestartPrompt(response: number): boolean { | ||
| return response === 0; | ||
| } | ||
|
|
||
| /** The failure carries the download error: the dialog that reports it shows | ||
| * `error.message` as detail, and collapsing the outcome to a bare string | ||
| * here is exactly how that detail once got lost between the helper and the | ||
| * caller. */ | ||
| export type DownloadAndInstallResult = | ||
| | { status: "failed"; error: Error } | ||
| | { status: "unavailable" } | ||
| | { status: "blocked" } | ||
| | { status: "cancelled" } | ||
| | { status: "installed" }; | ||
|
|
||
| export async function runUnblockedDownloadAndInstall(deps: { | ||
| download: () => Promise<UpdateOutcome>; | ||
| blocked: () => string | null; | ||
| confirmRestart: () => Promise<number>; | ||
| install: () => Promise<void>; | ||
| }): Promise<DownloadAndInstallResult> { | ||
| const downloaded = await deps.download(); | ||
| if (downloaded.kind === "failed") return { status: "failed", error: downloaded.error }; | ||
| // `downloadSelfUpdate` only ever reports downloaded|failed today, but the | ||
| // UpdateOutcome type admits current|unsupported — neither of which may | ||
| // reach the restart prompt, let alone quitAndInstall. | ||
| if (downloaded.kind !== "downloaded") return { status: "unavailable" }; | ||
| if (deps.blocked()) return { status: "blocked" }; | ||
| const choice = await deps.confirmRestart(); | ||
| if (!shouldQuitAndInstallAfterRestartPrompt(choice)) return { status: "cancelled" }; | ||
| await deps.install(); | ||
| return { status: "installed" }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.