From fa1b4650ce5463cfdb9629d268a6968963df49db Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 10 Apr 2026 15:53:25 +0900 Subject: [PATCH] test(core): update main test setup and core tests Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/create-managers.test.ts | 175 +++++++++++++++++++++++------------- src/index.test.ts | 128 +++++++++++++------------- test-setup.ts | 49 +++++++++- 3 files changed, 227 insertions(+), 125 deletions(-) diff --git a/src/create-managers.test.ts b/src/create-managers.test.ts index 94a18460d..fa32bca4e 100644 --- a/src/create-managers.test.ts +++ b/src/create-managers.test.ts @@ -1,8 +1,12 @@ /// -import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" +import type { PluginInput } from "@opencode-ai/plugin" +import { OhMyOpenCodeConfigSchema } from "./config/schema/oh-my-opencode-config" +import { createManagers } from "./create-managers" import * as openclawRuntimeDispatch from "./openclaw/runtime-dispatch" +import { createModelCacheState } from "./plugin-state" const markServerRunningInProcess = mock(() => {}) let backgroundManagerOptions: { @@ -10,57 +14,63 @@ let backgroundManagerOptions: { } | null = null const trackedPaneBySession = new Map() -mock.module("./features/background-agent", () => ({ - BackgroundManager: class BackgroundManager { - constructor(_ctx: unknown, _config: unknown, options: typeof backgroundManagerOptions) { - backgroundManagerOptions = options +class MockBackgroundManager { + constructor( + _ctx: PluginInput, + _config?: unknown, + options?: { + tmuxConfig?: unknown + onSubagentSessionCreated?: (event: { sessionID: string; parentID: string; title: string }) => Promise + onShutdown?: () => void | Promise + enableParentSessionNotifications?: boolean + }, + ) { + backgroundManagerOptions = options ?? null + } +} + +class MockSkillMcpManager { + constructor(..._args: unknown[]) {} +} + +class MockTmuxSessionManager { + constructor(_ctx: PluginInput, _config: unknown) {} + + async cleanup(): Promise {} + + async onSessionCreated(event: { properties?: { info?: { id?: string } } }): Promise { + const sessionID = event.properties?.info?.id + if (sessionID) { + trackedPaneBySession.set(sessionID, `%pane-${sessionID}`) } - }, -})) + } -mock.module("./features/skill-mcp-manager", () => ({ - SkillMcpManager: class SkillMcpManager { - constructor(..._args: unknown[]) {} - }, -})) + getTrackedPaneId(sessionID: string): string | undefined { + return trackedPaneBySession.get(sessionID) + } +} -mock.module("./features/task-toast-manager", () => ({ - initTaskToastManager: mock(() => {}), -})) +function createConfigHandler(): ReturnType { + return async () => {} +} -mock.module("./features/tmux-subagent", () => ({ - TmuxSessionManager: class TmuxSessionManager { - constructor(..._args: unknown[]) {} +function initTaskToastManager(): ReturnType { + return {} as ReturnType +} - async cleanup(): Promise {} - async onSessionCreated(event: { properties?: { info?: { id?: string } } }): Promise { - const sessionID = event.properties?.info?.id - if (sessionID) { - trackedPaneBySession.set(sessionID, `%pane-${sessionID}`) - } - } +function registerManagerForCleanup(): void {} - getTrackedPaneId(sessionID: string): string | undefined { - return trackedPaneBySession.get(sessionID) - } - }, -})) - -mock.module("./features/background-agent/process-cleanup", () => ({ - registerManagerForCleanup: mock(() => {}), -})) - -mock.module("./plugin-handlers", () => ({ - createConfigHandler: mock(() => ({ kind: "config-handler" })), -})) - -mock.module("./shared/tmux/tmux-utils/server-health", () => ({ - isServerRunning: mock(async () => true), - markServerRunningInProcess, - resetServerCheck: mock(() => {}), -})) - -const { createManagers } = await import("./create-managers") +function createDeps(): NonNullable[0]["deps"]> { + return { + BackgroundManagerClass: MockBackgroundManager as typeof import("./features/background-agent").BackgroundManager, + SkillMcpManagerClass: MockSkillMcpManager as typeof import("./features/skill-mcp-manager").SkillMcpManager, + TmuxSessionManagerClass: MockTmuxSessionManager as typeof import("./features/tmux-subagent").TmuxSessionManager, + initTaskToastManagerFn: initTaskToastManager, + registerManagerForCleanupFn: registerManagerForCleanup, + createConfigHandlerFn: createConfigHandler, + markServerRunningInProcessFn: markServerRunningInProcess, + } +} function createTmuxConfig(enabled: boolean) { return { @@ -73,28 +83,67 @@ function createTmuxConfig(enabled: boolean) { } } +function createContext(directory: string): PluginInput { + const shell = Object.assign( + () => { + throw new Error("shell should not be called in this test") + }, + { + braces: () => [], + escape: (input: string) => input, + env() { + return shell + }, + cwd() { + return shell + }, + nothrow() { + return shell + }, + throws() { + return shell + }, + }, + ) + + return { + project: { + id: "project-id", + worktree: directory, + time: { created: Date.now() }, + }, + directory, + worktree: directory, + serverUrl: new URL("http://localhost:4096"), + $: shell, + client: {} as PluginInput["client"], + } +} + describe("createManagers", () => { - const dispatchOpenClawEvent = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent") + let dispatchOpenClawEvent: ReturnType beforeEach(() => { + dispatchOpenClawEvent = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent") markServerRunningInProcess.mockClear() dispatchOpenClawEvent.mockReset() backgroundManagerOptions = null trackedPaneBySession.clear() }) - afterAll(() => { - mock.restore() + afterEach(() => { + dispatchOpenClawEvent.mockRestore() }) it("#given tmux integration is disabled #when managers are created #then it does not mark the tmux server as running", () => { const args = { - ctx: { directory: "/tmp", client: {} }, - pluginConfig: {}, + ctx: createContext("/tmp"), + pluginConfig: OhMyOpenCodeConfigSchema.parse({}), tmuxConfig: createTmuxConfig(false), - modelCacheState: {}, + modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, - } as Parameters[0] + deps: createDeps(), + } createManagers(args) @@ -103,12 +152,13 @@ describe("createManagers", () => { it("#given tmux integration is enabled #when managers are created #then it marks the tmux server as running", () => { const args = { - ctx: { directory: "/tmp", client: {} }, - pluginConfig: {}, + ctx: createContext("/tmp"), + pluginConfig: OhMyOpenCodeConfigSchema.parse({}), tmuxConfig: createTmuxConfig(true), - modelCacheState: {}, + modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, - } as Parameters[0] + deps: createDeps(), + } createManagers(args) @@ -117,18 +167,19 @@ describe("createManagers", () => { it("#given openclaw is enabled #when the background session-created callback runs #then it dispatches openclaw with the tracked pane id", async () => { const args = { - ctx: { directory: "/tmp/project", client: {} }, - pluginConfig: { + ctx: createContext("/tmp/project"), + pluginConfig: OhMyOpenCodeConfigSchema.parse({ openclaw: { enabled: true, gateways: {}, hooks: {}, }, - }, + }), tmuxConfig: createTmuxConfig(true), - modelCacheState: {}, + modelCacheState: createModelCacheState(), backgroundNotificationHookEnabled: false, - } as Parameters[0] + deps: createDeps(), + } createManagers(args) diff --git a/src/index.test.ts b/src/index.test.ts index e9d4baee3..7101b9f09 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" describe("experimental.session.compacting handler", () => { function createCompactingHandler(hooks: { @@ -257,83 +257,87 @@ const mockCreatePluginInterface = mock(() => ({})) const mockInitializeOpenClaw = mock(async () => {}) const mockStartTmuxCheck = mock(() => {}) -mock.module("./cli/config-manager/config-context", () => ({ - initConfigContext: mockInitConfigContext, -})) +let OhMyOpenCodePlugin: (typeof import("./index"))["default"] -mock.module("./shared/external-plugin-detector", () => ({ - detectExternalSkillPlugin: mockDetectExternalSkillPlugin, - getSkillPluginConflictWarning: mockGetSkillPluginConflictWarning, -})) +function installIndexModuleMocks(): void { + mock.module("./cli/config-manager/config-context", () => ({ + initConfigContext: mockInitConfigContext, + })) -mock.module("./shared", () => ({ - injectServerAuthIntoClient: mockInjectServerAuthIntoClient, - log: mock(() => {}), - logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning, -})) + mock.module("./shared/external-plugin-detector", () => ({ + detectExternalSkillPlugin: mockDetectExternalSkillPlugin, + getSkillPluginConflictWarning: mockGetSkillPluginConflictWarning, + })) -mock.module("./plugin-config", () => ({ - loadPluginConfig: mockLoadPluginConfig, -})) + mock.module("./shared", () => ({ + injectServerAuthIntoClient: mockInjectServerAuthIntoClient, + log: mock(() => {}), + logLegacyPluginStartupWarning: mockLogLegacyPluginStartupWarning, + })) -mock.module("./create-runtime-tmux-config", () => ({ - createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig, - isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled, - isInteractiveBashEnabled: mockIsInteractiveBashEnabled, -})) + mock.module("./plugin-config", () => ({ + loadPluginConfig: mockLoadPluginConfig, + })) -mock.module("./create-managers", () => ({ - createManagers: mockCreateManagers, -})) + mock.module("./create-runtime-tmux-config", () => ({ + createRuntimeTmuxConfig: mockCreateRuntimeTmuxConfig, + isTmuxIntegrationEnabled: mockIsTmuxIntegrationEnabled, + isInteractiveBashEnabled: mockIsInteractiveBashEnabled, + })) -mock.module("./create-tools", () => ({ - createTools: mockCreateTools, -})) + mock.module("./create-managers", () => ({ + createManagers: mockCreateManagers, + })) -mock.module("./create-hooks", () => ({ - createHooks: mockCreateHooks, -})) + mock.module("./create-tools", () => ({ + createTools: mockCreateTools, + })) -mock.module("./plugin-dispose", () => ({ - createPluginDispose: mockCreatePluginDispose, -})) + mock.module("./create-hooks", () => ({ + createHooks: mockCreateHooks, + })) -mock.module("./plugin-interface", () => ({ - createPluginInterface: mockCreatePluginInterface, -})) + mock.module("./plugin-dispose", () => ({ + createPluginDispose: mockCreatePluginDispose, + })) -mock.module("./plugin-state", () => ({ - createModelCacheState: mock(() => ({})), -})) + mock.module("./plugin-interface", () => ({ + createPluginInterface: mockCreatePluginInterface, + })) -mock.module("./shared/first-message-variant", () => ({ - createFirstMessageVariantGate: mock(() => ({ - shouldOverride: () => false, - markApplied: () => {}, - markSessionCreated: () => {}, - clear: () => {}, - })), -})) + mock.module("./plugin-state", () => ({ + createModelCacheState: mock(() => ({})), + })) -mock.module("./openclaw", () => ({ - initializeOpenClaw: mockInitializeOpenClaw, -})) + mock.module("./shared/first-message-variant", () => ({ + createFirstMessageVariantGate: mock(() => ({ + shouldOverride: () => false, + markApplied: () => {}, + markSessionCreated: () => {}, + clear: () => {}, + })), + })) -mock.module("./tools/interactive-bash", () => ({ - interactive_bash: {}, - startBackgroundCheck: mockStartTmuxCheck, -})) + mock.module("./openclaw", () => ({ + initializeOpenClaw: mockInitializeOpenClaw, + })) -mock.module("./tools/lsp/client", () => ({ - lspManager: { - cleanupTempDirectoryClients: async () => {}, - }, -})) + mock.module("./tools/interactive-bash", () => ({ + interactive_bash: {}, + startBackgroundCheck: mockStartTmuxCheck, + })) -const { default: OhMyOpenCodePlugin } = await import("./index") +} + +async function importFreshIndexModule(): Promise { + return import(`./index?test=${Date.now()}-${Math.random()}`) +} describe("OhMyOpenCodePlugin", () => { - beforeEach(() => { + beforeEach(async () => { + mock.restore() + installIndexModuleMocks() + ;({ default: OhMyOpenCodePlugin } = await importFreshIndexModule()) mockInitConfigContext.mockClear() mockDetectExternalSkillPlugin.mockClear() mockGetSkillPluginConflictWarning.mockClear() @@ -352,7 +356,7 @@ describe("OhMyOpenCodePlugin", () => { mockStartTmuxCheck.mockClear() }) - afterAll(() => { + afterEach(() => { mock.restore() }) diff --git a/test-setup.ts b/test-setup.ts index 6bb51814b..e66350edb 100644 --- a/test-setup.ts +++ b/test-setup.ts @@ -1,10 +1,57 @@ -import { beforeEach } from "bun:test" +import { afterEach, beforeEach, mock } from "bun:test" +import { rmSync } from "node:fs" import { _resetForTesting as resetClaudeSessionState } from "./src/features/claude-code-session-state/state" +import { _resetTaskToastManagerForTesting as resetTaskToastManager } from "./src/features/task-toast-manager/manager" import { _resetForTesting as resetModelFallbackState } from "./src/hooks/model-fallback/hook" import { _resetMemCacheForTesting as resetConnectedProvidersCache } from "./src/shared/connected-providers-cache" +import { getOmoOpenCodeCacheDir } from "./src/shared/data-path" +import { installModuleMockLifecycle } from "./src/testing/module-mock-lifecycle" + +const { restoreModuleMocks } = installModuleMockLifecycle(mock) +let environmentSnapshot: NodeJS.ProcessEnv = { ...process.env } +let workingDirectorySnapshot = process.cwd() + +function cleanupOmoCacheDir(cacheDir: string): void { + rmSync(cacheDir, { recursive: true, force: true }) +} beforeEach(() => { + environmentSnapshot = { ...process.env } + workingDirectorySnapshot = process.cwd() + process.env.OMO_DISABLE_POSTHOG = "true" + cleanupOmoCacheDir(getOmoOpenCodeCacheDir()) resetClaudeSessionState() + resetTaskToastManager() resetModelFallbackState() resetConnectedProvidersCache() }) + +afterEach(() => { + const currentCacheDir = getOmoOpenCodeCacheDir() + + for (const key of Object.keys(process.env)) { + if (!(key in environmentSnapshot)) { + delete process.env[key] + } + } + + for (const [key, value] of Object.entries(environmentSnapshot)) { + if (value === undefined) { + delete process.env[key] + continue + } + + process.env[key] = value + } + + if (process.cwd() !== workingDirectorySnapshot) { + process.chdir(workingDirectorySnapshot) + } + + cleanupOmoCacheDir(currentCacheDir) + cleanupOmoCacheDir(getOmoOpenCodeCacheDir()) + resetTaskToastManager() + resetConnectedProvidersCache() + mock.restore() + restoreModuleMocks() +})