From b1c98e8469c8e01074246ed18a1df190bcfc8d6d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 03:20:09 +0100 Subject: [PATCH] test: stabilize browser and provider ci shards --- .../browser/chrome.default-browser.test.ts | 8 +- .../client-fetch.loopback-auth.test.ts | 8 +- .../control-service.plugin-disabled.test.ts | 8 +- .../src/browser/profiles-service.test.ts | 13 +- ...re.clamps-timeoutms-scrollintoview.test.ts | 8 +- .../pw-tools-core.interactions.batch.test.ts | 8 +- ...s-core.interactions.evaluate.abort.test.ts | 8 +- ...-core.interactions.set-input-files.test.ts | 6 +- ...ls-core.last-file-chooser-arm-wins.test.ts | 8 +- ...-core.screenshots-element-selector.test.ts | 8 +- .../routes/agent.existing-session.test.ts | 11 +- .../routes/basic.existing-session.test.ts | 11 +- .../server-context.existing-session.test.ts | 11 +- ...server-context.hot-reload-profiles.test.ts | 18 +- ...xt.remote-profile-tab-ops.fallback.test.ts | 8 +- ....remote-profile-tab-ops.playwright.test.ts | 8 +- .../src/browser/server-lifecycle.test.ts | 11 +- .../server.control-server.test-harness.ts | 3 +- ...te-disabled-does-not-block-storage.test.ts | 11 +- .../imessage/src/channel.outbound.test.ts | 283 +----------------- .../ollama/src/web-search-provider.test.ts | 1 + .../provider-catalog.contract-test-support.ts | 75 +++-- .../event-handler.inbound-context.test.ts | 18 +- .../event-handler.mention-gating.test.ts | 25 +- .../slack/src/monitor.tool-result.test.ts | 26 +- .../slack/src/monitor/message-handler.test.ts | 8 +- .../prepare.thread-context-allowlist.test.ts | 32 +- .../prepare.thread-session-key.test.ts | 29 +- extensions/slack/src/monitor/slash.test.ts | 10 +- src/plugins/bundled-capability-runtime.ts | 17 +- src/plugins/bundled-plugin-metadata.test.ts | 40 +++ src/plugins/bundled-plugin-metadata.ts | 34 +++ src/plugins/contracts/registry.ts | 16 + .../contracts/speech-vitest-registry.ts | 98 +++++- 34 files changed, 345 insertions(+), 542 deletions(-) diff --git a/extensions/browser/src/browser/chrome.default-browser.test.ts b/extensions/browser/src/browser/chrome.default-browser.test.ts index ab4502d959c..c28bd486e41 100644 --- a/extensions/browser/src/browser/chrome.default-browser.test.ts +++ b/extensions/browser/src/browser/chrome.default-browser.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("node:child_process", async () => { const { mockNodeBuiltinModule } = await import("../../../../test/helpers/node-builtin-mocks.js"); @@ -31,11 +31,11 @@ vi.mock("node:os", async () => { import { execFileSync } from "node:child_process"; import * as fs from "node:fs"; import os from "node:os"; +const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js"); describe("browser default executable detection", () => { const launchServicesPlist = "com.apple.launchservices.secure.plist"; const chromeExecutablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; - let resolveBrowserExecutableForPlatform: typeof import("./chrome.executables.js").resolveBrowserExecutableForPlatform; function mockMacDefaultBrowser(bundleId: string, appPath = ""): void { vi.mocked(execFileSync).mockImplementation((cmd, args) => { @@ -63,10 +63,6 @@ describe("browser default executable detection", () => { }); } - beforeAll(async () => { - ({ resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js")); - }); - beforeEach(() => { vi.clearAllMocks(); vi.mocked(os.homedir).mockReturnValue("/Users/test"); diff --git a/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts b/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts index 3380b3aebb6..d7658eb0c8e 100644 --- a/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts +++ b/extensions/browser/src/browser/client-fetch.loopback-auth.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { BrowserDispatchResponse } from "./routes/dispatcher.js"; function okDispatchResponse(): BrowserDispatchResponse { @@ -49,7 +49,7 @@ vi.mock("./routes/dispatcher.js", () => ({ })), })); -let fetchBrowserJson: typeof import("./client-fetch.js").fetchBrowserJson; +const { fetchBrowserJson } = await import("./client-fetch.js"); function stubJsonFetchOk() { const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>( @@ -85,10 +85,6 @@ async function expectThrownBrowserFetchError( } describe("fetchBrowserJson loopback auth", () => { - beforeAll(async () => { - ({ fetchBrowserJson } = await import("./client-fetch.js")); - }); - beforeEach(() => { vi.restoreAllMocks(); vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "loopback-token"); diff --git a/extensions/browser/src/browser/control-service.plugin-disabled.test.ts b/extensions/browser/src/browser/control-service.plugin-disabled.test.ts index b757bc66028..d3b39143e3e 100644 --- a/extensions/browser/src/browser/control-service.plugin-disabled.test.ts +++ b/extensions/browser/src/browser/control-service.plugin-disabled.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ ensureBrowserControlAuth: vi.fn(async () => ({ generatedToken: false })), @@ -42,13 +42,9 @@ vi.mock("./runtime-lifecycle.js", () => ({ stopBrowserRuntime: vi.fn(async () => {}), })); -let startBrowserControlServiceFromConfig: typeof import("../control-service.js").startBrowserControlServiceFromConfig; +const { startBrowserControlServiceFromConfig } = await import("../control-service.js"); describe("startBrowserControlServiceFromConfig", () => { - beforeAll(async () => { - ({ startBrowserControlServiceFromConfig } = await import("../control-service.js")); - }); - beforeEach(() => { mocks.ensureBrowserControlAuth.mockClear(); mocks.createBrowserRuntimeState.mockClear(); diff --git a/extensions/browser/src/browser/profiles-service.test.ts b/extensions/browser/src/browser/profiles-service.test.ts index 0cd88916988..495b4064c0d 100644 --- a/extensions/browser/src/browser/profiles-service.test.ts +++ b/extensions/browser/src/browser/profiles-service.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { loadConfig, writeConfigFile } from "../config/config.js"; import { resolveOpenClawUserDataDir } from "./chrome.js"; import type { BrowserRouteContext, BrowserServerState } from "./server-context.js"; @@ -23,8 +23,10 @@ vi.mock("./chrome.js", () => ({ resolveOpenClawUserDataDir: vi.fn(() => "/tmp/openclaw-test/openclaw/user-data"), })); -let resolveBrowserConfig: typeof import("./config.js").resolveBrowserConfig; -let createBrowserProfilesService: typeof import("./profiles-service.js").createBrowserProfilesService; +const [{ resolveBrowserConfig }, { createBrowserProfilesService }] = await Promise.all([ + import("./config.js"), + import("./profiles-service.js"), +]); function createCtx(resolved: BrowserServerState["resolved"]) { const state: BrowserServerState = { @@ -57,11 +59,6 @@ async function createWorkProfileWithConfig(params: { } describe("BrowserProfilesService", () => { - beforeAll(async () => { - ({ resolveBrowserConfig } = await import("./config.js")); - ({ createBrowserProfilesService } = await import("./profiles-service.js")); - }); - beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts b/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts index c1978912eea..fa1e0c01e7d 100644 --- a/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { installPwToolsCoreTestHooks, setPwToolsCoreCurrentPage, @@ -6,13 +6,9 @@ import { } from "./pw-tools-core.test-harness.js"; installPwToolsCoreTestHooks(); -let mod: typeof import("./pw-tools-core.js"); +const mod = await import("./pw-tools-core.js"); describe("pw-tools-core", () => { - beforeAll(async () => { - mod = await import("./pw-tools-core.js"); - }); - it("clamps timeoutMs for scrollIntoView", async () => { const scrollIntoViewIfNeeded = vi.fn(async () => {}); setPwToolsCoreCurrentRefLocator({ scrollIntoViewIfNeeded }); diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.batch.test.ts b/extensions/browser/src/browser/pw-tools-core.interactions.batch.test.ts index 2801ebe8190..d722485b516 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.batch.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.batch.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; let page: { evaluate: ReturnType } | null = null; @@ -31,13 +31,9 @@ vi.mock("./pw-tools-core.snapshot.js", () => ({ resizeViewportViaPlaywright, })); -let batchViaPlaywright: typeof import("./pw-tools-core.interactions.js").batchViaPlaywright; +const { batchViaPlaywright } = await import("./pw-tools-core.interactions.js"); describe("batchViaPlaywright", () => { - beforeAll(async () => { - ({ batchViaPlaywright } = await import("./pw-tools-core.interactions.js")); - }); - beforeEach(() => { vi.clearAllMocks(); page = { diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.evaluate.abort.test.ts b/extensions/browser/src/browser/pw-tools-core.interactions.evaluate.abort.test.ts index 21c71e071e8..28eb9de5214 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.evaluate.abort.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.evaluate.abort.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; let page: { evaluate: ReturnType } | null = null; let locator: { evaluate: ReturnType } | null = null; @@ -29,7 +29,7 @@ vi.mock("./pw-session.js", () => { }; }); -let evaluateViaPlaywright: typeof import("./pw-tools-core.interactions.js").evaluateViaPlaywright; +const { evaluateViaPlaywright } = await import("./pw-tools-core.interactions.js"); function createPendingEval() { let evalCalled!: () => void; @@ -43,10 +43,6 @@ function createPendingEval() { } describe("evaluateViaPlaywright (abort)", () => { - beforeAll(async () => { - ({ evaluateViaPlaywright } = await import("./pw-tools-core.interactions.js")); - }); - beforeEach(() => { vi.clearAllMocks(); page = null; diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts b/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts index cfe2bc8673b..3991e24b4e3 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts @@ -39,7 +39,7 @@ vi.mock("./paths.js", () => { }; }); -let setInputFilesViaPlaywright: typeof import("./pw-tools-core.interactions.js").setInputFilesViaPlaywright; +const { setInputFilesViaPlaywright } = await import("./pw-tools-core.interactions.js"); function seedSingleLocatorPage(): { setInputFiles: ReturnType } { const setInputFiles = vi.fn(async () => {}); @@ -54,9 +54,7 @@ function seedSingleLocatorPage(): { setInputFiles: ReturnType } { } describe("setInputFilesViaPlaywright", () => { - beforeEach(async () => { - vi.resetModules(); - ({ setInputFilesViaPlaywright } = await import("./pw-tools-core.interactions.js")); + beforeEach(() => { vi.clearAllMocks(); page = null; locator = null; diff --git a/extensions/browser/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts b/extensions/browser/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts index c83091382c3..16264ba9eb3 100644 --- a/extensions/browser/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { DEFAULT_UPLOAD_DIR } from "./paths.js"; import { installPwToolsCoreTestHooks, @@ -9,13 +9,9 @@ import { } from "./pw-tools-core.test-harness.js"; installPwToolsCoreTestHooks(); -let mod: typeof import("./pw-tools-core.js"); +const mod = await import("./pw-tools-core.js"); describe("pw-tools-core", () => { - beforeAll(async () => { - mod = await import("./pw-tools-core.js"); - }); - it("last file-chooser arm wins", async () => { const firstPath = path.join(DEFAULT_UPLOAD_DIR, `vitest-arm-1-${crypto.randomUUID()}.txt`); const secondPath = path.join(DEFAULT_UPLOAD_DIR, `vitest-arm-2-${crypto.randomUUID()}.txt`); diff --git a/extensions/browser/src/browser/pw-tools-core.screenshots-element-selector.test.ts b/extensions/browser/src/browser/pw-tools-core.screenshots-element-selector.test.ts index 7c6e924f61e..5da747108a4 100644 --- a/extensions/browser/src/browser/pw-tools-core.screenshots-element-selector.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.screenshots-element-selector.test.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_UPLOAD_DIR } from "./paths.js"; import { getPwToolsCoreSessionMocks, @@ -12,7 +12,7 @@ import { installPwToolsCoreTestHooks(); const sessionMocks = getPwToolsCoreSessionMocks(); -let mod: typeof import("./pw-tools-core.js"); +const mod = await import("./pw-tools-core.js"); function createFileChooserPageMocks() { const fileChooser = { setFiles: vi.fn(async () => {}) }; @@ -26,10 +26,6 @@ function createFileChooserPageMocks() { } describe("pw-tools-core", () => { - beforeAll(async () => { - mod = await import("./pw-tools-core.js"); - }); - beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/browser/src/browser/routes/agent.existing-session.test.ts b/extensions/browser/src/browser/routes/agent.existing-session.test.ts index a2742423f14..a133fb20664 100644 --- a/extensions/browser/src/browser/routes/agent.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/agent.existing-session.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js"; import type { BrowserRequest } from "./types.js"; @@ -94,13 +94,8 @@ vi.mock("./agent.shared.js", () => ({ }), })); -let registerBrowserAgentActRoutes: typeof import("./agent.act.js").registerBrowserAgentActRoutes; -let registerBrowserAgentSnapshotRoutes: typeof import("./agent.snapshot.js").registerBrowserAgentSnapshotRoutes; - -beforeAll(async () => { - ({ registerBrowserAgentActRoutes } = await import("./agent.act.js")); - ({ registerBrowserAgentSnapshotRoutes } = await import("./agent.snapshot.js")); -}); +const { registerBrowserAgentActRoutes } = await import("./agent.act.js"); +const { registerBrowserAgentSnapshotRoutes } = await import("./agent.snapshot.js"); function getSnapshotGetHandler() { const { app, getHandlers } = createBrowserRouteApp(); diff --git a/extensions/browser/src/browser/routes/basic.existing-session.test.ts b/extensions/browser/src/browser/routes/basic.existing-session.test.ts index 32179aa5bd6..e61b0d6be8c 100644 --- a/extensions/browser/src/browser/routes/basic.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/basic.existing-session.test.ts @@ -1,12 +1,12 @@ -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js"; vi.mock("../chrome-mcp.js", () => ({ getChromeMcpPid: vi.fn(() => 4321), })); -let registerBrowserBasicRoutes: typeof import("./basic.js").registerBrowserBasicRoutes; -let BrowserProfileUnavailableError: typeof import("../errors.js").BrowserProfileUnavailableError; +const { BrowserProfileUnavailableError } = await import("../errors.js"); +const { registerBrowserBasicRoutes } = await import("./basic.js"); function createExistingSessionProfileState(params?: { isHttpReachable?: () => Promise }) { return { @@ -52,11 +52,6 @@ async function callBasicRouteWithState(params: { return response; } -beforeAll(async () => { - ({ BrowserProfileUnavailableError } = await import("../errors.js")); - ({ registerBrowserBasicRoutes } = await import("./basic.js")); -}); - describe("basic browser routes", () => { it("maps existing-session status failures to JSON browser errors", async () => { const response = await callBasicRouteWithState({ diff --git a/extensions/browser/src/browser/server-context.existing-session.test.ts b/extensions/browser/src/browser/server-context.existing-session.test.ts index e40db27ff0a..74c95c4815a 100644 --- a/extensions/browser/src/browser/server-context.existing-session.test.ts +++ b/extensions/browser/src/browser/server-context.existing-session.test.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { BrowserServerState } from "./server-context.js"; vi.mock("./chrome-mcp.js", () => ({ @@ -19,8 +19,8 @@ vi.mock("./chrome-mcp.js", () => ({ getChromeMcpPid: vi.fn(() => 4321), })); -let createBrowserRouteContext: typeof import("./server-context.js").createBrowserRouteContext; -let chromeMcp: typeof import("./chrome-mcp.js"); +const { createBrowserRouteContext } = await import("./server-context.js"); +const chromeMcp = await import("./chrome-mcp.js"); function makeState(): BrowserServerState { return { @@ -58,11 +58,6 @@ function makeState(): BrowserServerState { }; } -beforeAll(async () => { - ({ createBrowserRouteContext } = await import("./server-context.js")); - chromeMcp = await import("./chrome-mcp.js"); -}); - beforeEach(() => { vi.clearAllMocks(); }); diff --git a/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts b/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts index 15e74af5fb3..10c6fb892a0 100644 --- a/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts +++ b/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts @@ -61,19 +61,13 @@ vi.mock("./config-refresh-source.js", () => ({ loadBrowserConfigForRuntimeRefresh: () => buildConfig(), })); -describe("server-context hot-reload profiles", () => { - let loadConfig: typeof import("../config/config.js").loadConfig; - let resolveBrowserConfig: typeof import("./config.js").resolveBrowserConfig; - let resolveProfile: typeof import("./config.js").resolveProfile; - let refreshResolvedBrowserConfigFromDisk: typeof import("./resolved-config-refresh.js").refreshResolvedBrowserConfigFromDisk; - let resolveBrowserProfileWithHotReload: typeof import("./resolved-config-refresh.js").resolveBrowserProfileWithHotReload; +const { loadConfig } = await import("../config/config.js"); +const { resolveBrowserConfig, resolveProfile } = await import("./config.js"); +const { refreshResolvedBrowserConfigFromDisk, resolveBrowserProfileWithHotReload } = + await import("./resolved-config-refresh.js"); - beforeEach(async () => { - vi.resetModules(); - ({ loadConfig } = await import("../config/config.js")); - ({ resolveBrowserConfig, resolveProfile } = await import("./config.js")); - ({ refreshResolvedBrowserConfigFromDisk, resolveBrowserProfileWithHotReload } = - await import("./resolved-config-refresh.js")); +describe("server-context hot-reload profiles", () => { + beforeEach(() => { vi.clearAllMocks(); mockState.cfgProfiles = { openclaw: { cdpPort: 18800, color: "#FF4500" }, diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts index d1094b45b57..9a8e92a8fcf 100644 --- a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts @@ -1,14 +1,10 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loadRemoteProfileTestDeps, type RemoteProfileTestDeps, } from "./server-context.remote-profile-tab-ops.shared.js"; -let deps: RemoteProfileTestDeps; - -beforeAll(async () => { - deps = await loadRemoteProfileTestDeps(); -}); +const deps: RemoteProfileTestDeps = await loadRemoteProfileTestDeps(); beforeEach(() => { vi.clearAllMocks(); diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts index 98090648d7d..602918945c2 100644 --- a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts @@ -1,14 +1,10 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loadRemoteProfileTestDeps, type RemoteProfileTestDeps, } from "./server-context.remote-profile-tab-ops.shared.js"; -let deps: RemoteProfileTestDeps; - -beforeAll(async () => { - deps = await loadRemoteProfileTestDeps(); -}); +const deps: RemoteProfileTestDeps = await loadRemoteProfileTestDeps(); beforeEach(() => { vi.clearAllMocks(); diff --git a/extensions/browser/src/browser/server-lifecycle.test.ts b/extensions/browser/src/browser/server-lifecycle.test.ts index 593af698665..85b0500108f 100644 --- a/extensions/browser/src/browser/server-lifecycle.test.ts +++ b/extensions/browser/src/browser/server-lifecycle.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const { stopOpenClawChromeMock } = vi.hoisted(() => ({ stopOpenClawChromeMock: vi.fn(async () => {}), @@ -18,13 +18,8 @@ vi.mock("./server-context.js", () => ({ listKnownProfileNames: listKnownProfileNamesMock, })); -let ensureExtensionRelayForProfiles: typeof import("./server-lifecycle.js").ensureExtensionRelayForProfiles; -let stopKnownBrowserProfiles: typeof import("./server-lifecycle.js").stopKnownBrowserProfiles; - -beforeAll(async () => { - ({ ensureExtensionRelayForProfiles, stopKnownBrowserProfiles } = - await import("./server-lifecycle.js")); -}); +const { ensureExtensionRelayForProfiles, stopKnownBrowserProfiles } = + await import("./server-lifecycle.js"); beforeEach(() => { createBrowserRouteContextMock.mockClear(); diff --git a/extensions/browser/src/browser/server.control-server.test-harness.ts b/extensions/browser/src/browser/server.control-server.test-harness.ts index 9cb807bbcba..db9e5d91b11 100644 --- a/extensions/browser/src/browser/server.control-server.test-harness.ts +++ b/extensions/browser/src/browser/server.control-server.test-harness.ts @@ -400,6 +400,7 @@ export async function cleanupBrowserControlServerTestContext(): Promise { } export function installBrowserControlServerHooks() { + const hookTimeoutMs = process.platform === "win32" ? 300_000 : 240_000; beforeEach(async () => { vi.useRealTimers(); cdpMocks.createTargetViaCdp.mockImplementation(async () => { @@ -463,7 +464,7 @@ export function installBrowserControlServerHooks() { return makeResponse({}, { ok: false, status: 500, text: "unexpected" }); }), ); - }); + }, hookTimeoutMs); afterEach(async () => { await cleanupBrowserControlServerTestContext(); diff --git a/extensions/browser/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts b/extensions/browser/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts index 4e574defb78..b68905adf11 100644 --- a/extensions/browser/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts +++ b/extensions/browser/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getBrowserTestFetch } from "./test-fetch.js"; import { getFreePort } from "./test-port.js"; @@ -65,15 +65,10 @@ vi.mock("./server-context.js", async () => { }; }); -let startBrowserControlServerFromConfig: typeof import("./server.js").startBrowserControlServerFromConfig; -let stopBrowserControlServer: typeof import("./server.js").stopBrowserControlServer; +const { startBrowserControlServerFromConfig, stopBrowserControlServer } = + await import("./server.js"); describe("browser control evaluate gating", () => { - beforeAll(async () => { - ({ startBrowserControlServerFromConfig, stopBrowserControlServer } = - await import("./server.js")); - }); - beforeEach(async () => { testPort = await getFreePort(); prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; diff --git a/extensions/imessage/src/channel.outbound.test.ts b/extensions/imessage/src/channel.outbound.test.ts index 70bdd44bfcf..2c0ed5fc406 100644 --- a/extensions/imessage/src/channel.outbound.test.ts +++ b/extensions/imessage/src/channel.outbound.test.ts @@ -5,22 +5,6 @@ import type { IMessageRpcClient } from "./client.js"; import { imessageOutbound } from "./outbound-adapter.js"; import { sendMessageIMessage } from "./send.js"; -function requireIMessageSendText() { - const sendText = imessagePlugin.outbound?.sendText; - if (!sendText) { - throw new Error("imessage outbound.sendText unavailable"); - } - return sendText; -} - -function requireIMessageSendMedia() { - const sendMedia = imessagePlugin.outbound?.sendMedia; - if (!sendMedia) { - throw new Error("imessage outbound.sendMedia unavailable"); - } - return sendMedia; -} - function requireIMessageChunker() { const chunker = imessagePlugin.outbound?.chunker; if (!chunker) { @@ -63,136 +47,43 @@ function getSentParams() { return requestMock.mock.calls[0]?.[1] as Record; } -async function expectDirectOutboundResult(params: { - invoke: () => Promise<{ channel: string; messageId: string }>; - sendIMessage: ReturnType; - to: string; - text: string; - expectedOptions: Record; - expectedResult: { channel: string; messageId: string }; -}) { - const result = await params.invoke(); - expect(params.sendIMessage).toHaveBeenCalledWith( - params.to, - params.text, - expect.objectContaining(params.expectedOptions), - ); - expect(result).toEqual(params.expectedResult); -} - async function expectReplyToTextForwarding(params: { invoke: () => Promise<{ channel: string; messageId: string }>; sendIMessage: ReturnType; }) { - await expectDirectOutboundResult({ - invoke: params.invoke, - sendIMessage: params.sendIMessage, - to: "chat_id:12", - text: "hello", - expectedOptions: { + const result = await params.invoke(); + expect(params.sendIMessage).toHaveBeenCalledWith( + "chat_id:12", + "hello", + expect.objectContaining({ accountId: "default", replyToId: "reply-1", maxBytes: 3 * 1024 * 1024, - }, - expectedResult: { channel: "imessage", messageId: "m-text" }, - }); + }), + ); + expect(result).toEqual({ channel: "imessage", messageId: "m-text" }); } async function expectMediaLocalRootsForwarding(params: { invoke: () => Promise<{ channel: string; messageId: string }>; sendIMessage: ReturnType; }) { - await expectDirectOutboundResult({ - invoke: params.invoke, - sendIMessage: params.sendIMessage, - to: "chat_id:88", - text: "caption", - expectedOptions: { + const result = await params.invoke(); + expect(params.sendIMessage).toHaveBeenCalledWith( + "chat_id:88", + "caption", + expect.objectContaining({ mediaUrl: "/tmp/workspace/pic.png", mediaLocalRoots: ["/tmp/workspace"], accountId: "acct-1", replyToId: "reply-2", maxBytes: 3 * 1024 * 1024, - }, - expectedResult: { channel: "imessage", messageId: "m-media-local" }, - }); + }), + ); + expect(result).toEqual({ channel: "imessage", messageId: "m-media-local" }); } describe("imessagePlugin outbound", () => { - const cfg = { - channels: { - imessage: { - mediaMaxMb: 3, - }, - }, - }; - - it("forwards replyToId on direct sendText adapter path", async () => { - const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-text" }); - const sendText = requireIMessageSendText(); - - await expectReplyToTextForwarding({ - invoke: async () => - await sendText({ - cfg, - to: "chat_id:12", - text: "hello", - accountId: "default", - replyToId: "reply-1", - deps: { sendIMessage }, - }), - sendIMessage, - }); - }); - - it("forwards replyToId on direct sendMedia adapter path", async () => { - const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-media" }); - const sendMedia = requireIMessageSendMedia(); - - const result = await sendMedia({ - cfg, - to: "chat_id:77", - text: "caption", - mediaUrl: "https://example.com/pic.png", - accountId: "acct-1", - replyToId: "reply-2", - deps: { sendIMessage }, - }); - - expect(sendIMessage).toHaveBeenCalledWith( - "chat_id:77", - "caption", - expect.objectContaining({ - mediaUrl: "https://example.com/pic.png", - accountId: "acct-1", - replyToId: "reply-2", - maxBytes: 3 * 1024 * 1024, - }), - ); - expect(result).toEqual({ channel: "imessage", messageId: "m-media" }); - }); - - it("forwards mediaLocalRoots on direct sendMedia adapter path", async () => { - const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-media-local" }); - const sendMedia = requireIMessageSendMedia(); - const mediaLocalRoots = ["/tmp/workspace"]; - - await expectMediaLocalRootsForwarding({ - invoke: async () => - await sendMedia({ - cfg, - to: "chat_id:88", - text: "caption", - mediaUrl: "/tmp/workspace/pic.png", - mediaLocalRoots, - accountId: "acct-1", - replyToId: "reply-2", - deps: { sendIMessage }, - }), - sendIMessage, - }); - }); - it("chunks outbound text without requiring iMessage runtime initialization", () => { const chunker = requireIMessageChunker(); @@ -258,146 +149,4 @@ describe("sendMessageIMessage", () => { expect(params.chat_id).toBe(123); expect(params.text).toBe("hi"); }); - - it("applies sms service prefix", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("sms:+1555", "hello"); - const params = getSentParams(); - expect(params.service).toBe("sms"); - expect(params.to).toBe("+1555"); - }); - - it("adds file attachment with placeholder text", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:7", "", { - mediaUrl: "http://x/y.jpg", - resolveAttachmentImpl: async () => ({ - path: "/tmp/imessage-media.jpg", - contentType: "image/jpeg", - }), - }); - const params = getSentParams(); - expect(params.file).toBe("/tmp/imessage-media.jpg"); - expect(params.text).toBe(""); - }); - - it("normalizes mixed-case parameterized MIME for attachment placeholder text", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:7", "", { - mediaUrl: "http://x/voice", - resolveAttachmentImpl: async () => ({ - path: "/tmp/imessage-media.ogg", - contentType: " Audio/Ogg; codecs=opus ", - }), - }); - const params = getSentParams(); - expect(params.file).toBe("/tmp/imessage-media.ogg"); - expect(params.text).toBe(""); - }); - - it("returns message id when rpc provides one", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true, id: 123 }); - stopMock.mockClear().mockResolvedValue(undefined); - - const result = await sendWithDefaults("chat_id:7", "hello"); - expect(result.messageId).toBe("123"); - }); - - it("passes replyToId as separate reply_to param instead of embedding in text", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", "hello world", { - replyToId: "abc-123", - }); - const params = getSentParams(); - expect(params.text).toBe("hello world"); - expect(params.reply_to).toBe("abc-123"); - }); - - it("strips inline reply tags from text and passes replyToId as reply_to param", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", " [[reply_to:old-id]] hello", { - replyToId: "new-id", - }); - const params = getSentParams(); - expect(params.text).toBe("hello"); - expect(params.reply_to).toBe("new-id"); - }); - - it("sanitizes replyToId before passing as reply_to param", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", "hello", { - replyToId: " [ab]\n\u0000c\td ] ", - }); - const params = getSentParams(); - expect(params.text).toBe("hello"); - expect(params.reply_to).toBe("abcd"); - }); - - it("omits reply_to param when sanitized replyToId is empty", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", "hello", { - replyToId: "[]\u0000\n\r", - }); - const params = getSentParams(); - expect(params.text).toBe("hello"); - expect(params.reply_to).toBeUndefined(); - }); - - it("strips stray [[reply_to:...]] tags from text even without replyToId option", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", "[[reply_to:65]] Great question"); - const params = getSentParams(); - expect(params.text).toBe("Great question"); - expect(params.reply_to).toBeUndefined(); - }); - - it("strips [[audio_as_voice]] tags from outbound text", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", "hello [[audio_as_voice]] world"); - const params = getSentParams(); - expect(params.text).toBe("hello world"); - }); - - it("throws when text is only directive tags and no media", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await expect(sendWithDefaults("chat_id:123", "[[reply_to:65]]")).rejects.toThrow( - "iMessage send requires text or media", - ); - }); - - it("normalizes string message_id values from rpc result", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true, message_id: " guid-1 " }); - stopMock.mockClear().mockResolvedValue(undefined); - - const result = await sendWithDefaults("chat_id:7", "hello"); - expect(result.messageId).toBe("guid-1"); - }); - - it("does not stop an injected client", async () => { - requestMock.mockClear().mockResolvedValue({ ok: true }); - stopMock.mockClear().mockResolvedValue(undefined); - - await sendWithDefaults("chat_id:123", "hello"); - expect(stopMock).not.toHaveBeenCalled(); - }); }); diff --git a/extensions/ollama/src/web-search-provider.test.ts b/extensions/ollama/src/web-search-provider.test.ts index e805533add2..15242e8bd29 100644 --- a/extensions/ollama/src/web-search-provider.test.ts +++ b/extensions/ollama/src/web-search-provider.test.ts @@ -31,6 +31,7 @@ describe("ollama web search provider", () => { const webSearchProviders: unknown[] = []; plugin.register({ + registerMemoryEmbeddingProvider() {}, registerProvider() {}, registerWebSearchProvider(provider: unknown) { webSearchProviders.push(provider); diff --git a/extensions/openai/test-support/provider-catalog.contract-test-support.ts b/extensions/openai/test-support/provider-catalog.contract-test-support.ts index 81aaf16db81..77495b228f5 100644 --- a/extensions/openai/test-support/provider-catalog.contract-test-support.ts +++ b/extensions/openai/test-support/provider-catalog.contract-test-support.ts @@ -1,4 +1,4 @@ -import { beforeAll, beforeEach, describe, it, vi } from "vitest"; +import { beforeEach, describe, it, vi } from "vitest"; import { expectAugmentedCodexCatalog, expectCodexBuiltInSuppression, @@ -38,46 +38,42 @@ vi.mock("../../../src/plugins/providers.runtime.js", () => ({ })); export function describeOpenAIProviderCatalogContract() { - let augmentModelCatalogWithProviderPlugins: Awaited< - ReturnType - >["augmentModelCatalogWithProviderPlugins"]; - let resetProviderRuntimeHookCacheForTest: Awaited< - ReturnType - >["resetProviderRuntimeHookCacheForTest"]; - let resolveProviderBuiltInModelSuppression: Awaited< - ReturnType - >["resolveProviderBuiltInModelSuppression"]; - let openaiProviders: ProviderPlugin[]; - let openaiProvider: ProviderPlugin; + const contractDepsPromise = (async () => { + vi.resetModules(); + const openaiPlugin = loadBundledPluginPublicSurfaceSync<{ + default: Parameters[0]["plugin"]; + }>({ + pluginId: "openai", + artifactBasename: "index.js", + }); + const openaiProviders = ( + await registerProviderPlugin({ + plugin: openaiPlugin.default, + id: "openai", + name: "OpenAI", + }) + ).providers; + const openaiProvider = requireRegisteredProvider(openaiProviders, "openai", "provider"); + const { + augmentModelCatalogWithProviderPlugins, + resetProviderRuntimeHookCacheForTest, + resolveProviderBuiltInModelSuppression, + } = await importProviderRuntimeCatalogModule(); + return { + augmentModelCatalogWithProviderPlugins, + resetProviderRuntimeHookCacheForTest, + resolveProviderBuiltInModelSuppression, + openaiProviders, + openaiProvider, + }; + })(); describe( "openai provider catalog contract", { timeout: PROVIDER_CATALOG_CONTRACT_TIMEOUT_MS }, () => { - beforeAll(async () => { - vi.resetModules(); - const openaiPlugin = loadBundledPluginPublicSurfaceSync<{ - default: Parameters[0]["plugin"]; - }>({ - pluginId: "openai", - artifactBasename: "index.js", - }); - openaiProviders = ( - await registerProviderPlugin({ - plugin: openaiPlugin.default, - id: "openai", - name: "OpenAI", - }) - ).providers; - openaiProvider = requireRegisteredProvider(openaiProviders, "openai", "provider"); - ({ - augmentModelCatalogWithProviderPlugins, - resetProviderRuntimeHookCacheForTest, - resolveProviderBuiltInModelSuppression, - } = await importProviderRuntimeCatalogModule()); - }); - - beforeEach(() => { + beforeEach(async () => { + const { resetProviderRuntimeHookCacheForTest, openaiProviders } = await contractDepsPromise; resetProviderRuntimeHookCacheForTest(); resolvePluginProvidersMock.mockReset(); @@ -105,17 +101,20 @@ export function describeOpenAIProviderCatalogContract() { resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]); }); - it("keeps codex-only missing-auth hints wired through the provider runtime", () => { + it("keeps codex-only missing-auth hints wired through the provider runtime", async () => { + const { openaiProvider } = await contractDepsPromise; expectCodexMissingAuthHint( (params) => openaiProvider.buildMissingAuthMessage?.(params.context) ?? undefined, ); }); - it("keeps built-in model suppression wired through the provider runtime", () => { + it("keeps built-in model suppression wired through the provider runtime", async () => { + const { resolveProviderBuiltInModelSuppression } = await contractDepsPromise; expectCodexBuiltInSuppression(resolveProviderBuiltInModelSuppression); }); it("keeps bundled model augmentation wired through the provider runtime", async () => { + const { augmentModelCatalogWithProviderPlugins } = await contractDepsPromise; await expectAugmentedCodexCatalog(augmentModelCatalogWithProviderPlugins); }); }, diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index 006172a8447..4afef4670a8 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -1,8 +1,11 @@ import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/test-helpers.js"; -let createBaseSignalEventHandlerDeps: typeof import("./event-handler.test-harness.js").createBaseSignalEventHandlerDeps; -let createSignalReceiveEvent: typeof import("./event-handler.test-harness.js").createSignalReceiveEvent; +vi.useRealTimers(); +const [ + { createBaseSignalEventHandlerDeps, createSignalReceiveEvent }, + { createSignalEventHandler }, +] = await Promise.all([import("./event-handler.test-harness.js"), import("./event-handler.js")]); const { sendTypingMock, sendReadReceiptMock, dispatchInboundMessageMock, capture } = vi.hoisted( () => { @@ -48,16 +51,7 @@ vi.mock("../../../../src/pairing/pairing-store.js", () => ({ upsertChannelPairingRequest: vi.fn(), })); -let createSignalEventHandler: typeof import("./event-handler.js").createSignalEventHandler; - describe("signal createSignalEventHandler inbound context", () => { - beforeAll(async () => { - vi.useRealTimers(); - ({ createBaseSignalEventHandlerDeps, createSignalReceiveEvent } = - await import("./event-handler.test-harness.js")); - ({ createSignalEventHandler } = await import("./event-handler.js")); - }); - beforeEach(() => { capture.ctx = undefined; sendTypingMock.mockReset().mockResolvedValue(true); diff --git a/extensions/signal/src/monitor/event-handler.mention-gating.test.ts b/extensions/signal/src/monitor/event-handler.mention-gating.test.ts index cedb833396e..b597b7c3c95 100644 --- a/extensions/signal/src/monitor/event-handler.mention-gating.test.ts +++ b/extensions/signal/src/monitor/event-handler.mention-gating.test.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildDispatchInboundCaptureMock } from "../../../../src/channels/plugins/contracts/inbound-testkit.js"; type SignalMsgContext = Pick & { @@ -23,14 +23,15 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async () => { }); }); -let createBaseSignalEventHandlerDeps: typeof import("./event-handler.test-harness.js").createBaseSignalEventHandlerDeps; -let createSignalReceiveEvent: typeof import("./event-handler.test-harness.js").createSignalReceiveEvent; -let createSignalEventHandler: typeof import("./event-handler.js").createSignalEventHandler; -let renderSignalMentions: typeof import("./mentions.js").renderSignalMentions; - -beforeAll(async () => { - ({ renderSignalMentions } = await import("./mentions.js")); -}); +const [ + { createBaseSignalEventHandlerDeps, createSignalReceiveEvent }, + { createSignalEventHandler }, + { renderSignalMentions }, +] = await Promise.all([ + import("./event-handler.test-harness.js"), + import("./event-handler.js"), + import("./mentions.js"), +]); type GroupEventOpts = { message?: string; @@ -106,12 +107,6 @@ async function expectSkippedGroupHistory(opts: GroupEventOpts, expectedBody: str } describe("signal mention gating", () => { - beforeAll(async () => { - ({ createBaseSignalEventHandlerDeps, createSignalReceiveEvent } = - await import("./event-handler.test-harness.js")); - ({ createSignalEventHandler } = await import("./event-handler.js")); - }); - beforeEach(() => { capturedCtx = undefined; }); diff --git a/extensions/slack/src/monitor.tool-result.test.ts b/extensions/slack/src/monitor.tool-result.test.ts index cb1143e4841..c8ce7611612 100644 --- a/extensions/slack/src/monitor.tool-result.test.ts +++ b/extensions/slack/src/monitor.tool-result.test.ts @@ -1,11 +1,11 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { expectPairingReplyText } from "../../../test/helpers/pairing-reply.js"; import { defaultSlackTestConfig, getSlackTestState, + getSlackHandlerOrThrow, getSlackClient, getSlackHandlers, - getSlackHandlerOrThrow, flush, resetSlackTestState, runSlackMessageOnce, @@ -13,21 +13,21 @@ import { stopSlackMonitor, } from "./monitor.test-helpers.js"; -let resetInboundDedupe: typeof import("openclaw/plugin-sdk/reply-runtime").resetInboundDedupe; -let HISTORY_CONTEXT_MARKER: typeof import("../../../src/auto-reply/reply/history.js").HISTORY_CONTEXT_MARKER; -let CURRENT_MESSAGE_MARKER: typeof import("../../../src/auto-reply/reply/mentions.js").CURRENT_MESSAGE_MARKER; -let monitorSlackProvider: typeof import("./monitor.js").monitorSlackProvider; +const [ + { resetInboundDedupe }, + { HISTORY_CONTEXT_MARKER }, + { CURRENT_MESSAGE_MARKER }, + { monitorSlackProvider }, +] = await Promise.all([ + import("openclaw/plugin-sdk/reply-runtime"), + import("../../../src/auto-reply/reply/history.js"), + import("../../../src/auto-reply/reply/mentions.js"), + import("./monitor/provider.js"), +]); const slackTestState = getSlackTestState(); const { sendMock, replyMock, reactMock, upsertPairingRequestMock } = slackTestState; -beforeAll(async () => { - ({ resetInboundDedupe } = await import("openclaw/plugin-sdk/reply-runtime")); - ({ HISTORY_CONTEXT_MARKER } = await import("../../../src/auto-reply/reply/history.js")); - ({ CURRENT_MESSAGE_MARKER } = await import("../../../src/auto-reply/reply/mentions.js")); - ({ monitorSlackProvider } = await import("./monitor.js")); -}); - beforeEach(() => { resetInboundDedupe(); resetSlackTestState(defaultSlackTestConfig()); diff --git a/extensions/slack/src/monitor/message-handler.test.ts b/extensions/slack/src/monitor/message-handler.test.ts index 585ed026b18..778f29a5586 100644 --- a/extensions/slack/src/monitor/message-handler.test.ts +++ b/extensions/slack/src/monitor/message-handler.test.ts @@ -1,11 +1,11 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const enqueueMock = vi.fn(async (_entry: unknown) => {}); const flushKeyMock = vi.fn(async (_key: string) => {}); const resolveThreadTsMock = vi.fn(async ({ message }: { message: Record }) => ({ ...message, })); -let createSlackMessageHandler: typeof import("./message-handler.js").createSlackMessageHandler; +const { createSlackMessageHandler } = await import("./message-handler.js"); vi.mock("openclaw/plugin-sdk/channel-inbound", async () => { const actual = await vi.importActual( @@ -72,10 +72,6 @@ async function handleDirectMessage( } describe("createSlackMessageHandler", () => { - beforeAll(async () => { - ({ createSlackMessageHandler } = await import("./message-handler.js")); - }); - beforeEach(() => { enqueueMock.mockClear(); flushKeyMock.mockClear(); diff --git a/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts b/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts index 5eb3976f8ab..d3a9ce52fd7 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts @@ -3,30 +3,17 @@ import os from "node:os"; import path from "node:path"; import type { App } from "@slack/bolt"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; import type { SlackMessageEvent } from "../../types.js"; -type PrepareSlackMessage = typeof import("./prepare.js").prepareSlackMessage; -type CreateInboundSlackTestContext = - typeof import("./prepare.test-helpers.js").createInboundSlackTestContext; -type CreateSlackTestAccount = typeof import("./prepare.test-helpers.js").createSlackTestAccount; - -let prepareSlackMessage: PrepareSlackMessage; -let createInboundSlackTestContext: CreateInboundSlackTestContext; -let createSlackTestAccount: CreateSlackTestAccount; -let fixtureRoot = ""; +const [{ prepareSlackMessage }, helpers] = await Promise.all([ + import("./prepare.js"), + import("./prepare.test-helpers.js"), +]); +const { createInboundSlackTestContext, createSlackTestAccount } = helpers; +let fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-room-thread-context-")); let caseId = 0; -async function loadSlackPrepareModules() { - const [{ prepareSlackMessage: loadedPrepareSlackMessage }, helpers] = await Promise.all([ - import("./prepare.js"), - import("./prepare.test-helpers.js"), - ]); - prepareSlackMessage = loadedPrepareSlackMessage; - createInboundSlackTestContext = helpers.createInboundSlackTestContext; - createSlackTestAccount = helpers.createSlackTestAccount; -} - function makeTmpStorePath() { if (!fixtureRoot) { throw new Error("fixtureRoot missing"); @@ -37,11 +24,6 @@ function makeTmpStorePath() { } describe("prepareSlackMessage thread context allowlists", () => { - beforeAll(async () => { - await loadSlackPrepareModules(); - fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-room-thread-context-")); - }); - afterAll(() => { if (fixtureRoot) { fs.rmSync(fixtureRoot, { recursive: true, force: true }); diff --git a/extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts b/extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts index 2945efa5b5b..6b4a6c40b4b 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts @@ -1,26 +1,13 @@ import type { App } from "@slack/bolt"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SlackMessageEvent } from "../../types.js"; -type PrepareSlackMessage = typeof import("./prepare.js").prepareSlackMessage; -type CreateInboundSlackTestContext = - typeof import("./prepare.test-helpers.js").createInboundSlackTestContext; -type CreateSlackTestAccount = typeof import("./prepare.test-helpers.js").createSlackTestAccount; - -let prepareSlackMessage: PrepareSlackMessage; -let createInboundSlackTestContext: CreateInboundSlackTestContext; -let createSlackTestAccount: CreateSlackTestAccount; - -async function loadSlackPrepareModules() { - const [{ prepareSlackMessage: loadedPrepareSlackMessage }, helpers] = await Promise.all([ - import("./prepare.js"), - import("./prepare.test-helpers.js"), - ]); - prepareSlackMessage = loadedPrepareSlackMessage; - createInboundSlackTestContext = helpers.createInboundSlackTestContext; - createSlackTestAccount = helpers.createSlackTestAccount; -} +const [{ prepareSlackMessage }, helpers] = await Promise.all([ + import("./prepare.js"), + import("./prepare.test-helpers.js"), +]); +const { createInboundSlackTestContext, createSlackTestAccount } = helpers; function buildCtx(overrides?: { replyToMode?: "all" | "first" | "off" }) { const replyToMode = overrides?.replyToMode ?? "all"; @@ -48,10 +35,6 @@ function buildChannelMessage(overrides?: Partial): SlackMessa } describe("thread-level session keys", () => { - beforeAll(async () => { - await loadSlackPrepareModules(); - }); - it("keeps top-level channel turns in one session when replyToMode=off", async () => { const ctx = buildCtx({ replyToMode: "off" }); ctx.resolveUserName = async () => ({ name: "Alice" }); diff --git a/extensions/slack/src/monitor/slash.test.ts b/extensions/slack/src/monitor/slash.test.ts index 46ecab6f94d..0b1a9c3e770 100644 --- a/extensions/slack/src/monitor/slash.test.ts +++ b/extensions/slack/src/monitor/slash.test.ts @@ -180,16 +180,12 @@ vi.mock("./slash-commands.runtime.js", () => { }); type RegisterFn = (params: { ctx: unknown; account: unknown }) => Promise; -let registerSlackMonitorSlashCommands: RegisterFn; +const { registerSlackMonitorSlashCommands } = (await import("./slash.js")) as { + registerSlackMonitorSlashCommands: RegisterFn; +}; const { dispatchMock } = getSlackSlashMocks(); -beforeAll(async () => { - ({ registerSlackMonitorSlashCommands } = (await import("./slash.js")) as { - registerSlackMonitorSlashCommands: RegisterFn; - }); -}); - beforeEach(() => { resetSlackSlashMocks(); }); diff --git a/src/plugins/bundled-capability-runtime.ts b/src/plugins/bundled-capability-runtime.ts index 59be1cea448..9b7d5d73775 100644 --- a/src/plugins/bundled-capability-runtime.ts +++ b/src/plugins/bundled-capability-runtime.ts @@ -7,6 +7,7 @@ import { withBundledPluginEnablementCompat, withBundledPluginVitestCompat, } from "./bundled-compat.js"; +import { resolveBundledPluginRepoEntryPath } from "./bundled-plugin-metadata.js"; import { createCapturedPluginRegistration } from "./captured-registration.js"; import { discoverOpenClawPlugins } from "./discovery.js"; import type { PluginLoadOptions } from "./loader.js"; @@ -213,6 +214,7 @@ export function loadBundledCapabilityRuntimeRegistry(params: { manifestRegistry.plugins.map((record) => [record.rootDir, record]), ); const seenPluginIds = new Set(); + const repoRoot = process.cwd(); for (const candidate of discovery.candidates) { const manifest = manifestByRoot.get(candidate.rootDir); @@ -229,15 +231,22 @@ export function loadBundledCapabilityRuntimeRegistry(params: { name: manifest.name, description: manifest.description, version: manifest.version, - source: candidate.source, + source: + env?.VITEST && params.pluginSdkResolution === "dist" + ? (resolveBundledPluginRepoEntryPath({ + rootDir: repoRoot, + pluginId: manifest.id, + preferBuilt: true, + }) ?? candidate.source) + : candidate.source, rootDir: candidate.rootDir, workspaceDir: candidate.workspaceDir, }); const opened = openBoundaryFileSync({ - absolutePath: candidate.source, - rootPath: candidate.rootDir, - boundaryLabel: "plugin root", + absolutePath: record.source, + rootPath: record.source === candidate.source ? candidate.rootDir : repoRoot, + boundaryLabel: record.source === candidate.source ? "plugin root" : "repo root", rejectHardlinks: false, skipLexicalRootCheck: true, }); diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index e19746f040b..3a93f293059 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -5,6 +5,7 @@ import { clearBundledPluginMetadataCache, listBundledPluginMetadata, resolveBundledPluginGeneratedPath, + resolveBundledPluginRepoEntryPath, } from "./bundled-plugin-metadata.js"; import { createGeneratedPluginTempRoot, @@ -175,6 +176,45 @@ describe("bundled plugin metadata", () => { expectGeneratedPathResolution(tempRoot, path.join("plugin", "index.js")); }); + it("resolves bundled repo entry paths from dist before workspace source", () => { + const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-repo-entry-"); + const pluginRoot = path.join(tempRoot, "extensions", "alpha"); + const distPluginRoot = path.join(tempRoot, "dist", "extensions", "alpha"); + + writeJson(path.join(pluginRoot, "package.json"), { + name: "@openclaw/alpha", + version: "0.0.1", + openclaw: { + extensions: ["./index.ts"], + }, + }); + writeJson(path.join(pluginRoot, "openclaw.plugin.json"), { + id: "alpha", + configSchema: { type: "object" }, + }); + fs.writeFileSync(path.join(pluginRoot, "index.ts"), "export const source = true;\n", "utf8"); + + expect( + resolveBundledPluginRepoEntryPath({ + rootDir: tempRoot, + pluginId: "alpha", + preferBuilt: true, + }), + ).toBe(path.join(pluginRoot, "index.ts")); + + fs.mkdirSync(distPluginRoot, { recursive: true }); + fs.writeFileSync(path.join(distPluginRoot, "index.js"), "export const built = true;\n", "utf8"); + + clearBundledPluginMetadataCache(); + expect( + resolveBundledPluginRepoEntryPath({ + rootDir: tempRoot, + pluginId: "alpha", + preferBuilt: true, + }), + ).toBe(path.join(distPluginRoot, "index.js")); + }); + it("merges runtime channel schema metadata with manifest-owned channel config fields", () => { const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-channel-configs-"); diff --git a/src/plugins/bundled-plugin-metadata.ts b/src/plugins/bundled-plugin-metadata.ts index 068875ec402..90651b5e5f2 100644 --- a/src/plugins/bundled-plugin-metadata.ts +++ b/src/plugins/bundled-plugin-metadata.ts @@ -243,3 +243,37 @@ export function resolveBundledPluginGeneratedPath( } return null; } + +function normalizeRelativePluginEntryPath(entryPath: string): string { + return entryPath.replace(/^\.\//u, ""); +} + +export function resolveBundledPluginRepoEntryPath(params: { + rootDir: string; + pluginId: string; + preferBuilt?: boolean; +}): string | null { + const metadata = findBundledPluginMetadataById(params.pluginId, { rootDir: params.rootDir }); + if (!metadata) { + return null; + } + + const entryOrder = params.preferBuilt + ? [metadata.source.built, metadata.source.source] + : [metadata.source.source, metadata.source.built]; + const baseDirs = [ + path.resolve(params.rootDir, "dist", "extensions", metadata.dirName), + path.resolve(params.rootDir, "extensions", metadata.dirName), + ]; + + for (const baseDir of baseDirs) { + for (const entryPath of entryOrder) { + const candidate = path.resolve(baseDir, normalizeRelativePluginEntryPath(entryPath)); + if (fs.existsSync(candidate)) { + return candidate; + } + } + } + + return null; +} diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index 9002ef939de..f0f66d955f7 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -15,6 +15,7 @@ import type { WebFetchProviderPlugin, WebSearchProviderPlugin, } from "../types.js"; +import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./inventory/bundled-capability-metadata.js"; import { loadVitestImageGenerationProviderContractRegistry, loadVitestMediaUnderstandingProviderContractRegistry, @@ -91,6 +92,21 @@ function uniqueStrings(values: readonly string[]): string[] { } function resolveBundledManifestContracts(): PluginRegistrationContractEntry[] { + if (process.env.VITEST) { + return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.map((entry) => ({ + pluginId: entry.pluginId, + providerIds: [...entry.providerIds], + speechProviderIds: [...entry.speechProviderIds], + realtimeTranscriptionProviderIds: [...entry.realtimeTranscriptionProviderIds], + realtimeVoiceProviderIds: [...entry.realtimeVoiceProviderIds], + mediaUnderstandingProviderIds: [...entry.mediaUnderstandingProviderIds], + imageGenerationProviderIds: [...entry.imageGenerationProviderIds], + videoGenerationProviderIds: [...entry.videoGenerationProviderIds], + webFetchProviderIds: [...entry.webFetchProviderIds], + webSearchProviderIds: [...entry.webSearchProviderIds], + toolNames: [...entry.toolNames], + })); + } return loadPluginManifestRegistry({}) .plugins.filter( (plugin) => diff --git a/src/plugins/contracts/speech-vitest-registry.ts b/src/plugins/contracts/speech-vitest-registry.ts index e4867206711..83f931e2636 100644 --- a/src/plugins/contracts/speech-vitest-registry.ts +++ b/src/plugins/contracts/speech-vitest-registry.ts @@ -1,5 +1,8 @@ +import { createJiti } from "jiti"; import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js"; -import { resolveManifestContractPluginIds } from "../manifest-registry.js"; +import { resolveBundledPluginRepoEntryPath } from "../bundled-plugin-metadata.js"; +import { createCapturedPluginRegistration } from "../captured-registration.js"; +import type { OpenClawPluginDefinition } from "../types.js"; import type { ImageGenerationProviderPlugin, MediaUnderstandingProviderPlugin, @@ -9,6 +12,7 @@ import type { SpeechProviderPlugin, VideoGenerationProviderPlugin, } from "../types.js"; +import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./inventory/bundled-capability-metadata.js"; export type SpeechProviderContractEntry = { pluginId: string; @@ -54,6 +58,67 @@ type ManifestContractKey = | "videoGenerationProviders" | "musicGenerationProviders"; +const VITEST_CONTRACT_PLUGIN_IDS = { + imageGenerationProviders: BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter( + (entry) => entry.imageGenerationProviderIds.length > 0, + ).map((entry) => entry.pluginId), + speechProviders: BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter( + (entry) => entry.speechProviderIds.length > 0, + ).map((entry) => entry.pluginId), + mediaUnderstandingProviders: BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter( + (entry) => entry.mediaUnderstandingProviderIds.length > 0, + ).map((entry) => entry.pluginId), + realtimeVoiceProviders: BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter( + (entry) => entry.realtimeVoiceProviderIds.length > 0, + ).map((entry) => entry.pluginId), + realtimeTranscriptionProviders: BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter( + (entry) => entry.realtimeTranscriptionProviderIds.length > 0, + ).map((entry) => entry.pluginId), + videoGenerationProviders: BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter( + (entry) => entry.videoGenerationProviderIds.length > 0, + ).map((entry) => entry.pluginId), +} satisfies Record; + +function loadVitestVideoGenerationFallbackEntries( + pluginIds: readonly string[], +): VideoGenerationProviderContractEntry[] { + const jiti = createJiti(import.meta.url, { + interopDefault: true, + moduleCache: false, + fsCache: false, + }); + const repoRoot = process.cwd(); + return pluginIds.flatMap((pluginId) => { + const modulePath = resolveBundledPluginRepoEntryPath({ + rootDir: repoRoot, + pluginId, + preferBuilt: true, + }); + if (!modulePath) { + return []; + } + try { + const mod = jiti(modulePath) as + | OpenClawPluginDefinition + | { default?: OpenClawPluginDefinition }; + const plugin = + (mod as { default?: OpenClawPluginDefinition }).default ?? + (mod as OpenClawPluginDefinition); + if (typeof plugin?.register !== "function") { + return []; + } + const captured = createCapturedPluginRegistration(); + void plugin.register(captured.api); + return captured.videoGenerationProviders.map((provider) => ({ + pluginId, + provider, + })); + } catch { + return []; + } + }); +} + function loadVitestCapabilityContractEntries(params: { contract: ManifestContractKey; pickEntries: (registry: ReturnType) => Array<{ @@ -61,19 +126,30 @@ function loadVitestCapabilityContractEntries(params: { provider: T; }>; }): Array<{ pluginId: string; provider: T }> { - const pluginIds = resolveManifestContractPluginIds({ - contract: params.contract, - origin: "bundled", - }); + const pluginIds = VITEST_CONTRACT_PLUGIN_IDS[params.contract]; if (pluginIds.length === 0) { return []; } - return params.pickEntries( + const bulkEntries = params.pickEntries( loadBundledCapabilityRuntimeRegistry({ pluginIds, pluginSdkResolution: "dist", }), ); + const coveredPluginIds = new Set(bulkEntries.map((entry) => entry.pluginId)); + if (coveredPluginIds.size === pluginIds.length) { + return bulkEntries; + } + return pluginIds.flatMap((pluginId) => + params + .pickEntries( + loadBundledCapabilityRuntimeRegistry({ + pluginIds: [pluginId], + pluginSdkResolution: "dist", + }), + ) + .filter((entry) => entry.pluginId === pluginId), + ); } export function loadVitestSpeechProviderContractRegistry(): SpeechProviderContractEntry[] { @@ -132,7 +208,7 @@ export function loadVitestImageGenerationProviderContractRegistry(): ImageGenera } export function loadVitestVideoGenerationProviderContractRegistry(): VideoGenerationProviderContractEntry[] { - return loadVitestCapabilityContractEntries({ + const entries = loadVitestCapabilityContractEntries({ contract: "videoGenerationProviders", pickEntries: (registry) => registry.videoGenerationProviders.map((entry) => ({ @@ -140,6 +216,14 @@ export function loadVitestVideoGenerationProviderContractRegistry(): VideoGenera provider: entry.provider, })), }); + const coveredPluginIds = new Set(entries.map((entry) => entry.pluginId)); + const missingPluginIds = VITEST_CONTRACT_PLUGIN_IDS.videoGenerationProviders.filter( + (pluginId) => !coveredPluginIds.has(pluginId), + ); + if (missingPluginIds.length === 0) { + return entries; + } + return [...entries, ...loadVitestVideoGenerationFallbackEntries(missingPluginIds)]; } export function loadVitestMusicGenerationProviderContractRegistry(): MusicGenerationProviderContractEntry[] {