diff --git a/src/hooks/auto-update-checker/hook.test.ts b/src/hooks/auto-update-checker/hook.test.ts new file mode 100644 index 000000000..b7d8e5232 --- /dev/null +++ b/src/hooks/auto-update-checker/hook.test.ts @@ -0,0 +1,100 @@ +import type { PluginInput } from "@opencode-ai/plugin" +import { describe, expect, mock, test } from "bun:test" + +let latestVersionCallCount = 0 +let scheduleDeferredIdleCheckCallCount = 0 +const flushMicrotasks = async (count: number): Promise => { + for (let index = 0; index < count; index += 1) { + await Promise.resolve() + } +} + +const latestVersionMock = async () => { + latestVersionCallCount += 1 + return "3.0.1" +} + +const scheduleDeferredIdleCheckMock = (runCheck: () => void) => { + scheduleDeferredIdleCheckCallCount += 1 + scheduledCheck = runCheck +} + +let scheduledCheck: (() => void) | null = null + +mock.module("./checker/latest-version", () => ({ + getLatestVersion: latestVersionMock, +})) + +mock.module("./hook/deferred-idle-check", () => ({ + scheduleDeferredIdleCheck: scheduleDeferredIdleCheckMock, +})) + +const createHook = async () => { + const module = await import("./hook") + return module.createAutoUpdateCheckerHook( + { + directory: "/tmp/project", + client: { + tui: { + showToast: async () => undefined, + }, + }, + } satisfies PluginInput, + { + showStartupToast: false, + autoUpdate: false, + }, + { + getCachedVersion: () => "3.0.0", + getLocalDevVersion: () => null, + showConfigErrorsIfAny: async () => undefined, + updateAndShowConnectedProvidersCacheStatus: async () => undefined, + refreshModelCapabilitiesOnStartup: async () => undefined, + showModelCacheWarningIfNeeded: async () => undefined, + showLocalDevToast: async () => undefined, + showVersionToast: async () => undefined, + runBackgroundUpdateCheck: async () => { + await latestVersionMock() + }, + log: () => undefined, + }, + ) +} + +describe("auto-update-checker hook", () => { + test("defers update check until first session idle", async () => { + // given + latestVersionCallCount = 0 + scheduleDeferredIdleCheckCallCount = 0 + scheduledCheck = null + const hook = await createHook() + + // when + hook.event({ event: { type: "session.created" } }) + + // then + expect(scheduleDeferredIdleCheckCallCount).toBe(0) + expect(latestVersionCallCount).toBe(0) + + // when + hook.event({ event: { type: "session.idle" } }) + + // then + expect(scheduleDeferredIdleCheckCallCount).toBe(1) + expect(latestVersionCallCount).toBe(0) + + // when + await scheduledCheck?.() + await flushMicrotasks(8) + + // then + expect(latestVersionCallCount).toBe(1) + + // when + hook.event({ event: { type: "session.idle" } }) + + // then + expect(scheduleDeferredIdleCheckCallCount).toBe(1) + expect(latestVersionCallCount).toBe(1) + }) +}) diff --git a/src/hooks/auto-update-checker/hook.ts b/src/hooks/auto-update-checker/hook.ts index fbe3998da..73f5eed4b 100644 --- a/src/hooks/auto-update-checker/hook.ts +++ b/src/hooks/auto-update-checker/hook.ts @@ -3,6 +3,7 @@ import { log } from "../../shared/logger" import type { AutoUpdateCheckerOptions } from "./types" import { getCachedVersion, getLocalDevVersion } from "./checker" import { runBackgroundUpdateCheck } from "./hook/background-update-check" +import { scheduleDeferredIdleCheck } from "./hook/deferred-idle-check" import { showConfigErrorsIfAny } from "./hook/config-errors-toast" import { updateAndShowConnectedProvidersCacheStatus } from "./hook/connected-providers-status" import { refreshModelCapabilitiesOnStartup } from "./hook/model-capabilities-status" @@ -60,44 +61,45 @@ export function createAutoUpdateCheckerHook( } let hasChecked = false + let hasScheduled = false return { event: ({ event }: { event: { type: string; properties?: unknown } }) => { - if (event.type !== "session.created") return + if (event.type !== "session.idle") return if (isCliRunMode) return - if (hasChecked) return + if (hasChecked || hasScheduled) return - const props = event.properties as { info?: { parentID?: string } } | undefined - if (props?.info?.parentID) return + hasScheduled = true + scheduleDeferredIdleCheck(() => { hasChecked = true + void (async () => { + const cachedVersion = deps.getCachedVersion() + const localDevVersion = deps.getLocalDevVersion(ctx.directory) + const displayVersion = localDevVersion ?? cachedVersion - setTimeout(async () => { - const cachedVersion = deps.getCachedVersion() - const localDevVersion = deps.getLocalDevVersion(ctx.directory) - const displayVersion = localDevVersion ?? cachedVersion + await deps.showConfigErrorsIfAny(ctx) + await deps.updateAndShowConnectedProvidersCacheStatus(ctx) + await deps.refreshModelCapabilitiesOnStartup(modelCapabilities) + await deps.showModelCacheWarningIfNeeded(ctx) - await deps.showConfigErrorsIfAny(ctx) - await deps.updateAndShowConnectedProvidersCacheStatus(ctx) - await deps.refreshModelCapabilitiesOnStartup(modelCapabilities) - await deps.showModelCacheWarningIfNeeded(ctx) - - if (localDevVersion) { - if (showStartupToast) { - deps.showLocalDevToast(ctx, displayVersion, isSisyphusEnabled).catch(() => {}) + if (localDevVersion) { + if (showStartupToast) { + deps.showLocalDevToast(ctx, displayVersion, isSisyphusEnabled).catch(() => {}) + } + deps.log("[auto-update-checker] Local development mode") + return } - deps.log("[auto-update-checker] Local development mode") - return - } - if (showStartupToast) { - deps.showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {}) - } + if (showStartupToast) { + deps.showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {}) + } - deps.runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage).catch((err) => { - deps.log("[auto-update-checker] Background update check failed:", err) - }) - }, 0) + deps.runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage).catch((err) => { + deps.log("[auto-update-checker] Background update check failed:", err) + }) + })() + }) }, } } diff --git a/src/hooks/auto-update-checker/hook/deferred-idle-check.ts b/src/hooks/auto-update-checker/hook/deferred-idle-check.ts new file mode 100644 index 000000000..a929cf4ee --- /dev/null +++ b/src/hooks/auto-update-checker/hook/deferred-idle-check.ts @@ -0,0 +1,4 @@ +export function scheduleDeferredIdleCheck(runCheck: () => void): void { + const timeout = setTimeout(runCheck, 5000) + timeout.unref?.() +}