Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/code/src/main/services/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface SettingsSchema {
autoSuspendEnabled: boolean;
maxActiveWorktrees: number;
autoSuspendAfterDays: number;
autoDownloadUpdates: boolean;
}

function getDefaultWorktreeLocation(): string {
Expand Down Expand Up @@ -84,6 +85,10 @@ const schema = {
minimum: 1,
maximum: 365,
},
autoDownloadUpdates: {
type: "boolean" as const,
default: true,
},
};

export const settingsStore = new Store<SettingsSchema>({
Expand All @@ -96,6 +101,7 @@ export const settingsStore = new Store<SettingsSchema>({
autoSuspendEnabled: true,
maxActiveWorktrees: 5,
autoSuspendAfterDays: 7,
autoDownloadUpdates: true,
},
});

Expand Down Expand Up @@ -166,3 +172,11 @@ export function getAutoSuspendAfterDays(): number {
export function setAutoSuspendAfterDays(value: number): void {
settingsStore.set("autoSuspendAfterDays", value);
}

export function getAutoDownloadUpdatesEnabled(): boolean {
return settingsStore.get("autoDownloadUpdates", true);
}

export function setAutoDownloadUpdatesEnabled(value: boolean): void {
settingsStore.set("autoDownloadUpdates", value);
}
14 changes: 14 additions & 0 deletions apps/code/src/main/services/updates/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,24 @@ export const installUpdateOutput = z.object({
installed: z.boolean(),
});

export const autoDownloadUpdatesOutput = z.object({
enabled: z.boolean(),
});

export const setAutoDownloadUpdatesInput = z.object({
enabled: z.boolean(),
});

export type IsEnabledOutput = z.infer<typeof isEnabledOutput>;

export type CheckForUpdatesOutput = z.infer<typeof checkForUpdatesOutput>;
export type InstallUpdateOutput = z.infer<typeof installUpdateOutput>;
export type AutoDownloadUpdatesOutput = z.infer<
typeof autoDownloadUpdatesOutput
>;
export type SetAutoDownloadUpdatesInput = z.infer<
typeof setAutoDownloadUpdatesInput
>;

export const UpdatesEvent = {
Ready: "ready",
Expand Down
62 changes: 62 additions & 0 deletions apps/code/src/main/services/updates/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ vi.mock("../../utils/env.js", () => ({
isDevBuild: () => !mockAppMeta.isProduction,
}));

const mockAutoDownloadUpdatesEnabled = vi.hoisted(() => vi.fn(() => true));
const mockSetAutoDownloadUpdatesEnabled = vi.hoisted(() => vi.fn());

vi.mock("../settingsStore.js", () => ({
getAutoDownloadUpdatesEnabled: mockAutoDownloadUpdatesEnabled,
setAutoDownloadUpdatesEnabled: mockSetAutoDownloadUpdatesEnabled,
}));

// Import the service after mocks are set up
import { UpdatesService } from "./service";

Expand Down Expand Up @@ -135,6 +143,7 @@ describe("UpdatesService", () => {
mockAppMeta.version = "1.0.0";
mockUpdater.isSupported.mockReturnValue(true);
mockAppLifecycle.whenReady.mockResolvedValue(undefined);
mockAutoDownloadUpdatesEnabled.mockReturnValue(true);

// Set default platform to darwin (macOS)
Object.defineProperty(process, "platform", {
Expand Down Expand Up @@ -257,6 +266,16 @@ describe("UpdatesService", () => {
expect(mockAppLifecycle.whenReady).not.toHaveBeenCalled();
});

it("skips startup and periodic checks when auto-download is disabled", async () => {
mockAutoDownloadUpdatesEnabled.mockReturnValue(false);

await initializeService(service);

expect(mockUpdater.check).not.toHaveBeenCalled();
vi.advanceTimersByTime(60 * 60 * 1000);
expect(mockUpdater.check).not.toHaveBeenCalled();
});

it("prevents multiple initializations", async () => {
await initializeService(service);

Expand Down Expand Up @@ -404,6 +423,49 @@ describe("UpdatesService", () => {
});
});

describe("settings", () => {
it.each([true, false])(
"reads auto-download toggle from settings store (%s)",
(value) => {
mockAutoDownloadUpdatesEnabled.mockReturnValue(value);
expect(service.autoDownloadUpdatesEnabled).toBe(value);
},
);

it.each([true, false])(
"persists auto-download toggle changes (%s)",
(value) => {
service.setAutoDownloadUpdatesEnabled(value);
expect(mockSetAutoDownloadUpdatesEnabled).toHaveBeenCalledWith(value);
},
);

it("stops periodic checks when auto-download is disabled at runtime", async () => {
await initializeService(service);
updaterHandlers.noUpdate?.();
mockUpdater.check.mockClear();

service.setAutoDownloadUpdatesEnabled(false);
await vi.advanceTimersByTimeAsync(60 * 60 * 1000);

expect(mockUpdater.check).not.toHaveBeenCalled();
});

it("starts periodic checks when auto-download is enabled at runtime", async () => {
mockAutoDownloadUpdatesEnabled.mockReturnValue(false);
await initializeService(service);
mockUpdater.check.mockClear();

service.setAutoDownloadUpdatesEnabled(true);
expect(mockUpdater.check).toHaveBeenCalledTimes(1);

updaterHandlers.noUpdate?.();
await vi.advanceTimersByTimeAsync(60 * 60 * 1000);

expect(mockUpdater.check).toHaveBeenCalledTimes(2);
});
});

describe("installUpdate", () => {
it("returns false when no update is ready", async () => {
const result = await service.installUpdate();
Expand Down
64 changes: 54 additions & 10 deletions apps/code/src/main/services/updates/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { isDevBuild } from "../../utils/env";
import { logger } from "../../utils/logger";
import { TypedEventEmitter } from "../../utils/typed-event-emitter";
import type { AppLifecycleService } from "../app-lifecycle/service";
import {
getAutoDownloadUpdatesEnabled,
setAutoDownloadUpdatesEnabled,
} from "../settingsStore";
import {
type CheckForUpdatesOutput,
type InstallUpdateOutput,
Expand Down Expand Up @@ -91,6 +95,28 @@ export class UpdatesService extends TypedEventEmitter<UpdatesEvents> {
this.appLifecycle.whenReady().then(() => this.setupAutoUpdater());
}

get autoDownloadUpdatesEnabled(): boolean {
return getAutoDownloadUpdatesEnabled();
}

setAutoDownloadUpdatesEnabled(enabled: boolean): void {
const previous = this.autoDownloadUpdatesEnabled;
setAutoDownloadUpdatesEnabled(enabled);

if (this.initialized) {
if (enabled) {
this.startAutoDownloadChecks();
} else {
this.stopAutoDownloadChecks();
}
}

log.info("Auto-download updates setting changed", {
previous,
current: enabled,
});
}

triggerMenuCheck(): void {
this.emit(UpdatesEvent.CheckFromMenu, true);
}
Expand Down Expand Up @@ -191,13 +217,13 @@ export class UpdatesService extends TypedEventEmitter<UpdatesEvents> {
),
);

// Perform initial check (periodic source — not user-initiated)
this.checkForUpdates("periodic");
if (this.autoDownloadUpdatesEnabled) {
this.startAutoDownloadChecks();
return;
}

// Set up periodic checks
this.checkIntervalId = setInterval(
() => this.checkForUpdates("periodic"),
UpdatesService.CHECK_INTERVAL_MS,
log.info(
"Auto-download updates disabled; skipping startup and periodic checks",
);
}

Expand Down Expand Up @@ -329,13 +355,31 @@ export class UpdatesService extends TypedEventEmitter<UpdatesEvents> {
}
}

private startAutoDownloadChecks(): void {
if (this.checkIntervalId) {
return;
}

this.checkForUpdates("periodic");
this.checkIntervalId = setInterval(
() => this.checkForUpdates("periodic"),
UpdatesService.CHECK_INTERVAL_MS,
);
}

private stopAutoDownloadChecks(): void {
if (!this.checkIntervalId) {
return;
}

clearInterval(this.checkIntervalId);
this.checkIntervalId = null;
}

@preDestroy()
shutdown(): void {
this.clearCheckTimeout();
if (this.checkIntervalId) {
clearInterval(this.checkIntervalId);
this.checkIntervalId = null;
}
this.stopAutoDownloadChecks();
for (const unsub of this.unsubscribes) unsub();
this.unsubscribes = [];
}
Expand Down
15 changes: 15 additions & 0 deletions apps/code/src/main/trpc/routers/updates.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { container } from "../../di/container";
import { MAIN_TOKENS } from "../../di/tokens";
import {
autoDownloadUpdatesOutput,
checkForUpdatesOutput,
installUpdateOutput,
isEnabledOutput,
setAutoDownloadUpdatesInput,
UpdatesEvent,
type UpdatesEvents,
} from "../../services/updates/schemas";
Expand Down Expand Up @@ -39,6 +41,19 @@ export const updatesRouter = router({
return service.installUpdate();
}),

getAutoDownload: publicProcedure
.output(autoDownloadUpdatesOutput)
.query(() => ({ enabled: getService().autoDownloadUpdatesEnabled })),

setAutoDownload: publicProcedure
.input(setAutoDownloadUpdatesInput)
.output(autoDownloadUpdatesOutput)
.mutation(({ input }) => {
const service = getService();
service.setAutoDownloadUpdatesEnabled(input.enabled);
return { enabled: service.autoDownloadUpdatesEnabled };
}),

onReady: subscribe(UpdatesEvent.Ready),
onStatus: subscribe(UpdatesEvent.Status),
onCheckFromMenu: subscribe(UpdatesEvent.CheckFromMenu),
Expand Down
Loading