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()
+})