From 3a2c4fd099493c00d6238625fa8b661f52d11914 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 17:37:17 +0900 Subject: [PATCH 1/4] fix(tests): type config-handler test spy restore Avoid for the MCP env allowlist spy restore path so the config-handler test keeps the same behavior with specific typing. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugin-handlers/config-handler.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index 050a5ab69..3c0af3a84 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -31,6 +31,8 @@ function createPluginConfig(overrides: Partial = {}): OhMyOp } } +let setAdditionalAllowedMcpEnvVarsSpy: ReturnType | undefined + beforeEach(() => { spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, @@ -57,7 +59,7 @@ beforeEach(() => { spyOn(agentLoader, "loadProjectAgents" as any).mockReturnValue({}) spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ servers: {} }) - spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {}) + setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {}) spyOn(pluginLoader, "loadAllPluginComponents" as any).mockResolvedValue({ commands: {}, @@ -104,7 +106,7 @@ afterEach(() => { ;(agentLoader.loadUserAgents as any)?.mockRestore?.() ;(agentLoader.loadProjectAgents as any)?.mockRestore?.() ;(mcpLoader.loadMcpConfigs as any)?.mockRestore?.() - ;(mcpLoader.setAdditionalAllowedMcpEnvVars as any)?.mockRestore?.() + setAdditionalAllowedMcpEnvVarsSpy?.mockRestore() ;(pluginLoader.loadAllPluginComponents as any)?.mockRestore?.() ;(mcpModule.createBuiltinMcps as any)?.mockRestore?.() ;(shared.log as any)?.mockRestore?.() From 4e435dac73abd284b72b5ea4d76e7499616ef0a2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 17:37:17 +0900 Subject: [PATCH 2/4] fix(tests): type event handler test harness Replace broad casts in event handler tests with typed harness helpers so the regression coverage stays intact while matching the test-file typing rules. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugin/event.test.ts | 169 +++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 67 deletions(-) diff --git a/src/plugin/event.test.ts b/src/plugin/event.test.ts index 0492783e6..3f8e909ad 100644 --- a/src/plugin/event.test.ts +++ b/src/plugin/event.test.ts @@ -7,6 +7,60 @@ import { clearPendingModelFallback, createModelFallbackHook } from "../hooks/mod import { getSessionPromptParams, setSessionPromptParams } from "../shared/session-prompt-params-state" type EventInput = { event: { type: string; properties?: unknown } } +type EventHandlerArgs = Parameters[0] +type EventHandlerInput = Parameters>[0] +type ChatMessageHandlerArgs = Parameters[0] + +function asEventHandlerInput(input: EventInput): EventHandlerInput { + return input as unknown as EventHandlerInput +} + +function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] { + return ctx as unknown as EventHandlerArgs["ctx"] +} + +function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] { + return ctx as unknown as ChatMessageHandlerArgs["ctx"] +} + +function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] { + return config as unknown as EventHandlerArgs["pluginConfig"] +} + +function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] { + return config as unknown as ChatMessageHandlerArgs["pluginConfig"] +} + +function createEventHandlerManagers( + overrides: Record = {}, +): EventHandlerArgs["managers"] { + return { + ...({} as EventHandlerArgs["managers"]), + tmuxSessionManager: { + onSessionCreated: async () => {}, + onSessionDeleted: async () => {}, + }, + ...overrides, + } as unknown as EventHandlerArgs["managers"] +} + +function createEventHandlerHooks( + overrides: Record, +): EventHandlerArgs["hooks"] { + return { + ...({} as EventHandlerArgs["hooks"]), + ...overrides, + } as unknown as EventHandlerArgs["hooks"] +} + +function createChatMessageHandlerHooks( + overrides: Record, +): ChatMessageHandlerArgs["hooks"] { + return { + ...({} as ChatMessageHandlerArgs["hooks"]), + ...overrides, + } as unknown as ChatMessageHandlerArgs["hooks"] +} afterEach(() => { _resetForTesting() @@ -429,12 +483,12 @@ describe("createEventHandler - event forwarding", () => { const sessionID = "ses_forward_delete_event" //#when - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.deleted", properties: { info: { id: sessionID } }, }, - } as any) + })) //#then expect(forwardedEvents.length).toBe(1) @@ -471,12 +525,12 @@ describe("createEventHandler - event forwarding", () => { }) //#when - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.deleted", properties: { info: { id: sessionID } }, }, - }) + })) //#then expect(getSessionPromptParams(sessionID)).toBeUndefined() @@ -495,7 +549,7 @@ describe("createEventHandler - retry dedupe lifecycle", () => { const modelFallback = createModelFallbackHook() const eventHandler = createEventHandler({ - ctx: { + ctx: asEventHandlerContext({ directory: "/tmp", client: { session: { @@ -509,41 +563,37 @@ describe("createEventHandler - retry dedupe lifecycle", () => { }, }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: asPluginConfig({}), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { - tmuxSessionManager: { - onSessionCreated: async () => {}, - onSessionDeleted: async () => {}, - }, + managers: createEventHandlerManagers({ skillMcpManager: { disconnectSession: async () => {}, }, - } as any, - hooks: { + }), + hooks: createEventHandlerHooks({ modelFallback, stopContinuationGuard: { isStopped: () => false }, - } as any, + }), }) const chatMessageHandler = createChatMessageHandler({ - ctx: { + ctx: asChatMessageHandlerContext({ client: { tui: { showToast: async () => ({}), }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: asChatPluginConfig({}), firstMessageVariantGate: { shouldOverride: () => false, markApplied: () => {}, }, - hooks: { + hooks: createChatMessageHandlerHooks({ modelFallback, stopContinuationGuard: null, keywordDetector: null, @@ -551,7 +601,7 @@ describe("createEventHandler - retry dedupe lifecycle", () => { autoSlashCommand: null, startWork: null, ralphLoop: null, - } as any, + }), }) const retryStatus = { @@ -561,7 +611,7 @@ describe("createEventHandler - retry dedupe lifecycle", () => { next: 476, } as const - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "message.updated", properties: { @@ -575,10 +625,10 @@ describe("createEventHandler - retry dedupe lifecycle", () => { }, }, }, - } as any) + })) //#when - first retry key is handled - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.status", properties: { @@ -586,7 +636,7 @@ describe("createEventHandler - retry dedupe lifecycle", () => { status: retryStatus, }, }, - } as any) + })) const firstOutput = { message: {}, parts: [] as Array<{ type: string; text?: string }> } await chatMessageHandler( @@ -599,7 +649,7 @@ describe("createEventHandler - retry dedupe lifecycle", () => { ) //#when - session recovers to non-retry idle state - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.status", properties: { @@ -607,10 +657,10 @@ describe("createEventHandler - retry dedupe lifecycle", () => { status: { type: "idle" }, }, }, - } as any) + })) //#when - same retry key appears again after recovery - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.status", properties: { @@ -618,7 +668,7 @@ describe("createEventHandler - retry dedupe lifecycle", () => { status: retryStatus, }, }, - } as any) + })) //#then expect(abortCalls).toEqual([sessionID, sessionID]) @@ -634,7 +684,7 @@ describe("createEventHandler - session recovery compaction", () => { const callOrder: string[] = [] const eventHandler = createEventHandler({ - ctx: { + ctx: asEventHandlerContext({ directory: "/tmp", client: { session: { @@ -649,29 +699,24 @@ describe("createEventHandler - session recovery compaction", () => { }, }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: asPluginConfig({}), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { - tmuxSessionManager: { - onSessionCreated: async () => {}, - onSessionDeleted: async () => {}, - }, - } as any, - hooks: { + managers: createEventHandlerManagers(), + hooks: createEventHandlerHooks({ sessionRecovery: { isRecoverableError: () => true, handleSessionRecovery: async () => true, }, stopContinuationGuard: { isStopped: () => false }, - } as any, + }), }) //#when - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.error", properties: { @@ -680,7 +725,7 @@ describe("createEventHandler - session recovery compaction", () => { error: { name: "Error", message: "tool_result block(s) that are not immediately" }, }, }, - } as any) + })) //#then - summarize (compaction) must be called before prompt (continue) expect(callOrder).toEqual(["summarize", "prompt"]) @@ -693,7 +738,7 @@ describe("createEventHandler - session recovery compaction", () => { const callOrder: string[] = [] const eventHandler = createEventHandler({ - ctx: { + ctx: asEventHandlerContext({ directory: "/tmp", client: { session: { @@ -708,29 +753,24 @@ describe("createEventHandler - session recovery compaction", () => { }, }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: asPluginConfig({}), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { - tmuxSessionManager: { - onSessionCreated: async () => {}, - onSessionDeleted: async () => {}, - }, - } as any, - hooks: { + managers: createEventHandlerManagers(), + hooks: createEventHandlerHooks({ sessionRecovery: { isRecoverableError: () => true, handleSessionRecovery: async () => true, }, stopContinuationGuard: { isStopped: () => false }, - } as any, + }), }) //#when - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.error", properties: { @@ -739,7 +779,7 @@ describe("createEventHandler - session recovery compaction", () => { error: { name: "Error", message: "tool_result block(s) that are not immediately" }, }, }, - } as any) + })) //#then - continue is still sent even when compaction fails expect(callOrder).toEqual(["summarize", "prompt"]) @@ -750,7 +790,7 @@ describe("createEventHandler - session recovery compaction", () => { const runtimeFallbackCalls: EventInput[] = [] const eventHandler = createEventHandler({ - ctx: { + ctx: asEventHandlerContext({ directory: "/tmp", client: { session: { @@ -758,19 +798,14 @@ describe("createEventHandler - session recovery compaction", () => { prompt: async () => ({}), }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: asPluginConfig({}), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { - tmuxSessionManager: { - onSessionCreated: async () => {}, - onSessionDeleted: async () => {}, - }, - } as any, - hooks: { + managers: createEventHandlerManagers(), + hooks: createEventHandlerHooks({ autoUpdateChecker: { event: async () => { throw new Error("upstream hook failed") @@ -782,13 +817,13 @@ describe("createEventHandler - session recovery compaction", () => { }, }, stopContinuationGuard: { isStopped: () => false }, - } as any, + }), }) //#when let thrownError: unknown try { - await eventHandler({ + await eventHandler(asEventHandlerInput({ event: { type: "session.error", properties: { @@ -796,7 +831,7 @@ describe("createEventHandler - session recovery compaction", () => { error: { name: "Error", message: "retry me" }, }, }, - } as any) + })) } catch (error) { thrownError = error } From e3f3bcc063df23004abc74379e8539e9ea522073 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 17:37:17 +0900 Subject: [PATCH 3/4] fix(tests): type cliproxy fallback test harness Convert the cliproxy fallback matrix test to use typed harness helpers instead of so fallback coverage remains unchanged without violating test constraints. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../fallback.cliproxyapi-matrix.test.ts | 89 ++++++++++++++----- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/src/plugin/fallback.cliproxyapi-matrix.test.ts b/src/plugin/fallback.cliproxyapi-matrix.test.ts index d13930c6f..0acd7a507 100644 --- a/src/plugin/fallback.cliproxyapi-matrix.test.ts +++ b/src/plugin/fallback.cliproxyapi-matrix.test.ts @@ -14,10 +14,55 @@ import { createEventHandler } from "./event" import { createChatMessageHandler } from "./chat-message" import { createModelFallbackHook } from "../hooks/model-fallback/hook" import { createRuntimeFallbackHook } from "../hooks/runtime-fallback" +import type { RuntimeFallbackPluginInput } from "../hooks/runtime-fallback/types" import { _resetForTesting } from "../features/claude-code-session-state" import { _resetForTesting as _resetModelFallbackForTesting } from "../hooks/model-fallback/hook" import { SessionCategoryRegistry } from "../shared/session-category-registry" +type EventHandlerArgs = Parameters[0] +type ChatMessageHandlerArgs = Parameters[0] +type HarnessContext = EventHandlerArgs["ctx"] & RuntimeFallbackPluginInput +type HarnessEventInput = Parameters["eventHandler"]>[0] + +function asHarnessEventInput(input: unknown): HarnessEventInput { + return input as unknown as HarnessEventInput +} + +function asHarnessContext(ctx: unknown): HarnessContext { + return ctx as unknown as HarnessContext +} + +function createEventHandlerManagers( + overrides: Record = {}, +): EventHandlerArgs["managers"] { + return { + ...({} as EventHandlerArgs["managers"]), + tmuxSessionManager: { + onSessionCreated: async () => {}, + onSessionDeleted: async () => {}, + }, + ...overrides, + } as unknown as EventHandlerArgs["managers"] +} + +function createEventHandlerHooks( + overrides: Record, +): EventHandlerArgs["hooks"] { + return { + ...({} as EventHandlerArgs["hooks"]), + ...overrides, + } as unknown as EventHandlerArgs["hooks"] +} + +function createChatMessageHandlerHooks( + overrides: Record, +): ChatMessageHandlerArgs["hooks"] { + return { + ...({} as ChatMessageHandlerArgs["hooks"]), + ...overrides, + } as unknown as ChatMessageHandlerArgs["hooks"] +} + const PRIMARY_MODEL = { providerID: PROVIDER_ID, modelID: "claude-opus-4-6", @@ -59,7 +104,7 @@ function createPluginConfig(mode: HarnessMode) { }, } : {}), - } + } as unknown as EventHandlerArgs["pluginConfig"] } function createHarness(args: { @@ -72,7 +117,7 @@ function createHarness(args: { const promptAsyncCalls: PromptAsyncCall[] = [] const pluginConfig = createPluginConfig(args.mode) - const ctx = { + const ctx = asHarnessContext({ directory: "/tmp", client: { session: { @@ -119,7 +164,7 @@ function createHarness(args: { showToast: async () => ({}), }, }, - } as any + }) const hooks: Record = { stopContinuationGuard: null, @@ -145,38 +190,34 @@ function createHarness(args: { timeout_seconds: args.sessionTimeoutMs ? 30 : 0, notify_on_fallback: false, }, - pluginConfig, + pluginConfig: pluginConfig as unknown as EventHandlerArgs["pluginConfig"], ...(args.sessionTimeoutMs ? { session_timeout_ms: args.sessionTimeoutMs } : {}), }) } const eventHandler = createEventHandler({ ctx, - pluginConfig: pluginConfig as any, + pluginConfig: pluginConfig as unknown as EventHandlerArgs["pluginConfig"], firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { - tmuxSessionManager: { - onSessionCreated: async () => {}, - onSessionDeleted: async () => {}, - }, + managers: createEventHandlerManagers({ skillMcpManager: { disconnectSession: async () => {}, }, - } as any, - hooks: hooks as any, + }), + hooks: createEventHandlerHooks(hooks), }) const chatMessageHandler = createChatMessageHandler({ ctx, - pluginConfig: pluginConfig as any, + pluginConfig: pluginConfig as unknown as ChatMessageHandlerArgs["pluginConfig"], firstMessageVariantGate: { shouldOverride: () => false, markApplied: () => {}, }, - hooks: hooks as any, + hooks: createChatMessageHandlerHooks(hooks), }) return { @@ -192,7 +233,7 @@ async function primeMainSession( eventHandler: ReturnType["eventHandler"], sessionID: string, ) { - await eventHandler({ + await eventHandler(asHarnessEventInput({ event: { type: "session.created", properties: { @@ -202,9 +243,9 @@ async function primeMainSession( }, }, }, - }) + })) - await eventHandler({ + await eventHandler(asHarnessEventInput({ event: { type: "message.updated", properties: { @@ -221,7 +262,7 @@ async function primeMainSession( }, }, }, - }) + })) } async function sendNextMessage( @@ -241,7 +282,7 @@ async function triggerSessionError( eventHandler: ReturnType["eventHandler"], sessionID: string, ) { - await eventHandler({ + await eventHandler(asHarnessEventInput({ event: { type: "session.error", properties: { @@ -256,14 +297,14 @@ async function triggerSessionError( }, }, }, - }) + })) } async function triggerSessionStatusRetry( eventHandler: ReturnType["eventHandler"], sessionID: string, ) { - await eventHandler({ + await eventHandler(asHarnessEventInput({ event: { type: "session.status", properties: { @@ -279,14 +320,14 @@ async function triggerSessionStatusRetry( }, }, }, - }) + })) } async function triggerAssistantMessageError( eventHandler: ReturnType["eventHandler"], sessionID: string, ) { - await eventHandler({ + await eventHandler(asHarnessEventInput({ event: { type: "message.updated", properties: { @@ -307,7 +348,7 @@ async function triggerAssistantMessageError( }, }, }, - }) + })) } afterEach(() => { From f369971db99f0034bbf7b1bf404bc34bdc986536 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 17:37:17 +0900 Subject: [PATCH 4/4] fix(tests): type tmux fetch mocks Model the tmux fetch test doubles with the fetch shape Bun expects so the tests drop without changing assertions. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/tmux/tmux-utils.test.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/shared/tmux/tmux-utils.test.ts b/src/shared/tmux/tmux-utils.test.ts index c5c4ea243..421cc070b 100644 --- a/src/shared/tmux/tmux-utils.test.ts +++ b/src/shared/tmux/tmux-utils.test.ts @@ -10,6 +10,14 @@ import { } from "./tmux-utils" import { isInsideTmuxEnvironment } from "./tmux-utils/environment" +function createFetchMock(responseFactory: () => Promise): typeof fetch & ReturnType { + const fetchMock = mock(async (_input: RequestInfo | URL, _init?: RequestInit) => responseFactory()) + const preconnect = globalThis.fetch.preconnect?.bind(globalThis.fetch) + return Object.assign(fetchMock, { + preconnect, + }) as typeof fetch & ReturnType +} + describe("isInsideTmux", () => { test("returns true when TMUX env is set", () => { // given @@ -66,7 +74,7 @@ describe("isServerRunning", () => { test("returns true when server responds OK", async () => { // given - globalThis.fetch = mock(async () => ({ ok: true })) as any + globalThis.fetch = createFetchMock(async () => new Response(null, { status: 200 })) // when const result = await isServerRunning("http://localhost:4096") @@ -77,9 +85,9 @@ describe("isServerRunning", () => { test("returns false when server not reachable", async () => { // given - globalThis.fetch = mock(async () => { + globalThis.fetch = createFetchMock(async () => { throw new Error("ECONNREFUSED") - }) as any + }) // when const result = await isServerRunning("http://localhost:4096") @@ -90,7 +98,7 @@ describe("isServerRunning", () => { test("returns false when fetch returns not ok", async () => { // given - globalThis.fetch = mock(async () => ({ ok: false })) as any + globalThis.fetch = createFetchMock(async () => new Response(null, { status: 500 })) // when const result = await isServerRunning("http://localhost:4096") @@ -101,7 +109,7 @@ describe("isServerRunning", () => { test("caches successful result", async () => { // given - const fetchMock = mock(async () => ({ ok: true })) as any + const fetchMock = createFetchMock(async () => new Response(null, { status: 200 })) globalThis.fetch = fetchMock // when @@ -114,9 +122,9 @@ describe("isServerRunning", () => { test("does not cache failed result", async () => { // given - const fetchMock = mock(async () => { + const fetchMock = createFetchMock(async () => { throw new Error("ECONNREFUSED") - }) as any + }) globalThis.fetch = fetchMock // when @@ -129,7 +137,7 @@ describe("isServerRunning", () => { test("uses different cache for different URLs", async () => { // given - const fetchMock = mock(async () => ({ ok: true })) as any + const fetchMock = createFetchMock(async () => new Response(null, { status: 200 })) globalThis.fetch = fetchMock // when @@ -150,7 +158,7 @@ describe("resetServerCheck", () => { test("allows re-checking after reset", async () => { // given const originalFetch = globalThis.fetch - const fetchMock = mock(async () => ({ ok: true })) as any + const fetchMock = createFetchMock(async () => new Response(null, { status: 200 })) globalThis.fetch = fetchMock // when @@ -182,7 +190,7 @@ describe("markServerRunningInProcess", () => { test("skips HTTP fetch when marked as running in-process", async () => { // given - const fetchMock = mock(async () => ({ ok: true })) as any + const fetchMock = createFetchMock(async () => new Response(null, { status: 200 })) globalThis.fetch = fetchMock markServerRunningInProcess()