test(auto-update-checker): align test triggers with deferred idle check

This commit is contained in:
Sisyphus
2026-04-18 14:55:51 +09:00
parent 61675adbf1
commit 14868430bc
+160 -35
View File
@@ -1,8 +1,13 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { describe, expect, mock, test } from "bun:test" import { describe, expect, mock, test } from "bun:test"
type CreateAutoUpdateCheckerHook = typeof import("./hook").createAutoUpdateCheckerHook
type HookOptions = Parameters<CreateAutoUpdateCheckerHook>[1]
type HookDeps = NonNullable<Parameters<CreateAutoUpdateCheckerHook>[2]>
let latestVersionCallCount = 0 let latestVersionCallCount = 0
let scheduleDeferredIdleCheckCallCount = 0 let scheduleDeferredIdleCheckCallCount = 0
const flushMicrotasks = async (count: number): Promise<void> => { const flushMicrotasks = async (count: number): Promise<void> => {
for (let index = 0; index < count; index += 1) { for (let index = 0; index < count; index += 1) {
await Promise.resolve() await Promise.resolve()
@@ -29,72 +34,192 @@ mock.module("./hook/deferred-idle-check", () => ({
scheduleDeferredIdleCheck: scheduleDeferredIdleCheckMock, scheduleDeferredIdleCheck: scheduleDeferredIdleCheckMock,
})) }))
const createHook = async () => { const createPluginInput = (): PluginInput => ({
const module = await import("./hook") client: {} as PluginInput["client"],
return module.createAutoUpdateCheckerHook(
{
directory: "/tmp/project", directory: "/tmp/project",
client: { project: {} as PluginInput["project"],
tui: { worktree: "/tmp/project",
showToast: async () => undefined, serverUrl: new URL("https://example.com"),
}, $: {} as PluginInput["$"],
}, } satisfies PluginInput)
} satisfies PluginInput,
{ const createDeps = (overrides: Partial<HookDeps> = {}) => {
showStartupToast: false, const showConfigErrorsIfAny = mock(async () => undefined)
autoUpdate: false, const updateAndShowConnectedProvidersCacheStatus = mock(async () => undefined)
}, const refreshModelCapabilitiesOnStartup = mock(async () => undefined)
{ const showModelCacheWarningIfNeeded = mock(async () => undefined)
const showLocalDevToast = mock(async () => undefined)
const showVersionToast = mock(async () => undefined)
const runBackgroundUpdateCheck = mock(async () => {
await latestVersionMock()
})
const deps: HookDeps = {
getCachedVersion: () => "3.0.0", getCachedVersion: () => "3.0.0",
getLocalDevVersion: () => null, getLocalDevVersion: () => null,
showConfigErrorsIfAny: async () => undefined, showConfigErrorsIfAny,
updateAndShowConnectedProvidersCacheStatus: async () => undefined, updateAndShowConnectedProvidersCacheStatus,
refreshModelCapabilitiesOnStartup: async () => undefined, refreshModelCapabilitiesOnStartup,
showModelCacheWarningIfNeeded: async () => undefined, showModelCacheWarningIfNeeded,
showLocalDevToast: async () => undefined, showLocalDevToast,
showVersionToast: async () => undefined, showVersionToast,
runBackgroundUpdateCheck: async () => { runBackgroundUpdateCheck,
await latestVersionMock()
},
log: () => undefined, log: () => undefined,
...overrides,
}
return {
deps,
mocks: {
showConfigErrorsIfAny,
updateAndShowConnectedProvidersCacheStatus,
refreshModelCapabilitiesOnStartup,
showModelCacheWarningIfNeeded,
showLocalDevToast,
showVersionToast,
runBackgroundUpdateCheck,
}, },
) }
}
const createHook = async (
options: HookOptions = {},
overrides: Partial<HookDeps> = {},
) => {
const module = await import("./hook")
const { deps, mocks } = createDeps(overrides)
return {
hook: module.createAutoUpdateCheckerHook(
createPluginInput(),
{
showStartupToast: true,
autoUpdate: false,
...options,
},
deps,
),
mocks,
}
}
const resetDeferredState = (): void => {
latestVersionCallCount = 0
scheduleDeferredIdleCheckCallCount = 0
scheduledCheck = null
}
const triggerDeferredIdleCheck = async (
hook: ReturnType<CreateAutoUpdateCheckerHook>,
): Promise<void> => {
hook.event({ event: { type: "session.idle" } })
scheduledCheck?.()
await flushMicrotasks(8)
} }
describe("auto-update-checker hook", () => { describe("auto-update-checker hook", () => {
test("defers update check until first session idle", async () => { test("defers update check until first session idle", async () => {
// given // given
latestVersionCallCount = 0 resetDeferredState()
scheduleDeferredIdleCheckCallCount = 0 const { hook, mocks } = await createHook()
scheduledCheck = null
const hook = await createHook()
// when // when
hook.event({ event: { type: "session.created" } }) hook.event({ event: { type: "session.created" } })
// then // then
expect(scheduleDeferredIdleCheckCallCount).toBe(0) expect(scheduleDeferredIdleCheckCallCount).toBe(0)
expect(mocks.showVersionToast).not.toHaveBeenCalled()
expect(mocks.runBackgroundUpdateCheck).not.toHaveBeenCalled()
expect(latestVersionCallCount).toBe(0) expect(latestVersionCallCount).toBe(0)
// when
await triggerDeferredIdleCheck(hook)
// then
expect(scheduleDeferredIdleCheckCallCount).toBe(1)
expect(mocks.showVersionToast).toHaveBeenCalledTimes(1)
expect(mocks.runBackgroundUpdateCheck).toHaveBeenCalledTimes(1)
expect(latestVersionCallCount).toBe(1)
// when // when
hook.event({ event: { type: "session.idle" } }) hook.event({ event: { type: "session.idle" } })
// then // then
expect(scheduleDeferredIdleCheckCallCount).toBe(1) expect(scheduleDeferredIdleCheckCallCount).toBe(1)
expect(latestVersionCallCount).toBe(0) expect(mocks.runBackgroundUpdateCheck).toHaveBeenCalledTimes(1)
})
test("runs all startup checks on normal session.idle", async () => {
// given
resetDeferredState()
const { hook, mocks } = await createHook()
// when // when
await scheduledCheck?.() await triggerDeferredIdleCheck(hook)
// then
expect(mocks.showConfigErrorsIfAny).toHaveBeenCalledTimes(1)
expect(mocks.updateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1)
expect(mocks.refreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1)
expect(mocks.showModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1)
expect(mocks.showVersionToast).toHaveBeenCalledTimes(1)
expect(mocks.runBackgroundUpdateCheck).toHaveBeenCalledTimes(1)
})
test("runs only once (hasChecked guard)", async () => {
// given
resetDeferredState()
const { hook, mocks } = await createHook()
// when
hook.event({ event: { type: "session.idle" } })
hook.event({ event: { type: "session.idle" } })
scheduledCheck?.()
await flushMicrotasks(8) await flushMicrotasks(8)
// then // then
expect(latestVersionCallCount).toBe(1) expect(scheduleDeferredIdleCheckCallCount).toBe(1)
expect(mocks.showConfigErrorsIfAny).toHaveBeenCalledTimes(1)
expect(mocks.updateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1)
expect(mocks.showModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1)
expect(mocks.showVersionToast).toHaveBeenCalledTimes(1)
expect(mocks.runBackgroundUpdateCheck).toHaveBeenCalledTimes(1)
})
test("shows localDevToast when local dev version exists", async () => {
// given
resetDeferredState()
const { hook, mocks } = await createHook({}, {
getLocalDevVersion: () => "3.0.0-dev",
})
// when // when
hook.event({ event: { type: "session.idle" } }) await triggerDeferredIdleCheck(hook)
// then // then
expect(scheduleDeferredIdleCheckCallCount).toBe(1) expect(mocks.showConfigErrorsIfAny).toHaveBeenCalledTimes(1)
expect(latestVersionCallCount).toBe(1) expect(mocks.updateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1)
expect(mocks.showModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1)
expect(mocks.showLocalDevToast).toHaveBeenCalledTimes(1)
expect(mocks.showVersionToast).not.toHaveBeenCalled()
expect(mocks.runBackgroundUpdateCheck).not.toHaveBeenCalled()
expect(latestVersionCallCount).toBe(0)
})
test("passes correct toast message with sisyphus enabled", async () => {
// given
resetDeferredState()
const { hook, mocks } = await createHook({ isSisyphusEnabled: true })
// when
await triggerDeferredIdleCheck(hook)
// then
expect(mocks.showVersionToast).toHaveBeenCalledTimes(1)
expect(mocks.showVersionToast).toHaveBeenCalledWith(
expect.anything(),
"3.0.0",
expect.stringContaining("Sisyphus"),
)
}) })
}) })