diff --git a/src/plugin-config.test.ts b/src/plugin-config.test.ts index 7626ed2f4..8ac0bdee5 100644 --- a/src/plugin-config.test.ts +++ b/src/plugin-config.test.ts @@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, wr import { tmpdir } from "node:os" import { join } from "node:path" import * as shared from "./shared" -import { loadPluginConfig, mergeConfigs, parseConfigPartially } from "./plugin-config"; +import { mergeConfigs, parseConfigPartially } from "./plugin-config"; import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config"; const tempDirs: string[] = [] @@ -12,6 +12,10 @@ function createConfig(config: Partial): OhMyOpenCodeConfig { return OhMyOpenCodeConfigSchema.parse(config) } +async function importFreshPluginConfigModule(): Promise { + return import(`./plugin-config?test=${Date.now()}-${Math.random()}`) +} + afterEach(() => { mock.restore() @@ -297,7 +301,7 @@ describe("parseConfigPartially", () => { }); describe("loadPluginConfig", () => { - it("should only honor mcp_env_allowlist from user config", () => { + it("should only honor mcp_env_allowlist from user config", async () => { // given const rootDir = mkdtempSync(join(tmpdir(), "omo-plugin-config-")) const userConfigDir = join(rootDir, "user-config") @@ -317,16 +321,17 @@ describe("loadPluginConfig", () => { JSON.stringify({ mcp_env_allowlist: ["PROJECT_TOKEN"] }) ) - spyOn(shared, "getOpenCodeConfigDir").mockReturnValue(userConfigDir) + process.env.OPENCODE_CONFIG_DIR = userConfigDir // when + const { loadPluginConfig } = await importFreshPluginConfigModule() const config = loadPluginConfig(projectDir, {}) // then expect(config.mcp_env_allowlist).toEqual(["USER_ONLY_TOKEN"]) }) - it("should ignore edits to the renamed legacy backup after migration", () => { + it("should ignore edits to the renamed legacy backup after migration", async () => { // given const rootDir = mkdtempSync(join(tmpdir(), "omo-plugin-config-legacy-")) const userConfigDir = join(rootDir, "user-config") @@ -341,9 +346,10 @@ describe("loadPluginConfig", () => { mkdirSync(projectConfigDir, { recursive: true }) writeFileSync(legacyConfigPath, JSON.stringify({ agents: { oracle: { model: "openai/gpt-5.4" } } })) - spyOn(shared, "getOpenCodeConfigDir").mockReturnValue(userConfigDir) + process.env.OPENCODE_CONFIG_DIR = userConfigDir // when + const { loadPluginConfig } = await importFreshPluginConfigModule() loadPluginConfig(projectDir, {}) writeFileSync(backupConfigPath, JSON.stringify({ agents: { oracle: { model: "openai/gpt-5-nano" } } })) const reloadedConfig = loadPluginConfig(projectDir, {}) @@ -355,7 +361,7 @@ describe("loadPluginConfig", () => { expect(reloadedConfig.agents?.oracle?.model).toBe("openai/gpt-5.4") }) - it("should still load config from legacy path when migration fails", () => { + it("should still load config from legacy path when migration fails", async () => { // given - legacy config exists but canonical path is not writable const rootDir = mkdtempSync(join(tmpdir(), "omo-plugin-config-fail-")) const userConfigDir = join(rootDir, "user-config") @@ -374,12 +380,13 @@ describe("loadPluginConfig", () => { chmodSync(projectConfigDir, 0o555) } - spyOn(shared, "getOpenCodeConfigDir").mockReturnValue(userConfigDir) + process.env.OPENCODE_CONFIG_DIR = userConfigDir // when let config: OhMyOpenCodeConfig try { - config = loadPluginConfig(projectDir, {}) + const fresh = await importFreshPluginConfigModule() + config = fresh.loadPluginConfig(projectDir, {}) } finally { // Restore permissions for cleanup if (process.platform !== "win32") { @@ -391,7 +398,7 @@ describe("loadPluginConfig", () => { expect(config.agents?.oracle?.model).toBe("openai/gpt-5.4") }) - it("should load migrated legacy project config on the first load", () => { + it("should load migrated legacy project config on the first load", async () => { // given const rootDir = mkdtempSync(join(tmpdir(), "omo-plugin-config-first-load-")) const userConfigDir = join(rootDir, "user-config") @@ -405,9 +412,10 @@ describe("loadPluginConfig", () => { mkdirSync(projectConfigDir, { recursive: true }) writeFileSync(legacyConfigPath, JSON.stringify({ agents: { oracle: { model: "openai/gpt-5.4" } } })) - spyOn(shared, "getOpenCodeConfigDir").mockReturnValue(userConfigDir) + process.env.OPENCODE_CONFIG_DIR = userConfigDir // when + const { loadPluginConfig } = await importFreshPluginConfigModule() const config = loadPluginConfig(projectDir, {}) // then diff --git a/src/plugin/tool-registry.test.ts b/src/plugin/tool-registry.test.ts index 4c52b5b79..5c0a42bfb 100644 --- a/src/plugin/tool-registry.test.ts +++ b/src/plugin/tool-registry.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock, spyOn, test } from "bun:test" +import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test" import { tool } from "@opencode-ai/plugin" import type { OhMyOpenCodeConfig } from "../config" @@ -25,7 +25,12 @@ const syncSessionCreatedCallbacks: Array< ((event: { sessionID: string; parentID: string; title: string }) => Promise) | undefined > = [] -mock.module("../tools", () => ({ +const trackedPaneBySession = new Map() +let dispatchOpenClawEvent: ReturnType + +const { createToolRegistry, trimToolsToCap } = await import("./tool-registry") + +const toolFactories: NonNullable[0]["toolFactories"]> = { builtinTools: { bash: fakeTool, read: fakeTool }, createBackgroundTools: mock(() => ({})), createCallOmoAgent: mock(() => fakeTool), @@ -47,12 +52,7 @@ mock.module("../tools", () => ({ createTaskList: mock(() => fakeTool), createTaskUpdateTool: mock(() => fakeTool), createHashlineEditTool: mock(() => fakeTool), -})) - -const trackedPaneBySession = new Map() - -const { createToolRegistry, trimToolsToCap } = await import("./tool-registry") -const dispatchOpenClawEvent = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent") +} function createPluginConfig(overrides: Partial = {}): OhMyOpenCodeConfig { return { @@ -65,6 +65,11 @@ function createPluginConfig(overrides: Partial = {}): OhMyOp } } +beforeEach(() => { + dispatchOpenClawEvent = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent") + syncSessionCreatedCallbacks.length = 0 +}) + describe("#given tool trimming prioritization", () => { test("#when max_tools trims a hashline edit registration named edit #then edit is removed before higher-priority tools", () => { const filteredTools = { @@ -100,6 +105,7 @@ describe("#given task_system configuration", () => { disabledSkills: new Set(), }, availableCategories: [], + toolFactories, }) expect(result.taskSystemEnabled).toBe(false) @@ -129,6 +135,7 @@ describe("#given task_system configuration", () => { disabledSkills: new Set(), }, availableCategories: [], + toolFactories, }) expect(result.taskSystemEnabled).toBe(true) @@ -168,6 +175,7 @@ describe("#given tmux integration is disabled", () => { }, availableCategories: [], interactiveBashEnabled: true, + toolFactories, }) expect(result.filteredTools).toHaveProperty("interactive_bash") @@ -201,6 +209,7 @@ describe("#given tmux integration is disabled", () => { }, availableCategories: [], interactiveBashEnabled: false, + toolFactories, }) expect(result.filteredTools).not.toHaveProperty("interactive_bash") @@ -246,6 +255,7 @@ describe("#given openclaw is enabled for sync task sessions", () => { disabledSkills: new Set(), }, availableCategories: [], + toolFactories, }) const onSyncSessionCreated = syncSessionCreatedCallbacks[syncSessionCreatedCallbacks.length - 1] diff --git a/src/plugin/ultrawork-db-model-override.test.ts b/src/plugin/ultrawork-db-model-override.test.ts index dc3f15a38..ea5646d26 100644 --- a/src/plugin/ultrawork-db-model-override.test.ts +++ b/src/plugin/ultrawork-db-model-override.test.ts @@ -6,6 +6,12 @@ import { tmpdir } from "node:os" import * as dataPathModule from "../shared/data-path" import * as sharedModule from "../shared" +let scheduleDeferredModelOverride: (typeof import("./ultrawork-db-model-override"))["scheduleDeferredModelOverride"] + +async function importFreshUltraworkDbModelOverrideModule(): Promise { + return import(`./ultrawork-db-model-override?test=${Date.now()}-${Math.random()}`) +} + function flushMicrotasks(depth: number): Promise { return new Promise((resolve) => { let remaining = depth @@ -22,6 +28,11 @@ function flushWithTimeout(): Promise { return new Promise((resolve) => setTimeout(resolve, 10)) } +async function settleDeferredModelOverrideWork(): Promise { + await flushMicrotasks(12) + await flushWithTimeout() +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } @@ -32,7 +43,7 @@ describe("scheduleDeferredModelOverride", () => { let logSpy: ReturnType let getDataDirSpy: ReturnType - beforeEach(() => { + beforeEach(async () => { tempDir = mkdtempSync(join(tmpdir(), "ultrawork-db-test-")) const opencodePath = join(tempDir, "opencode") mkdirSync(opencodePath, { recursive: true }) @@ -50,11 +61,15 @@ describe("scheduleDeferredModelOverride", () => { `) db.close() - getDataDirSpy = spyOn(dataPathModule, "getDataDir").mockReturnValue(tempDir) - logSpy = spyOn(sharedModule, "log").mockImplementation(() => {}) + getDataDirSpy = spyOn(dataPathModule, "getDataDir") + getDataDirSpy.mockReturnValue(tempDir) + logSpy = spyOn(sharedModule, "log") + logSpy.mockImplementation(() => {}) + ;({ scheduleDeferredModelOverride } = await importFreshUltraworkDbModelOverrideModule()) }) - afterEach(() => { + afterEach(async () => { + await settleDeferredModelOverrideWork() getDataDirSpy?.mockRestore() logSpy?.mockRestore() rmSync(tempDir, { recursive: true, force: true }) @@ -95,7 +110,6 @@ describe("scheduleDeferredModelOverride", () => { insertMessage("msg_001", { providerID: "anthropic", modelID: "claude-sonnet-4-6" }) //#when - const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") scheduleDeferredModelOverride( "msg_001", { providerID: "anthropic", modelID: "claude-opus-4-6" }, @@ -112,7 +126,6 @@ describe("scheduleDeferredModelOverride", () => { insertMessage("msg_002", { providerID: "anthropic", modelID: "claude-sonnet-4-6" }) //#when - const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") scheduleDeferredModelOverride( "msg_002", { providerID: "anthropic", modelID: "claude-opus-4-6" }, @@ -129,7 +142,6 @@ describe("scheduleDeferredModelOverride", () => { //#given no message inserted //#when - const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") scheduleDeferredModelOverride( "msg_nonexistent", { providerID: "anthropic", modelID: "claude-opus-4-6" }, @@ -148,7 +160,6 @@ describe("scheduleDeferredModelOverride", () => { insertMessage("msg_003", { providerID: "anthropic", modelID: "claude-sonnet-4-6" }) //#when - const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") scheduleDeferredModelOverride( "msg_003", { providerID: "anthropic", modelID: "claude-opus-4-6" }, @@ -167,7 +178,6 @@ describe("scheduleDeferredModelOverride", () => { getDataDirSpy.mockReturnValue("/nonexistent/path/that/does/not/exist") //#when - const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") scheduleDeferredModelOverride( "msg_004", { providerID: "anthropic", modelID: "claude-opus-4-6" }, @@ -188,7 +198,6 @@ describe("scheduleDeferredModelOverride", () => { chmodSync(corruptedDbPath, 0o000) //#when - const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") scheduleDeferredModelOverride( "msg_corrupt", { providerID: "anthropic", modelID: "claude-opus-4-6" }, diff --git a/src/plugin/ultrawork-model-override.test.ts b/src/plugin/ultrawork-model-override.test.ts index 0204f0cae..feaf369c1 100644 --- a/src/plugin/ultrawork-model-override.test.ts +++ b/src/plugin/ultrawork-model-override.test.ts @@ -1,14 +1,29 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" -import { - applyUltraworkModelOverrideOnMessage, - resolveUltraworkOverride, - detectUltrawork, -} from "./ultrawork-model-override" import * as sharedModule from "../shared" import * as dbOverrideModule from "./ultrawork-db-model-override" import * as sessionStateModule from "../features/claude-code-session-state" +let resolveUltraworkOverride: (typeof import("./ultrawork-model-override"))["resolveUltraworkOverride"] +let detectUltrawork: (typeof import("./ultrawork-model-override"))["detectUltrawork"] +let applyUltraworkModelOverrideOnMessage: (typeof import("./ultrawork-model-override"))["applyUltraworkModelOverrideOnMessage"] + +async function importFreshUltraworkModelOverrideModule(): Promise { + return import(`./ultrawork-model-override?test=${Date.now()}-${Math.random()}`) +} + +async function loadFreshUltraworkModelOverrideModule(): Promise { + ;({ + resolveUltraworkOverride, + detectUltrawork, + applyUltraworkModelOverrideOnMessage, + } = await importFreshUltraworkModelOverrideModule()) +} + describe("detectUltrawork", () => { + beforeEach(async () => { + await loadFreshUltraworkModelOverrideModule() + }) + test("should detect ultrawork keyword", () => { expect(detectUltrawork("ultrawork do something")).toBe(true) }) @@ -41,6 +56,10 @@ describe("detectUltrawork", () => { }) describe("resolveUltraworkOverride", () => { + beforeEach(async () => { + await loadFreshUltraworkModelOverrideModule() + }) + function createOutput(text: string, agentName?: string) { return { message: { @@ -203,7 +222,8 @@ describe("resolveUltraworkOverride", () => { //#given const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6", variant: "max" }) const output = createOutput("ultrawork do something") - const getSessionAgentSpy = spyOn(sessionStateModule, "getSessionAgent").mockReturnValue("sisyphus") + const getSessionAgentSpy = spyOn(sessionStateModule, "getSessionAgent") + getSessionAgentSpy.mockReturnValue("sisyphus") //#when const result = resolveUltraworkOverride(config, undefined, output, "ses_test") @@ -220,9 +240,12 @@ describe("applyUltraworkModelOverrideOnMessage", () => { let logSpy: ReturnType let dbOverrideSpy: ReturnType - beforeEach(() => { - logSpy = spyOn(sharedModule, "log").mockImplementation(() => {}) - dbOverrideSpy = spyOn(dbOverrideModule, "scheduleDeferredModelOverride").mockImplementation(() => {}) + beforeEach(async () => { + logSpy = spyOn(sharedModule, "log") + logSpy.mockImplementation(() => {}) + dbOverrideSpy = spyOn(dbOverrideModule, "scheduleDeferredModelOverride") + dbOverrideSpy.mockImplementation(() => {}) + await loadFreshUltraworkModelOverrideModule() }) afterEach(() => {