diff --git a/test/setup.extensions.ts b/test/setup.extensions.ts new file mode 100644 index 00000000000..af739a38978 --- /dev/null +++ b/test/setup.extensions.ts @@ -0,0 +1,8 @@ +import { afterAll } from "vitest"; +import { installSharedTestSetup } from "./setup.shared.js"; + +const testEnv = installSharedTestSetup({ loadProfileEnv: false }); + +afterAll(() => { + testEnv.cleanup(); +}); diff --git a/test/setup.shared.ts b/test/setup.shared.ts new file mode 100644 index 00000000000..15a4857d22e --- /dev/null +++ b/test/setup.shared.ts @@ -0,0 +1,62 @@ +import { vi } from "vitest"; + +vi.mock("@mariozechner/pi-ai", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + getOAuthApiKey: () => undefined, + getOAuthProviders: () => [], + loginOpenAICodex: vi.fn(), + }; +}); + +vi.mock("@mariozechner/clipboard", () => ({ + availableFormats: () => [], + getText: async () => "", + setText: async () => {}, + hasText: () => false, + getImageBinary: async () => [], + getImageBase64: async () => "", + setImageBinary: async () => {}, + setImageBase64: async () => {}, + hasImage: () => false, + getHtml: async () => "", + setHtml: async () => {}, + hasHtml: () => false, + getRtf: async () => "", + setRtf: async () => {}, + hasRtf: () => false, + clear: async () => {}, + watch: () => {}, + callThreadsafeFunction: () => {}, +})); + +// Ensure Vitest environment is properly set. +process.env.VITEST = "true"; +// Config validation walks plugin manifests; keep an aggressive cache in tests to avoid +// repeated filesystem discovery across suites/workers. +process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS ??= "60000"; +// Vitest vm forks can load transitive lockfile helpers many times per worker. +// Raise listener budget to avoid noisy MaxListeners warnings and warning-stack overhead. +const TEST_PROCESS_MAX_LISTENERS = 128; +if (process.getMaxListeners() > 0 && process.getMaxListeners() < TEST_PROCESS_MAX_LISTENERS) { + process.setMaxListeners(TEST_PROCESS_MAX_LISTENERS); +} + +import { installProcessWarningFilter } from "../src/infra/warning-filter.js"; +import { withIsolatedTestHome } from "./test-env.js"; + +type SharedTestSetupOptions = { + loadProfileEnv?: boolean; +}; + +export function installSharedTestSetup(options?: SharedTestSetupOptions): { + cleanup: () => void; + tempHome: string; +} { + const testEnv = withIsolatedTestHome({ + loadProfileEnv: options?.loadProfileEnv, + }); + installProcessWarningFilter(); + return testEnv; +} diff --git a/test/setup.ts b/test/setup.ts index e37fd124a55..468bd4f8adb 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,48 +1,4 @@ -import { afterAll, afterEach, beforeAll, vi } from "vitest"; - -vi.mock("@mariozechner/pi-ai", async (importOriginal) => { - const original = await importOriginal(); - return { - ...original, - getOAuthApiKey: () => undefined, - getOAuthProviders: () => [], - loginOpenAICodex: vi.fn(), - }; -}); - -vi.mock("@mariozechner/clipboard", () => ({ - availableFormats: () => [], - getText: async () => "", - setText: async () => {}, - hasText: () => false, - getImageBinary: async () => [], - getImageBase64: async () => "", - setImageBinary: async () => {}, - setImageBase64: async () => {}, - hasImage: () => false, - getHtml: async () => "", - setHtml: async () => {}, - hasHtml: () => false, - getRtf: async () => "", - setRtf: async () => {}, - hasRtf: () => false, - clear: async () => {}, - watch: () => {}, - callThreadsafeFunction: () => {}, -})); - -// Ensure Vitest environment is properly set -process.env.VITEST = "true"; -// Config validation walks plugin manifests; keep an aggressive cache in tests to avoid -// repeated filesystem discovery across suites/workers. -process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS ??= "60000"; -// Vitest vm forks can load transitive lockfile helpers many times per worker. -// Raise listener budget to avoid noisy MaxListeners warnings and warning-stack overhead. -const TEST_PROCESS_MAX_LISTENERS = 128; -if (process.getMaxListeners() > 0 && process.getMaxListeners() < TEST_PROCESS_MAX_LISTENERS) { - process.setMaxListeners(TEST_PROCESS_MAX_LISTENERS); -} - +import { afterAll, afterEach, beforeAll } from "vitest"; import { resetContextWindowCacheForTest } from "../src/agents/context.js"; import { resetModelsJsonReadyCacheForTest } from "../src/agents/models-config.js"; import { @@ -57,16 +13,12 @@ import type { } from "../src/channels/plugins/types.js"; import type { OpenClawConfig } from "../src/config/config.js"; import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js"; -import { installProcessWarningFilter } from "../src/infra/warning-filter.js"; import type { PluginRegistry } from "../src/plugins/registry.js"; import { createTestRegistry } from "../src/test-utils/channel-plugins.js"; import { cleanupSessionStateForTest } from "../src/test-utils/session-state-cleanup.js"; -import { withIsolatedTestHome } from "./test-env.js"; +import { installSharedTestSetup } from "./setup.shared.js"; -// Set HOME/state isolation before importing any runtime OpenClaw modules. -const testEnv = withIsolatedTestHome(); - -installProcessWarningFilter(); +const testEnv = installSharedTestSetup({ loadProfileEnv: true }); const REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); diff --git a/test/test-env.ts b/test/test-env.ts index 030e07fcfc0..257d994491e 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -343,7 +343,10 @@ function stageLiveTestState(params: { restoreClaudeConfigFromBackupIfNeeded(params.tempHome); } -export function installTestEnv(): { cleanup: () => void; tempHome: string } { +export function installTestEnv(options?: { loadProfileEnv?: boolean }): { + cleanup: () => void; + tempHome: string; +} { const live = process.env.LIVE === "1" || process.env.OPENCLAW_LIVE_TEST === "1" || @@ -352,7 +355,9 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { const realHome = process.env.HOME ?? os.homedir(); const liveEnvSnapshot = { ...process.env }; - loadProfileEnv(realHome); + if (options?.loadProfileEnv ?? true) { + loadProfileEnv(realHome); + } if (live && allowRealHome) { return { cleanup: () => {}, tempHome: realHome }; @@ -368,6 +373,9 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { return testEnv; } -export function withIsolatedTestHome(): { cleanup: () => void; tempHome: string } { - return installTestEnv(); +export function withIsolatedTestHome(options?: { loadProfileEnv?: boolean }): { + cleanup: () => void; + tempHome: string; +} { + return installTestEnv(options); } diff --git a/vitest.config.ts b/vitest.config.ts index 91887901512..65294bbd128 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -53,6 +53,8 @@ export default defineConfig({ "package.json", "pnpm-lock.yaml", "test/setup.ts", + "test/setup.shared.ts", + "test/setup.extensions.ts", "scripts/test-parallel.mjs", "scripts/test-planner/catalog.mjs", "scripts/test-planner/executor.mjs", diff --git a/vitest.extensions.config.ts b/vitest.extensions.config.ts index a952a881e96..e3361914390 100644 --- a/vitest.extensions.config.ts +++ b/vitest.extensions.config.ts @@ -16,6 +16,7 @@ export function createExtensionsVitestConfig( dir: "extensions", env, passWithNoTests: true, + setupFiles: ["test/setup.extensions.ts"], // Most channel implementations stay on the channel surface, but a few // transport-only suites live better in the general extensions lane. exclude: extensionExcludedChannelTestGlobs, diff --git a/vitest.scoped-config.ts b/vitest.scoped-config.ts index cdd29730e3c..ba8ba0f323a 100644 --- a/vitest.scoped-config.ts +++ b/vitest.scoped-config.ts @@ -45,6 +45,7 @@ export function createScopedVitestConfig( exclude?: string[]; pool?: "threads" | "forks"; passWithNoTests?: boolean; + setupFiles?: string[]; }, ) { const base = baseConfig as unknown as Record; @@ -79,6 +80,7 @@ export function createScopedVitestConfig( ...(options?.passWithNoTests !== undefined ? { passWithNoTests: options.passWithNoTests } : {}), + ...(options?.setupFiles ? { setupFiles: options.setupFiles } : {}), }, }); }