Merge branch 'fix/perf-d04' into fix/perf-omo-in-tree
This commit is contained in:
@@ -107,9 +107,10 @@ describe("createRuntimeFallbackHook dispose", () => {
|
|||||||
globalThis.clearTimeout = originalClearTimeout
|
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
|
// given
|
||||||
const hook = createRuntimeFallbackHook(createMockContext(), { pluginConfig: {} })
|
const hook = createRuntimeFallbackHook(createMockContext(), { pluginConfig: {} })
|
||||||
|
await hook.event({ event: { type: "session.created", properties: {} } })
|
||||||
|
|
||||||
// when
|
// when
|
||||||
hook.dispose?.()
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { HookDeps, RuntimeFallbackHook, RuntimeFallbackInterval, RuntimeFallbackOptions, RuntimeFallbackPluginInput, RuntimeFallbackTimeout } from "./types"
|
import type { HookDeps, RuntimeFallbackHook, RuntimeFallbackInterval, RuntimeFallbackOptions, RuntimeFallbackPluginInput, RuntimeFallbackTimeout } from "./types"
|
||||||
import { DEFAULT_CONFIG, HOOK_NAME } from "./constants"
|
import { DEFAULT_CONFIG } from "./constants"
|
||||||
import { log } from "../../shared/logger"
|
|
||||||
import { loadPluginConfig } from "../../plugin-config"
|
|
||||||
import { createAutoRetryHelpers } from "./auto-retry"
|
import { createAutoRetryHelpers } from "./auto-retry"
|
||||||
import { createEventHandler } from "./event-handler"
|
import { createEventHandler } from "./event-handler"
|
||||||
import { createMessageUpdateHandler } from "./message-update-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,
|
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 = {
|
const deps: HookDeps = {
|
||||||
ctx,
|
ctx,
|
||||||
config,
|
config,
|
||||||
options,
|
options,
|
||||||
pluginConfig,
|
pluginConfig: options?.pluginConfig,
|
||||||
sessionStates: new Map(),
|
sessionStates: new Map(),
|
||||||
sessionLastAccess: new Map(),
|
sessionLastAccess: new Map(),
|
||||||
sessionRetryInFlight: new Set(),
|
sessionRetryInFlight: new Set(),
|
||||||
@@ -51,10 +40,23 @@ export function createRuntimeFallbackHook(
|
|||||||
const messageUpdateHandler = createMessageUpdateHandler(deps, helpers)
|
const messageUpdateHandler = createMessageUpdateHandler(deps, helpers)
|
||||||
const chatMessageHandler = createChatMessageHandler(deps)
|
const chatMessageHandler = createChatMessageHandler(deps)
|
||||||
|
|
||||||
const cleanupInterval = setInterval(helpers.cleanupStaleSessions, 5 * 60 * 1000)
|
let cleanupInterval: RuntimeFallbackInterval | null = null
|
||||||
cleanupInterval.unref()
|
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 } }) => {
|
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||||
|
ensureInterval()
|
||||||
|
|
||||||
if (event.type === "message.updated") {
|
if (event.type === "message.updated") {
|
||||||
if (!config.enabled) return
|
if (!config.enabled) return
|
||||||
const props = event.properties as Record<string, unknown> | undefined
|
const props = event.properties as Record<string, unknown> | undefined
|
||||||
@@ -65,7 +67,9 @@ export function createRuntimeFallbackHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dispose = () => {
|
const dispose = () => {
|
||||||
clearInterval(cleanupInterval)
|
if (cleanupInterval) {
|
||||||
|
clearInterval(cleanupInterval)
|
||||||
|
}
|
||||||
|
|
||||||
for (const fallbackTimeout of deps.sessionFallbackTimeouts.values()) {
|
for (const fallbackTimeout of deps.sessionFallbackTimeouts.values()) {
|
||||||
clearTimeout(fallbackTimeout)
|
clearTimeout(fallbackTimeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user