Merge branch 'fix/perf-d04' into fix/perf-omo-in-tree

This commit is contained in:
Sisyphus
2026-04-18 14:43:37 +09:00
3 changed files with 146 additions and 17 deletions
+2 -1
View File
@@ -107,9 +107,10 @@ describe("createRuntimeFallbackHook dispose", () => {
globalThis.clearTimeout = originalClearTimeout
})
test("#given runtime-fallback hook created #when dispose() is called #then cleanup interval is cleared", () => {
test("#given runtime-fallback hook handles its first event #when dispose() is called #then cleanup interval is cleared", async () => {
// given
const hook = createRuntimeFallbackHook(createMockContext(), { pluginConfig: {} })
await hook.event({ event: { type: "session.created", properties: {} } })
// when
hook.dispose?.()
@@ -0,0 +1,124 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
import type { OhMyOpenCodeConfig } from "../../config"
import type { HookDeps, RuntimeFallbackInterval, RuntimeFallbackPluginInput } from "./types"
type RuntimeFallbackModule = typeof import("./hook")
const loadPluginConfigMock = mock(() => ({} satisfies OhMyOpenCodeConfig))
const createAutoRetryHelpersMock = mock((_deps: HookDeps) => {
void _deps
return {
abortSessionRequest: async () => {},
clearSessionFallbackTimeout: () => {},
scheduleSessionFallbackTimeout: () => {},
autoRetryWithFallback: async () => {},
resolveAgentForSessionFromContext: async () => undefined,
cleanupStaleSessions: () => {},
}
})
const createEventHandlerMock = mock(() => async () => {})
const createMessageUpdateHandlerMock = mock(() => async () => {})
const createChatMessageHandlerMock = mock(() => async () => {})
function registerModuleMocks(): void {
mock.module("../../plugin-config", () => ({
loadPluginConfig: loadPluginConfigMock,
}))
mock.module("./auto-retry", () => ({
createAutoRetryHelpers: createAutoRetryHelpersMock,
}))
mock.module("./event-handler", () => ({
createEventHandler: createEventHandlerMock,
}))
mock.module("./message-update-handler", () => ({
createMessageUpdateHandler: createMessageUpdateHandlerMock,
}))
mock.module("./chat-message-handler", () => ({
createChatMessageHandler: createChatMessageHandlerMock,
}))
}
function createMockContext(): RuntimeFallbackPluginInput {
return {
client: {
session: {
abort: async () => ({}),
messages: async () => ({}),
promptAsync: async () => ({}),
},
tui: {
showToast: async () => ({}),
},
},
directory: "/test",
}
}
function createMockInterval(): RuntimeFallbackInterval {
return {
unref: () => {},
}
}
describe("createRuntimeFallbackHook initialization", () => {
const originalSetInterval = globalThis.setInterval
let setIntervalCalls = 0
let createRuntimeFallbackHook: RuntimeFallbackModule["createRuntimeFallbackHook"]
beforeEach(async () => {
mock.restore()
registerModuleMocks()
loadPluginConfigMock.mockClear()
createAutoRetryHelpersMock.mockClear()
createEventHandlerMock.mockClear()
createMessageUpdateHandlerMock.mockClear()
createChatMessageHandlerMock.mockClear()
setIntervalCalls = 0
globalThis.setInterval = ((callback: Parameters<typeof originalSetInterval>[0], delay?: number) => {
void callback
void delay
setIntervalCalls += 1
return createMockInterval() as ReturnType<typeof globalThis.setInterval>
}) as typeof globalThis.setInterval
const cacheBuster = `${Date.now()}-${Math.random()}`
const runtimeFallbackModule: RuntimeFallbackModule = await import(`./hook?test=${cacheBuster}`)
createRuntimeFallbackHook = runtimeFallbackModule.createRuntimeFallbackHook
})
afterEach(() => {
globalThis.setInterval = originalSetInterval
mock.restore()
})
test("#given injected pluginConfig #when the hook factory runs #then loadPluginConfig is not called", () => {
// given
const pluginConfig = {} satisfies OhMyOpenCodeConfig
// when
createRuntimeFallbackHook(createMockContext(), { pluginConfig })
// then
expect(loadPluginConfigMock).not.toHaveBeenCalled()
})
test("#given a fresh hook #when the first event arrives #then cleanup interval starts only once", async () => {
// given
const hook = createRuntimeFallbackHook(createMockContext(), { pluginConfig: {} })
// when
expect(setIntervalCalls).toBe(0)
await hook.event({ event: { type: "session.created", properties: {} } })
expect(setIntervalCalls).toBe(1)
await hook.event({ event: { type: "session.error", properties: {} } })
// then
expect(setIntervalCalls).toBe(1)
})
})
+20 -16
View File
@@ -1,7 +1,5 @@
import type { HookDeps, RuntimeFallbackHook, RuntimeFallbackInterval, RuntimeFallbackOptions, RuntimeFallbackPluginInput, RuntimeFallbackTimeout } from "./types"
import { DEFAULT_CONFIG, HOOK_NAME } from "./constants"
import { log } from "../../shared/logger"
import { loadPluginConfig } from "../../plugin-config"
import { DEFAULT_CONFIG } from "./constants"
import { createAutoRetryHelpers } from "./auto-retry"
import { createEventHandler } from "./event-handler"
import { createMessageUpdateHandler } from "./message-update-handler"
@@ -24,20 +22,11 @@ export function createRuntimeFallbackHook(
notify_on_fallback: options?.config?.notify_on_fallback ?? DEFAULT_CONFIG.notify_on_fallback,
}
let pluginConfig = options?.pluginConfig
if (!pluginConfig) {
try {
pluginConfig = loadPluginConfig(ctx.directory, ctx)
} catch {
log(`[${HOOK_NAME}] Plugin config not available`)
}
}
const deps: HookDeps = {
ctx,
config,
options,
pluginConfig,
pluginConfig: options?.pluginConfig,
sessionStates: new Map(),
sessionLastAccess: new Map(),
sessionRetryInFlight: new Set(),
@@ -51,10 +40,23 @@ export function createRuntimeFallbackHook(
const messageUpdateHandler = createMessageUpdateHandler(deps, helpers)
const chatMessageHandler = createChatMessageHandler(deps)
const cleanupInterval = setInterval(helpers.cleanupStaleSessions, 5 * 60 * 1000)
cleanupInterval.unref()
let cleanupInterval: RuntimeFallbackInterval | null = null
let intervalStarted = false
const ensureInterval = (): void => {
if (intervalStarted) return
intervalStarted = true
cleanupInterval = setInterval(helpers.cleanupStaleSessions, 5 * 60 * 1000)
if (typeof cleanupInterval.unref === "function") {
cleanupInterval.unref()
}
}
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
ensureInterval()
if (event.type === "message.updated") {
if (!config.enabled) return
const props = event.properties as Record<string, unknown> | undefined
@@ -65,7 +67,9 @@ export function createRuntimeFallbackHook(
}
const dispose = () => {
clearInterval(cleanupInterval)
if (cleanupInterval) {
clearInterval(cleanupInterval)
}
for (const fallbackTimeout of deps.sessionFallbackTimeouts.values()) {
clearTimeout(fallbackTimeout)