From f610d19f9f5e64a13ceb669e96064a404621e6cc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 4 Apr 2026 20:00:49 +0900 Subject: [PATCH] test: isolate mock-contaminating tests into zauc-mocks-* directories Bun's `mock.module()` is process-global. Tests that use it permanently replace the mocked module for all subsequent tests in the same process. The `mock.restore()` only restores function mocks (spyOn), not module mocks. Move the 4 test files causing contamination into isolated subdirectories: - migrate-legacy-plugin-entry.test.ts (mock.module("node:fs")) - connected-providers-cache.test.ts (reads cache from filesystem) - legacy-plugin-warning.test.ts (reads config from filesystem) - subagent-resolver.test.ts (spies on shared modules) This forces bun to run each in a separate process, preventing the process-global mock contamination that's been causing 68 CI failures. --- .../connected-providers-cache.test.ts | 2 +- .../legacy-plugin-warning.test.ts | 0 .../migrate-legacy-plugin-entry.test.ts | 220 ++++++++++++++++++ .../subagent-resolver.test.ts | 6 +- 4 files changed, 224 insertions(+), 4 deletions(-) rename src/shared/{ => zauc-mocks-connected-providers}/connected-providers-cache.test.ts (98%) rename src/shared/{ => zauc-mocks-legacy-plugin}/legacy-plugin-warning.test.ts (100%) create mode 100644 src/shared/zauc-mocks-migrate-legacy-plugin/migrate-legacy-plugin-entry.test.ts rename src/tools/delegate-task/{ => zauc-mocks-subagent-resolver}/subagent-resolver.test.ts (99%) diff --git a/src/shared/connected-providers-cache.test.ts b/src/shared/zauc-mocks-connected-providers/connected-providers-cache.test.ts similarity index 98% rename from src/shared/connected-providers-cache.test.ts rename to src/shared/zauc-mocks-connected-providers/connected-providers-cache.test.ts index 85c7ab27b..b44267b7b 100644 --- a/src/shared/connected-providers-cache.test.ts +++ b/src/shared/zauc-mocks-connected-providers/connected-providers-cache.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "node:os" import { join } from "node:path" const { createConnectedProvidersCacheStore, findProviderModelMetadata } = await import( - new URL("./connected-providers-cache.ts?real-connected-providers-cache-test", import.meta.url).href + new URL("../connected-providers-cache.ts?real-connected-providers-cache-test", import.meta.url).href ) function createTestCacheContext() { diff --git a/src/shared/legacy-plugin-warning.test.ts b/src/shared/zauc-mocks-legacy-plugin/legacy-plugin-warning.test.ts similarity index 100% rename from src/shared/legacy-plugin-warning.test.ts rename to src/shared/zauc-mocks-legacy-plugin/legacy-plugin-warning.test.ts diff --git a/src/shared/zauc-mocks-migrate-legacy-plugin/migrate-legacy-plugin-entry.test.ts b/src/shared/zauc-mocks-migrate-legacy-plugin/migrate-legacy-plugin-entry.test.ts new file mode 100644 index 000000000..2d8b1404a --- /dev/null +++ b/src/shared/zauc-mocks-migrate-legacy-plugin/migrate-legacy-plugin-entry.test.ts @@ -0,0 +1,220 @@ +/// + +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +async function importFreshMigrationModule(): Promise { + return import(`../migrate-legacy-plugin-entry?test=${Date.now()}-${Math.random()}`) +} + +afterAll(() => { + mock.restore() +}) + +describe("migrateLegacyPluginEntry", () => { + let testDir = "" + + beforeEach(() => { + testDir = join(tmpdir(), `omo-migrate-entry-${Date.now()}-${Math.random().toString(36).slice(2)}`) + mkdirSync(testDir, { recursive: true }) + }) + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }) + }) + + describe("#given opencode.json contains oh-my-opencode plugin entry", () => { + describe("#when migrating the config", () => { + it("#then replaces oh-my-opencode with oh-my-openagent", async () => { + const configPath = join(testDir, "opencode.json") + writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-opencode@latest"] }, null, 2)) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(true) + const content = readFileSync(configPath, "utf-8") + expect(content).toContain("oh-my-openagent@latest") + expect(content).not.toContain("oh-my-opencode") + }) + }) + }) + + describe("#given opencode.json contains bare oh-my-opencode entry", () => { + describe("#when migrating the config", () => { + it("#then replaces with oh-my-openagent", async () => { + const configPath = join(testDir, "opencode.json") + writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-opencode"] }, null, 2)) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(true) + const content = readFileSync(configPath, "utf-8") + expect(content).toContain('"oh-my-openagent"') + expect(content).not.toContain("oh-my-opencode") + }) + }) + }) + + describe("#given renaming the temp file fails after writing the migrated config", () => { + describe("#when migrating the config", () => { + it("#then keeps the original config untouched and writes the migrated content to a sibling temp file", async () => { + const configPath = join(testDir, "opencode.json") + const originalContent = JSON.stringify({ plugin: ["oh-my-opencode@latest"] }, null, 2) + const tempPath = `${configPath}.tmp` + writeFileSync(configPath, originalContent) + + const fs = await import("node:fs") + const originalRenameSync = fs.renameSync + + mock.module("node:fs", () => ({ + ...fs, + renameSync: () => { + throw new Error("simulated rename failure") + }, + })) + + try { + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(false) + expect(readFileSync(configPath, "utf-8")).toBe(originalContent) + expect(readFileSync(tempPath, "utf-8")).toContain("oh-my-openagent@latest") + expect(readFileSync(tempPath, "utf-8")).not.toContain("oh-my-opencode") + } finally { + mock.module("node:fs", () => ({ + ...fs, + renameSync: originalRenameSync, + })) + } + }) + }) + }) + + describe("#given opencode.json contains pinned oh-my-opencode version", () => { + describe("#when migrating the config", () => { + it("#then preserves the version pin", async () => { + const configPath = join(testDir, "opencode.json") + writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-opencode@3.11.0"] }, null, 2)) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(true) + const content = readFileSync(configPath, "utf-8") + expect(content).toContain("oh-my-openagent@3.11.0") + }) + }) + }) + + describe("#given opencode.json already uses oh-my-openagent", () => { + describe("#when checking for migration", () => { + it("#then returns false and does not modify the file", async () => { + const configPath = join(testDir, "opencode.json") + const original = JSON.stringify({ plugin: ["oh-my-openagent@latest"] }, null, 2) + writeFileSync(configPath, original) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(false) + expect(readFileSync(configPath, "utf-8")).toBe(original) + }) + }) + }) + + describe("#given plugin entries contain both canonical and legacy values", () => { + describe("#when migrating the config", () => { + it("#then removes the legacy entry instead of duplicating the canonical one", async () => { + const configPath = join(testDir, "opencode.json") + writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent", "oh-my-opencode"] }, null, 2)) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(true) + const saved = JSON.parse(readFileSync(configPath, "utf-8")) as { plugin: string[] } + expect(saved.plugin).toEqual(["oh-my-openagent"]) + }) + }) + }) + + describe("#given unrelated strings contain the legacy package name", () => { + describe("#when migrating the config", () => { + it("#then rewrites only plugin entries and preserves unrelated fields", async () => { + const configPath = join(testDir, "opencode.json") + writeFileSync( + configPath, + JSON.stringify( + { + plugin: ["oh-my-opencode"], + notes: "keep oh-my-opencode in this text field", + paths: ["/tmp/oh-my-opencode/cache"], + }, + null, + 2, + ), + ) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(true) + const saved = JSON.parse(readFileSync(configPath, "utf-8")) as { + plugin: string[] + notes: string + paths: string[] + } + expect(saved.plugin).toEqual(["oh-my-openagent"]) + expect(saved.notes).toBe("keep oh-my-opencode in this text field") + expect(saved.paths).toEqual(["/tmp/oh-my-opencode/cache"]) + }) + }) + }) + + describe("#given opencode.jsonc contains a nested plugin key before the top-level plugin array", () => { + describe("#when migrating the config", () => { + it("#then rewrites only the top-level plugin array", async () => { + const configPath = join(testDir, "opencode.jsonc") + writeFileSync( + configPath, + `{ + "nested": { + "plugin": ["oh-my-opencode"] + }, + "plugin": ["oh-my-opencode@latest"] +} +`, + ) + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + + const result = migrateLegacyPluginEntry(configPath) + + expect(result).toBe(true) + const content = readFileSync(configPath, "utf-8") + expect(content).toContain(`"nested": { + "plugin": ["oh-my-opencode"] + }`) + expect(content).toContain(`"plugin": [ + "oh-my-openagent@latest" + ]`) + }) + }) + }) + + describe("#given config file does not exist", () => { + describe("#when attempting migration", () => { + it("#then returns false", async () => { + const { migrateLegacyPluginEntry } = await importFreshMigrationModule() + const result = migrateLegacyPluginEntry(join(testDir, "nonexistent.json")) + + expect(result).toBe(false) + }) + }) + }) +}) diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts similarity index 99% rename from src/tools/delegate-task/subagent-resolver.test.ts rename to src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index 7f87f8531..99974409b 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -1,8 +1,8 @@ declare const require: (name: string) => any const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test") -import { resolveSubagentExecution } from "./subagent-resolver" -import type { DelegateTaskArgs } from "./types" -import type { ExecutorContext } from "./executor-types" +import { resolveSubagentExecution } from "../subagent-resolver" +import type { DelegateTaskArgs } from "../types" +import type { ExecutorContext } from "../executor-types" import * as logger from "../../shared/logger" import * as connectedProvidersCache from "../../shared/connected-providers-cache"