Let manual update checks bypass rollout
Automatic desktop update checks still respect rollout admission. Manual checks carry an explicit intent through the app and desktop updater service, and the up-to-date state now shows when the last check completed.
This commit is contained in:
@@ -38,13 +38,22 @@ function createUpdater(
|
||||
}
|
||||
|
||||
describe("desktop app updater — check", () => {
|
||||
it("forwards the requested release channel to the port", async () => {
|
||||
it("forwards manual check intent and the requested release channel to the port", async () => {
|
||||
const { updater, port } = createUpdater();
|
||||
port.nextCheckResult(buildFakeCheckResult());
|
||||
|
||||
await updater.checkForUpdates({ releaseChannel: "beta" });
|
||||
|
||||
expect(port.recordedChecks).toEqual([{ releaseChannel: "beta" }]);
|
||||
expect(port.recordedChecks).toEqual([{ releaseChannel: "beta", intent: "manual" }]);
|
||||
});
|
||||
|
||||
it("forwards automatic check intent independently from silent UI state", async () => {
|
||||
const { updater, port } = createUpdater();
|
||||
port.nextCheckResult(buildFakeCheckResult());
|
||||
|
||||
await updater.checkForUpdates({ releaseChannel: "stable", intent: "automatic", silent: true });
|
||||
|
||||
expect(port.recordedChecks).toEqual([{ releaseChannel: "stable", intent: "automatic" }]);
|
||||
});
|
||||
|
||||
it("moves to 'checking' during a non-silent check", async () => {
|
||||
@@ -66,7 +75,11 @@ describe("desktop app updater — check", () => {
|
||||
expect(updater.getSnapshot().status).toBe("available");
|
||||
|
||||
const deferred = port.deferNextCheck();
|
||||
const pending = updater.checkForUpdates({ releaseChannel: "stable", silent: true });
|
||||
const pending = updater.checkForUpdates({
|
||||
releaseChannel: "stable",
|
||||
intent: "automatic",
|
||||
silent: true,
|
||||
});
|
||||
expect(updater.getSnapshot().status).toBe("available");
|
||||
|
||||
deferred.resolve(buildFakeCheckResult({ hasUpdate: true, readyToInstall: true }));
|
||||
@@ -128,7 +141,7 @@ describe("desktop app updater — check", () => {
|
||||
const statusBeforeSilent = updater.getSnapshot().status;
|
||||
|
||||
port.failNextCheck(new Error("boom"));
|
||||
await updater.checkForUpdates({ releaseChannel: "stable", silent: true });
|
||||
await updater.checkForUpdates({ releaseChannel: "stable", intent: "automatic", silent: true });
|
||||
|
||||
expect(updater.getSnapshot().status).toBe(statusBeforeSilent);
|
||||
});
|
||||
@@ -226,6 +239,20 @@ describe("desktop app updater — subscribe", () => {
|
||||
describe("formatStatusText", () => {
|
||||
const formatVersion = (version: string | null | undefined) =>
|
||||
version ? `v${version.replace(/^v/i, "")}` : "\u2014";
|
||||
const formatLastCheckedAt = (timestamp: number) => `time-${timestamp}`;
|
||||
|
||||
it("shows when an up-to-date check completed", () => {
|
||||
expect(
|
||||
formatStatusText({
|
||||
status: "up-to-date",
|
||||
availableUpdate: null,
|
||||
installMessage: null,
|
||||
lastCheckedAt: 42,
|
||||
formatVersion,
|
||||
formatLastCheckedAt,
|
||||
}),
|
||||
).toBe("Up to date. Last checked at time-42.");
|
||||
});
|
||||
|
||||
it("uses the latest version in the 'available' message when present", () => {
|
||||
expect(
|
||||
@@ -233,7 +260,9 @@ describe("formatStatusText", () => {
|
||||
status: "available",
|
||||
availableUpdate: buildFakeCheckResult({ latestVersion: "1.2.3" }),
|
||||
installMessage: null,
|
||||
lastCheckedAt: null,
|
||||
formatVersion,
|
||||
formatLastCheckedAt,
|
||||
}),
|
||||
).toBe("Update ready: v1.2.3");
|
||||
});
|
||||
@@ -244,7 +273,9 @@ describe("formatStatusText", () => {
|
||||
status: "available",
|
||||
availableUpdate: null,
|
||||
installMessage: null,
|
||||
lastCheckedAt: null,
|
||||
formatVersion,
|
||||
formatLastCheckedAt,
|
||||
}),
|
||||
).toBe("An app update is ready to install.");
|
||||
});
|
||||
@@ -255,7 +286,9 @@ describe("formatStatusText", () => {
|
||||
status: "installed",
|
||||
availableUpdate: null,
|
||||
installMessage: "Restart now",
|
||||
lastCheckedAt: null,
|
||||
formatVersion,
|
||||
formatLastCheckedAt,
|
||||
}),
|
||||
).toBe("Restart now");
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
DesktopAppUpdateCheckResult,
|
||||
DesktopAppUpdateCheckIntent,
|
||||
DesktopAppUpdateInstallResult,
|
||||
DesktopReleaseChannel,
|
||||
} from "@/desktop/updates/desktop-updates";
|
||||
@@ -29,6 +30,7 @@ export interface DesktopAppUpdaterSnapshot {
|
||||
export interface DesktopAppUpdaterPort {
|
||||
checkDesktopAppUpdate(input: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
intent: DesktopAppUpdateCheckIntent;
|
||||
}): Promise<DesktopAppUpdateCheckResult>;
|
||||
installDesktopAppUpdate(input: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
@@ -52,6 +54,7 @@ export interface DesktopAppUpdater {
|
||||
subscribe(listener: () => void): () => void;
|
||||
checkForUpdates(options?: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
intent?: DesktopAppUpdateCheckIntent;
|
||||
silent?: boolean;
|
||||
}): Promise<DesktopAppUpdateCheckResult | null>;
|
||||
installUpdate(options: {
|
||||
@@ -102,9 +105,18 @@ export function formatStatusText(input: {
|
||||
status: DesktopAppUpdateStatus;
|
||||
availableUpdate: DesktopAppUpdateCheckResult | null;
|
||||
installMessage: string | null;
|
||||
lastCheckedAt: number | null;
|
||||
formatVersion: (version: string | null | undefined) => string;
|
||||
formatLastCheckedAt: (timestamp: number) => string;
|
||||
}): string {
|
||||
const { status, availableUpdate, installMessage, formatVersion } = input;
|
||||
const {
|
||||
status,
|
||||
availableUpdate,
|
||||
installMessage,
|
||||
lastCheckedAt,
|
||||
formatVersion,
|
||||
formatLastCheckedAt,
|
||||
} = input;
|
||||
|
||||
if (status === "checking") {
|
||||
return "Checking for app updates...";
|
||||
@@ -115,7 +127,10 @@ export function formatStatusText(input: {
|
||||
}
|
||||
|
||||
if (status === "up-to-date") {
|
||||
return "App is up to date.";
|
||||
if (lastCheckedAt != null) {
|
||||
return `Up to date. Last checked at ${formatLastCheckedAt(lastCheckedAt)}.`;
|
||||
}
|
||||
return "Up to date.";
|
||||
}
|
||||
|
||||
if (status === "pending") {
|
||||
@@ -155,12 +170,13 @@ export function createDesktopAppUpdater(deps: DesktopAppUpdaterDeps): DesktopApp
|
||||
|
||||
async function checkForUpdates(options?: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
intent?: DesktopAppUpdateCheckIntent;
|
||||
silent?: boolean;
|
||||
}): Promise<DesktopAppUpdateCheckResult | null> {
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
const { releaseChannel, silent = false } = options;
|
||||
const { releaseChannel, intent = "manual", silent = false } = options;
|
||||
const requestVersion = state.requestVersion + 1;
|
||||
|
||||
commit({
|
||||
@@ -171,7 +187,7 @@ export function createDesktopAppUpdater(deps: DesktopAppUpdaterDeps): DesktopApp
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await deps.port.checkDesktopAppUpdate({ releaseChannel });
|
||||
const result = await deps.port.checkDesktopAppUpdate({ releaseChannel, intent });
|
||||
if (requestVersion !== state.requestVersion) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface DesktopRuntimeInfo {
|
||||
}
|
||||
|
||||
export type DesktopReleaseChannel = "stable" | "beta";
|
||||
export type DesktopAppUpdateCheckIntent = "automatic" | "manual";
|
||||
|
||||
export interface LocalDaemonUpdateResult {
|
||||
exitCode: number;
|
||||
@@ -99,10 +100,15 @@ export async function getDesktopRuntimeInfo(): Promise<DesktopRuntimeInfo> {
|
||||
|
||||
export async function checkDesktopAppUpdate({
|
||||
releaseChannel,
|
||||
intent,
|
||||
}: {
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
intent: DesktopAppUpdateCheckIntent;
|
||||
}): Promise<DesktopAppUpdateCheckResult> {
|
||||
const result = await invokeDesktopCommand<unknown>("check_app_update", { releaseChannel });
|
||||
const result = await invokeDesktopCommand<unknown>("check_app_update", {
|
||||
releaseChannel,
|
||||
intent,
|
||||
});
|
||||
if (!isRecord(result)) {
|
||||
throw new Error("Unexpected response while checking desktop updates.");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type {
|
||||
DesktopAppUpdateCheckResult,
|
||||
DesktopAppUpdateCheckIntent,
|
||||
DesktopAppUpdateInstallResult,
|
||||
DesktopReleaseChannel,
|
||||
} from "@/desktop/updates/desktop-updates";
|
||||
import type { DesktopAppUpdaterPort } from "@/desktop/updates/desktop-app-updater";
|
||||
|
||||
export interface FakeDesktopAppUpdaterPort extends DesktopAppUpdaterPort {
|
||||
readonly recordedChecks: Array<{ releaseChannel: DesktopReleaseChannel }>;
|
||||
readonly recordedChecks: Array<{
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
intent: DesktopAppUpdateCheckIntent;
|
||||
}>;
|
||||
readonly recordedInstalls: Array<{ releaseChannel: DesktopReleaseChannel }>;
|
||||
nextCheckResult(result: DesktopAppUpdateCheckResult): void;
|
||||
deferNextCheck(): {
|
||||
@@ -53,7 +57,10 @@ function buildInstallResult(
|
||||
}
|
||||
|
||||
export function createFakeDesktopAppUpdaterPort(): FakeDesktopAppUpdaterPort {
|
||||
const recordedChecks: Array<{ releaseChannel: DesktopReleaseChannel }> = [];
|
||||
const recordedChecks: Array<{
|
||||
releaseChannel: DesktopReleaseChannel;
|
||||
intent: DesktopAppUpdateCheckIntent;
|
||||
}> = [];
|
||||
const recordedInstalls: Array<{ releaseChannel: DesktopReleaseChannel }> = [];
|
||||
const checkOutcomes: CheckOutcome[] = [];
|
||||
const installOutcomes: InstallOutcome[] = [];
|
||||
|
||||
@@ -62,10 +62,10 @@ export function UpdateCalloutSource() {
|
||||
useEffect(() => {
|
||||
if (!isDesktopApp) return;
|
||||
|
||||
void checkForUpdates({ silent: true });
|
||||
void checkForUpdates({ intent: "automatic", silent: true });
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
void checkForUpdates({ silent: true });
|
||||
void checkForUpdates({ intent: "automatic", silent: true });
|
||||
}, CHECK_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
installDesktopAppUpdate,
|
||||
shouldShowDesktopUpdateSection,
|
||||
type DesktopAppUpdateCheckResult,
|
||||
type DesktopAppUpdateCheckIntent,
|
||||
type DesktopAppUpdateInstallResult,
|
||||
} from "@/desktop/updates/desktop-updates";
|
||||
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
formatStatusText,
|
||||
type DesktopAppUpdateStatus,
|
||||
} from "@/desktop/updates/desktop-app-updater";
|
||||
import { formatMessageTimestamp } from "@/utils/time";
|
||||
|
||||
export type { DesktopAppUpdateStatus };
|
||||
|
||||
@@ -27,7 +29,10 @@ export interface UseDesktopAppUpdaterReturn {
|
||||
lastCheckedAt: number | null;
|
||||
isChecking: boolean;
|
||||
isInstalling: boolean;
|
||||
checkForUpdates: (options?: { silent?: boolean }) => Promise<DesktopAppUpdateCheckResult | null>;
|
||||
checkForUpdates: (options?: {
|
||||
intent?: DesktopAppUpdateCheckIntent;
|
||||
silent?: boolean;
|
||||
}) => Promise<DesktopAppUpdateCheckResult | null>;
|
||||
installUpdate: () => Promise<DesktopAppUpdateInstallResult | null>;
|
||||
}
|
||||
|
||||
@@ -57,11 +62,15 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
);
|
||||
|
||||
const checkForUpdates = useCallback(
|
||||
async (options: { silent?: boolean } = {}) => {
|
||||
async (options: { intent?: DesktopAppUpdateCheckIntent; silent?: boolean } = {}) => {
|
||||
if (!isDesktopApp) {
|
||||
return null;
|
||||
}
|
||||
return updater.checkForUpdates({ releaseChannel, silent: options.silent });
|
||||
return updater.checkForUpdates({
|
||||
releaseChannel,
|
||||
intent: options.intent ?? "manual",
|
||||
silent: options.silent,
|
||||
});
|
||||
},
|
||||
[isDesktopApp, releaseChannel, updater],
|
||||
);
|
||||
@@ -77,7 +86,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
if (!isDesktopApp) {
|
||||
return;
|
||||
}
|
||||
void checkForUpdates({ silent: true });
|
||||
void checkForUpdates({ intent: "automatic", silent: true });
|
||||
}, [checkForUpdates, isDesktopApp]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -86,7 +95,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
void checkForUpdates({ silent: true });
|
||||
void checkForUpdates({ intent: "automatic", silent: true });
|
||||
}, PENDING_RECHECK_MS);
|
||||
|
||||
return () => {
|
||||
@@ -101,7 +110,9 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
||||
status: snapshot.status,
|
||||
availableUpdate: snapshot.availableUpdate,
|
||||
installMessage: snapshot.installMessage,
|
||||
lastCheckedAt: snapshot.lastCheckedAt,
|
||||
formatVersion: formatVersionWithPrefix,
|
||||
formatLastCheckedAt: (timestamp) => formatMessageTimestamp(new Date(timestamp)),
|
||||
}),
|
||||
availableUpdate: snapshot.availableUpdate,
|
||||
errorMessage: snapshot.errorMessage,
|
||||
|
||||
@@ -523,7 +523,7 @@ function DesktopAppUpdateRow() {
|
||||
if (!isDesktopApp) {
|
||||
return undefined;
|
||||
}
|
||||
void checkForUpdates({ silent: true });
|
||||
void checkForUpdates({ intent: "automatic", silent: true });
|
||||
return undefined;
|
||||
}, [checkForUpdates, isDesktopApp]),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user