diff --git a/extensions/browser/src/browser-tool.test.ts b/extensions/browser/src/browser-tool.test.ts index 5f4ed3b414a..2ab1c36b202 100644 --- a/extensions/browser/src/browser-tool.test.ts +++ b/extensions/browser/src/browser-tool.test.ts @@ -115,7 +115,13 @@ vi.mock("../../../src/agents/tools/gateway.js", () => gatewayMocks); const configMocks = vi.hoisted(() => ({ loadConfig: vi.fn(() => ({ browser: {} })), })); -vi.mock("openclaw/plugin-sdk/config-runtime", () => configMocks); +vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: configMocks.loadConfig, + }; +}); const sessionTabRegistryMocks = vi.hoisted(() => ({ trackSessionBrowserTab: vi.fn(), diff --git a/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts b/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts index 8b2fef26fb7..2ccb924a182 100644 --- a/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts +++ b/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts @@ -8,9 +8,13 @@ const { loadConfigMock, isNodeCommandAllowedMock, resolveNodeCommandAllowlistMoc }), ); -vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ - loadConfig: loadConfigMock, -})); +vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: loadConfigMock, + }; +}); vi.mock("../../../../src/gateway/node-command-policy.js", () => ({ isNodeCommandAllowed: isNodeCommandAllowedMock, diff --git a/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts b/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts index 29986919558..699d34157b0 100644 --- a/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts @@ -1,5 +1,8 @@ import "./lifecycle.test-support.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; +import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; +import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; import { createFeishuLifecycleFixture, createFeishuTextMessageEvent, @@ -8,10 +11,7 @@ import { restoreFeishuLifecycleStateDir, setFeishuLifecycleStateDir, setupFeishuLifecycleHandler, -} from "../../../test/helpers/plugins/feishu-lifecycle.js"; -import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; -import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; +} from "./test-support/lifecycle.js"; import type { ResolvedFeishuAccount } from "./types.js"; const { diff --git a/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts b/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts index 4a4b4127535..103a0e95c04 100644 --- a/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts @@ -1,5 +1,8 @@ import "./lifecycle.test-support.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; +import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; +import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; import { createFeishuLifecycleConfig, createFeishuLifecycleReplyDispatcher, @@ -12,10 +15,7 @@ import { restoreFeishuLifecycleStateDir, setFeishuLifecycleStateDir, setupFeishuLifecycleHandler, -} from "../../../test/helpers/plugins/feishu-lifecycle.js"; -import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; -import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; +} from "./test-support/lifecycle.js"; import type { ResolvedFeishuAccount } from "./types.js"; const { diff --git a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts index b9a9353c684..e9889546040 100644 --- a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts @@ -1,5 +1,9 @@ import "./lifecycle.test-support.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createNonExitingRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; +import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; +import { FeishuConfigSchema } from "./config-schema.js"; +import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; import { createFeishuTextMessageEvent, createFeishuLifecycleReplyDispatcher, @@ -9,11 +13,7 @@ import { runFeishuLifecycleSequence, setFeishuLifecycleStateDir, setupFeishuLifecycleHandler, -} from "../../../test/helpers/plugins/feishu-lifecycle.js"; -import { createNonExitingRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; -import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { FeishuConfigSchema } from "./config-schema.js"; -import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; +} from "./test-support/lifecycle.js"; import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js"; const { diff --git a/extensions/feishu/src/monitor.card-action.lifecycle.test.ts b/extensions/feishu/src/monitor.card-action.lifecycle.test.ts index 71fdbaecce3..7ecc3319e96 100644 --- a/extensions/feishu/src/monitor.card-action.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.card-action.lifecycle.test.ts @@ -1,5 +1,10 @@ import "./lifecycle.test-support.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; +import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; +import { resetProcessedFeishuCardActionTokensForTests } from "./card-action.js"; +import { createFeishuCardInteractionEnvelope } from "./card-interaction.js"; +import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; import { createFeishuLifecycleConfig, createFeishuLifecycleReplyDispatcher, @@ -12,12 +17,7 @@ import { restoreFeishuLifecycleStateDir, setFeishuLifecycleStateDir, setupFeishuLifecycleHandler, -} from "../../../test/helpers/plugins/feishu-lifecycle.js"; -import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; -import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { resetProcessedFeishuCardActionTokensForTests } from "./card-action.js"; -import { createFeishuCardInteractionEnvelope } from "./card-interaction.js"; -import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; +} from "./test-support/lifecycle.js"; import type { ResolvedFeishuAccount } from "./types.js"; const { diff --git a/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts b/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts index 5e9f6ab35da..6ef4f7544cd 100644 --- a/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.reply-once.lifecycle.test.ts @@ -1,5 +1,8 @@ import "./lifecycle.test-support.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; +import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; +import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; import { createFeishuLifecycleConfig, createFeishuLifecycleReplyDispatcher, @@ -13,10 +16,7 @@ import { restoreFeishuLifecycleStateDir, setFeishuLifecycleStateDir, setupFeishuLifecycleHandler, -} from "../../../test/helpers/plugins/feishu-lifecycle.js"; -import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; -import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; +} from "./test-support/lifecycle.js"; import type { ResolvedFeishuAccount } from "./types.js"; const { diff --git a/test/helpers/plugins/feishu-lifecycle.ts b/extensions/feishu/src/test-support/lifecycle.ts similarity index 92% rename from test/helpers/plugins/feishu-lifecycle.ts rename to extensions/feishu/src/test-support/lifecycle.ts index 6db8f5168c6..f1bc21854fa 100644 --- a/test/helpers/plugins/feishu-lifecycle.ts +++ b/extensions/feishu/src/test-support/lifecycle.ts @@ -1,39 +1,8 @@ -import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { expect, vi } from "vitest"; -import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -import { createPluginRuntimeMock } from "./plugin-runtime-mock.js"; - -type ResolvedFeishuAccount = { - accountId: string; - selectionSource: string; - enabled: boolean; - configured: boolean; - name?: string; - appId?: string; - appSecret?: string; - encryptKey?: string; - verificationToken?: string; - domain: string; - config: Record; -}; - -const { monitorSingleAccount } = loadBundledPluginPublicSurfaceSync<{ - monitorSingleAccount: (params: { - cfg: ClawdbotConfig; - account: ResolvedFeishuAccount; - runtime: RuntimeEnv; - botOpenIdSource: typeof FEISHU_PREFETCHED_BOT_OPEN_ID_SOURCE; - }) => Promise; -}>({ - pluginId: "feishu", - artifactBasename: "src/monitor.account.js", -}); -const { setFeishuRuntime } = loadBundledPluginPublicSurfaceSync<{ - setFeishuRuntime: (runtime: PluginRuntime) => void; -}>({ - pluginId: "feishu", - artifactBasename: "src/runtime.js", -}); +import { createPluginRuntimeMock } from "../../../../test/helpers/plugins/plugin-runtime-mock.js"; +import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../../runtime-api.js"; +import { setFeishuRuntime } from "../runtime.js"; +import type { ResolvedFeishuAccount } from "../types.js"; type InboundDebouncerParams = { onFlush?: (items: T[]) => Promise; @@ -261,7 +230,7 @@ export function createResolvedFeishuLifecycleAccount(params: { }): ResolvedFeishuAccount { return { accountId: params.accountId, - selectionSource: "explicit", + selectionSource: "config", enabled: true, configured: true, appId: params.appId, @@ -400,6 +369,11 @@ export function expectFeishuReplyDispatcherSentFinalReplyOnce(params: { expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1); } +async function loadMonitorSingleAccount() { + const module = await import("../monitor.account.js"); + return module.monitorSingleAccount; +} + export async function setupFeishuLifecycleHandler(params: { createEventDispatcherMock: { mockReturnValue: (value: unknown) => unknown; @@ -422,6 +396,7 @@ export async function setupFeishuLifecycleHandler(params: params.createEventDispatcherMock.mockReturnValue({ register }); } + const monitorSingleAccount = await loadMonitorSingleAccount(); await monitorSingleAccount({ cfg: params.cfg, account: params.account, diff --git a/src/plugin-sdk/matrix-runtime-surface.ts b/src/plugin-sdk/matrix-runtime-surface.ts index 92e63e93846..86125c6a033 100644 --- a/src/plugin-sdk/matrix-runtime-surface.ts +++ b/src/plugin-sdk/matrix-runtime-surface.ts @@ -2,10 +2,7 @@ import type { PluginSdkFacadeTypeMap } from "../generated/plugin-sdk-facade-type-map.generated.js"; type FacadeEntry = PluginSdkFacadeTypeMap["matrix-runtime-surface"]; type FacadeModule = FacadeEntry["module"]; -import { - createLazyFacadeObjectValue, - loadBundledPluginPublicSurfaceModuleSync, -} from "./facade-runtime.js"; +import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; function loadFacadeModule(): FacadeModule { return loadBundledPluginPublicSurfaceModuleSync({ @@ -19,6 +16,5 @@ export const resolveMatrixAccountStringValues: FacadeModule["resolveMatrixAccoun loadFacadeModule()["resolveMatrixAccountStringValues"]( ...args, )) as FacadeModule["resolveMatrixAccountStringValues"]; -export const setMatrixRuntime: FacadeModule["setMatrixRuntime"] = createLazyFacadeObjectValue( - () => loadFacadeModule()["setMatrixRuntime"] as object, -) as FacadeModule["setMatrixRuntime"]; +export const setMatrixRuntime: FacadeModule["setMatrixRuntime"] = ((...args) => + loadFacadeModule()["setMatrixRuntime"](...args)) as FacadeModule["setMatrixRuntime"]; diff --git a/test/fixtures/test-parallel.behavior.json b/test/fixtures/test-parallel.behavior.json index 645223e05ad..b2e027ef465 100644 --- a/test/fixtures/test-parallel.behavior.json +++ b/test/fixtures/test-parallel.behavior.json @@ -146,6 +146,10 @@ "file": "extensions/duckduckgo/src/ddg-search-provider.test.ts", "reason": "This provider test hoists a client mock and imports the provider lazily; keep it in its own forked lane so shared extension workers do not reuse a previously cached module under test." }, + { + "file": "extensions/browser/src/browser/chrome.test.ts", + "reason": "This Chrome CDP helper suite opens mock WebSocket servers and stubs global fetch state; keep it isolated so shared extension workers do not inherit stale browser probe state from neighboring files." + }, { "file": "extensions/firecrawl/src/firecrawl-scrape-tool.test.ts", "reason": "This scrape-tool suite hoists client mocks and imports the tool lazily; keep it in its own forked lane so shared extension workers cannot bypass the mocked client via cached modules." @@ -158,6 +162,26 @@ "file": "extensions/firecrawl/src/firecrawl-search-tool.test.ts", "reason": "This tool suite hoists the Firecrawl client mock and imports lazily; keep it isolated so shared extension workers cannot leak cached implementations across files." }, + { + "file": "extensions/feishu/src/monitor.acp-init-failure.lifecycle.test.ts", + "reason": "This Feishu websocket lifecycle suite can stall only in the shared extensions swarm when reconnect state bleeds across files; keep it isolated so ACP init-failure teardown stays deterministic." + }, + { + "file": "extensions/feishu/src/monitor.bot-menu.lifecycle.test.ts", + "reason": "This Feishu websocket lifecycle suite can stall only in the shared extensions swarm when reconnect state bleeds across files; keep it isolated so bot-menu lifecycle teardown stays deterministic." + }, + { + "file": "extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts", + "reason": "This Feishu websocket lifecycle suite can stall only in the shared extensions swarm when reconnect state bleeds across files; keep it isolated so broadcast reply-once teardown stays deterministic." + }, + { + "file": "extensions/feishu/src/monitor.card-action.lifecycle.test.ts", + "reason": "This Feishu websocket lifecycle suite shares the same reconnect helpers and module state; keep it isolated with the other Feishu lifecycle files so card-action runs do not inherit cross-file websocket state." + }, + { + "file": "extensions/feishu/src/monitor.reply-once.lifecycle.test.ts", + "reason": "This Feishu websocket lifecycle suite can stall only in the shared extensions swarm when reconnect state bleeds across files; keep it isolated so reply-once teardown stays deterministic." + }, { "file": "extensions/line/src/download.test.ts", "reason": "This LINE media-download suite depends on hoisted SDK mocks; run it in its own forked lane so shared extension workers do not fall back to the real SDK client." diff --git a/test/helpers/memory-tool-manager-mock.ts b/test/helpers/memory-tool-manager-mock.ts index 29001ad88c4..422a910cc56 100644 --- a/test/helpers/memory-tool-manager-mock.ts +++ b/test/helpers/memory-tool-manager-mock.ts @@ -1,5 +1,4 @@ import { vi } from "vitest"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../src/test-utils/bundled-plugin-public-surface.js"; export type SearchImpl = () => Promise; export type MemoryReadParams = { relPath: string; from?: number; lines?: number }; @@ -39,16 +38,10 @@ const readAgentMemoryFileMock = vi.fn( async (params: MemoryReadParams) => await readFileImpl(params), ); -const memoryIndexModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "memory-core", - artifactBasename: "src/memory/index.js", -}); -const memoryToolsRuntimeModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "memory-core", - artifactBasename: "src/tools.runtime.js", -}); +const { memoryIndexModuleId, memoryToolsRuntimeModuleId } = vi.hoisted(() => ({ + memoryIndexModuleId: "../../extensions/memory-core/src/memory/index.js", + memoryToolsRuntimeModuleId: "../../extensions/memory-core/src/tools.runtime.js", +})); vi.mock(memoryIndexModuleId, () => ({ getMemorySearchManager: getMemorySearchManagerMock, diff --git a/test/helpers/plugins/provider-auth-contract.ts b/test/helpers/plugins/provider-auth-contract.ts index d377594d193..e5687cc1e25 100644 --- a/test/helpers/plugins/provider-auth-contract.ts +++ b/test/helpers/plugins/provider-auth-contract.ts @@ -3,7 +3,6 @@ import { clearRuntimeAuthProfileStoreSnapshots } from "../../../src/agents/auth- import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js"; import { registerProviders, requireProvider } from "../../../src/plugins/contracts/testkit.js"; import { createNonExitingRuntime } from "../../../src/runtime.js"; -import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; import type { WizardMultiSelectParams, WizardPrompter, @@ -26,6 +25,13 @@ const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn( const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn()); const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); +const providerAuthContractModules = vi.hoisted(() => ({ + githubCopilotIndexModuleUrl: new URL( + "../../../extensions/github-copilot/index.ts", + import.meta.url, + ).href, + openAIIndexModuleUrl: new URL("../../../extensions/openai/index.ts", import.meta.url).href, +})); vi.mock("openclaw/plugin-sdk/provider-auth-login", async (importOriginal) => { const actual = await importOriginal(); @@ -45,18 +51,9 @@ vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => { }; }); -const { default: githubCopilotPlugin } = loadBundledPluginPublicSurfaceSync<{ - default: Parameters[0]; -}>({ - pluginId: "github-copilot", - artifactBasename: "index.js", -}); -const { default: openAIPlugin } = loadBundledPluginPublicSurfaceSync<{ - default: Parameters[0]; -}>({ - pluginId: "openai", - artifactBasename: "index.js", -}); +async function importBundledProviderPlugin(moduleUrl: string): Promise { + return (await import(`${moduleUrl}?t=${Date.now()}`)) as T; +} function buildPrompter(): WizardPrompter { const progress: WizardProgress = { @@ -166,6 +163,9 @@ export function describeOpenAICodexProviderAuthContract() { installSharedAuthProfileStoreHooks(state); async function expectStableFallbackProfile(params: { access: string; profileId: string }) { + const { default: openAIPlugin } = await importBundledProviderPlugin<{ + default: Parameters[0]; + }>(providerAuthContractModules.openAIIndexModuleUrl); const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex"); loginOpenAICodexOAuthMock.mockResolvedValueOnce({ refresh: "refresh-token", @@ -183,12 +183,15 @@ export function describeOpenAICodexProviderAuthContract() { ); } - function getProvider() { + async function getProvider() { + const { default: openAIPlugin } = await importBundledProviderPlugin<{ + default: Parameters[0]; + }>(providerAuthContractModules.openAIIndexModuleUrl); return requireProvider(registerProviders(openAIPlugin), "openai-codex"); } it("keeps OAuth auth results provider-owned", async () => { - const provider = getProvider(); + const provider = await getProvider(); loginOpenAICodexOAuthMock.mockResolvedValueOnce({ email: "user@example.com", refresh: "refresh-token", @@ -210,7 +213,7 @@ export function describeOpenAICodexProviderAuthContract() { }); it("backfills OAuth email from the JWT profile claim", async () => { - const provider = getProvider(); + const provider = await getProvider(); const access = createJwt({ "https://api.openai.com/profile": { email: "jwt-user@example.com", @@ -274,7 +277,7 @@ export function describeOpenAICodexProviderAuthContract() { }); it("falls back to the default profile when JWT parsing yields no identity", async () => { - const provider = getProvider(); + const provider = await getProvider(); loginOpenAICodexOAuthMock.mockResolvedValueOnce({ refresh: "refresh-token", access: "not-a-jwt-token", @@ -294,7 +297,7 @@ export function describeOpenAICodexProviderAuthContract() { }); it("keeps OAuth failures non-fatal at the provider layer", async () => { - const provider = getProvider(); + const provider = await getProvider(); loginOpenAICodexOAuthMock.mockRejectedValueOnce(new Error("oauth failed")); await expect(provider.auth[0]?.run(buildAuthContext() as never)).resolves.toEqual({ @@ -312,12 +315,15 @@ export function describeGithubCopilotProviderAuthContract() { describe("github-copilot provider auth contract", () => { installSharedAuthProfileStoreHooks(state); - function getProvider() { + async function getProvider() { + const { default: githubCopilotPlugin } = await importBundledProviderPlugin<{ + default: Parameters[0]; + }>(providerAuthContractModules.githubCopilotIndexModuleUrl); return requireProvider(registerProviders(githubCopilotPlugin), "github-copilot"); } it("keeps device auth results provider-owned", async () => { - const provider = getProvider(); + const provider = await getProvider(); state.authStore.profiles["github-copilot:github"] = { type: "token", provider: "github-copilot", @@ -362,7 +368,7 @@ export function describeGithubCopilotProviderAuthContract() { }); it("keeps auth gated on interactive TTYs", async () => { - const provider = getProvider(); + const provider = await getProvider(); const stdin = process.stdin as NodeJS.ReadStream & { isTTY?: boolean }; const hadOwnIsTTY = Object.prototype.hasOwnProperty.call(stdin, "isTTY"); const previousIsTTYDescriptor = Object.getOwnPropertyDescriptor(stdin, "isTTY"); diff --git a/test/helpers/plugins/provider-discovery-contract.ts b/test/helpers/plugins/provider-discovery-contract.ts index ff013829142..bc1dbf671b4 100644 --- a/test/helpers/plugins/provider-discovery-contract.ts +++ b/test/helpers/plugins/provider-discovery-contract.ts @@ -3,10 +3,6 @@ import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.j import type { OpenClawConfig } from "../../../src/config/config.js"; import type { ModelDefinitionConfig } from "../../../src/config/types.models.js"; import { registerProviders, requireProvider } from "../../../src/plugins/contracts/testkit.js"; -import { - loadBundledPluginPublicSurfaceSync, - resolveRelativeBundledPluginPublicModuleId, -} from "../../../src/test-utils/bundled-plugin-public-surface.js"; const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn()); const buildOllamaProviderMock = vi.hoisted(() => vi.fn()); @@ -14,6 +10,33 @@ const buildVllmProviderMock = vi.hoisted(() => vi.fn()); const buildSglangProviderMock = vi.hoisted(() => vi.fn()); const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); +const bundledProviderModules = vi.hoisted(() => ({ + cloudflareAiGatewayIndexModuleUrl: new URL( + "../../../extensions/cloudflare-ai-gateway/index.ts", + import.meta.url, + ).href, + cloudflareAiGatewayIndexModuleId: new URL( + "../../../extensions/cloudflare-ai-gateway/index.js", + import.meta.url, + ).pathname, + githubCopilotIndexModuleUrl: new URL( + "../../../extensions/github-copilot/index.ts", + import.meta.url, + ).href, + githubCopilotTokenModuleId: new URL( + "../../../extensions/github-copilot/token.js", + import.meta.url, + ).pathname, + minimaxIndexModuleUrl: new URL("../../../extensions/minimax/index.ts", import.meta.url).href, + modelStudioIndexModuleUrl: new URL("../../../extensions/modelstudio/index.ts", import.meta.url) + .href, + ollamaApiModuleId: new URL("../../../extensions/ollama/api.js", import.meta.url).pathname, + ollamaIndexModuleUrl: new URL("../../../extensions/ollama/index.ts", import.meta.url).href, + sglangApiModuleId: new URL("../../../extensions/sglang/api.js", import.meta.url).pathname, + sglangIndexModuleUrl: new URL("../../../extensions/sglang/index.ts", import.meta.url).href, + vllmApiModuleId: new URL("../../../extensions/vllm/api.js", import.meta.url).pathname, + vllmIndexModuleUrl: new URL("../../../extensions/vllm/index.ts", import.meta.url).href, +})); type ProviderHandle = Awaited>; @@ -108,28 +131,12 @@ function runCatalog( }); } +async function importBundledProviderPlugin(moduleUrl: string): Promise { + return (await import(`${moduleUrl}?t=${Date.now()}`)) as T; +} + function installDiscoveryHooks(state: DiscoveryState) { beforeEach(async () => { - const githubCopilotTokenModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "github-copilot", - artifactBasename: "token.js", - }); - const ollamaApiModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "ollama", - artifactBasename: "api.js", - }); - const vllmApiModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "vllm", - artifactBasename: "api.js", - }); - const sglangApiModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "sglang", - artifactBasename: "api.js", - }); vi.resetModules(); vi.doMock("openclaw/plugin-sdk/agent-runtime", async () => { const actual = await import("../../../src/plugin-sdk/agent-runtime.ts"); @@ -147,29 +154,31 @@ function installDiscoveryHooks(state: DiscoveryState) { listProfilesForProvider: listProfilesForProviderMock, }; }); - vi.doMock(githubCopilotTokenModuleId, async () => { - const actual = await vi.importActual(githubCopilotTokenModuleId); + vi.doMock(bundledProviderModules.githubCopilotTokenModuleId, async () => { + const actual = await vi.importActual( + bundledProviderModules.githubCopilotTokenModuleId, + ); return { ...actual, resolveCopilotApiToken: resolveCopilotApiTokenMock, }; }); - vi.doMock(ollamaApiModuleId, async () => { - const actual = await vi.importActual(ollamaApiModuleId); + vi.doMock(bundledProviderModules.ollamaApiModuleId, async () => { + const actual = await vi.importActual(bundledProviderModules.ollamaApiModuleId); return { ...actual, buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args), }; }); - vi.doMock(vllmApiModuleId, async () => { - const actual = await vi.importActual(vllmApiModuleId); + vi.doMock(bundledProviderModules.vllmApiModuleId, async () => { + const actual = await vi.importActual(bundledProviderModules.vllmApiModuleId); return { ...actual, buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args), }; }); - vi.doMock(sglangApiModuleId, async () => { - const actual = await vi.importActual(sglangApiModuleId); + vi.doMock(bundledProviderModules.sglangApiModuleId, async () => { + const actual = await vi.importActual(bundledProviderModules.sglangApiModuleId); return { ...actual, buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args), @@ -186,27 +195,27 @@ function installDiscoveryHooks(state: DiscoveryState) { { default: modelStudioPlugin }, { default: cloudflareAiGatewayPlugin }, ] = await Promise.all([ - loadBundledPluginPublicSurfaceSync<{ + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "github-copilot", artifactBasename: "index.js" }), - loadBundledPluginPublicSurfaceSync<{ + }>(bundledProviderModules.githubCopilotIndexModuleUrl), + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "ollama", artifactBasename: "index.js" }), - loadBundledPluginPublicSurfaceSync<{ + }>(bundledProviderModules.ollamaIndexModuleUrl), + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "vllm", artifactBasename: "index.js" }), - loadBundledPluginPublicSurfaceSync<{ + }>(bundledProviderModules.vllmIndexModuleUrl), + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "sglang", artifactBasename: "index.js" }), - loadBundledPluginPublicSurfaceSync<{ + }>(bundledProviderModules.sglangIndexModuleUrl), + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "minimax", artifactBasename: "index.js" }), - loadBundledPluginPublicSurfaceSync<{ + }>(bundledProviderModules.minimaxIndexModuleUrl), + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "modelstudio", artifactBasename: "index.js" }), - loadBundledPluginPublicSurfaceSync<{ + }>(bundledProviderModules.modelStudioIndexModuleUrl), + importBundledProviderPlugin<{ default: Parameters[0]; - }>({ pluginId: "cloudflare-ai-gateway", artifactBasename: "index.js" }), + }>(bundledProviderModules.cloudflareAiGatewayIndexModuleUrl), ]); state.githubCopilotProvider = requireProvider( registerProviders(githubCopilotPlugin), diff --git a/test/helpers/plugins/provider-runtime-contract.ts b/test/helpers/plugins/provider-runtime-contract.ts index c82670e1e49..bcfa4649193 100644 --- a/test/helpers/plugins/provider-runtime-contract.ts +++ b/test/helpers/plugins/provider-runtime-contract.ts @@ -4,10 +4,6 @@ import path from "node:path"; import type { StreamFn } from "@mariozechner/pi-agent-core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ProviderPlugin, ProviderRuntimeModel } from "../../../src/plugins/types.js"; -import { - loadBundledPluginPublicSurfaceSync, - resolveRelativeBundledPluginPublicModuleId, -} from "../../../src/test-utils/bundled-plugin-public-surface.js"; import { createProviderUsageFetch, makeResponse, @@ -24,6 +20,24 @@ const getOAuthProvidersMock = vi.hoisted(() => { id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" }, ]), ); +const providerRuntimeContractModules = vi.hoisted(() => ({ + anthropicIndexModuleUrl: new URL("../../../extensions/anthropic/index.ts", import.meta.url).href, + githubCopilotIndexModuleUrl: new URL( + "../../../extensions/github-copilot/index.ts", + import.meta.url, + ).href, + googleIndexModuleUrl: new URL("../../../extensions/google/index.ts", import.meta.url).href, + openAIIndexModuleUrl: new URL("../../../extensions/openai/index.ts", import.meta.url).href, + openAICodexProviderRuntimeModuleId: new URL( + "../../../extensions/openai/openai-codex-provider.runtime.js", + import.meta.url, + ).pathname, + openRouterIndexModuleUrl: new URL("../../../extensions/openrouter/index.ts", import.meta.url) + .href, + veniceIndexModuleUrl: new URL("../../../extensions/venice/index.ts", import.meta.url).href, + xAIIndexModuleUrl: new URL("../../../extensions/xai/index.ts", import.meta.url).href, + zaiIndexModuleUrl: new URL("../../../extensions/zai/index.ts", import.meta.url).href, +})); vi.mock("@mariozechner/pi-ai/oauth", async () => { const actual = await vi.importActual( @@ -36,16 +50,14 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { }; }); -const openAICodexProviderRuntimeModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "openai", - artifactBasename: "openai-codex-provider.runtime.js", -}); - -vi.mock(openAICodexProviderRuntimeModuleId, () => ({ +vi.mock(providerRuntimeContractModules.openAICodexProviderRuntimeModuleId, () => ({ refreshOpenAICodexToken: refreshOpenAICodexTokenMock, })); +async function importBundledProviderPlugin(moduleUrl: string): Promise { + return (await import(`${moduleUrl}?t=${Date.now()}`)) as T; +} + function createModel(overrides: Partial & Pick) { return { id: overrides.id, @@ -74,96 +86,72 @@ const PROVIDER_RUNTIME_CONTRACT_FIXTURES: readonly ProviderRuntimeContractFixtur pluginId: "anthropic", name: "Anthropic", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "anthropic", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.anthropicIndexModuleUrl), }, { providerIds: ["github-copilot"], pluginId: "github-copilot", name: "GitHub Copilot", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "github-copilot", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.githubCopilotIndexModuleUrl), }, { providerIds: ["google", "google-gemini-cli"], pluginId: "google", name: "Google", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "google", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.googleIndexModuleUrl), }, { providerIds: ["openai", "openai-codex"], pluginId: "openai", name: "OpenAI", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "openai", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.openAIIndexModuleUrl), }, { providerIds: ["openrouter"], pluginId: "openrouter", name: "OpenRouter", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "openrouter", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.openRouterIndexModuleUrl), }, { providerIds: ["venice"], pluginId: "venice", name: "Venice", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "venice", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.veniceIndexModuleUrl), }, { providerIds: ["xai"], pluginId: "xai", name: "xAI", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "xai", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.xAIIndexModuleUrl), }, { providerIds: ["zai"], pluginId: "zai", name: "Z.AI", load: async () => - loadBundledPluginPublicSurfaceSync<{ + await importBundledProviderPlugin<{ default: Parameters[0]["plugin"]; - }>({ - pluginId: "zai", - artifactBasename: "index.js", - }), + }>(providerRuntimeContractModules.zaiIndexModuleUrl), }, ] as const; @@ -736,6 +724,27 @@ export function describeXAIProviderRuntimeContract() { }); }); + it("owns downstream xai compat contributions for x-ai routed models", () => { + const provider = requireProviderContractProvider("xai"); + + expect( + provider.contributeResolvedModelCompat?.({ + provider: "openrouter", + modelId: "x-ai/grok-4-1-fast", + model: createModel({ + id: "x-ai/grok-4-1-fast", + provider: "openrouter", + api: "openai-completions", + baseUrl: "https://openrouter.ai/api/v1", + }), + } as never), + ).toMatchObject({ + toolSchemaProfile: "xai", + nativeWebSearchTool: true, + toolCallArgumentsEncoding: "html-entities", + }); + }); + it("owns xai tool_stream defaults", () => { const provider = requireProviderContractProvider("xai"); @@ -804,25 +813,22 @@ export function describeOpenRouterProviderRuntimeContract() { describe("openrouter provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { installRuntimeHooks(); - it("owns xai downstream compat flags for x-ai routed models", () => { + it("owns dynamic OpenRouter model defaults", () => { const provider = requireProviderContractProvider("openrouter"); - expect( - provider.normalizeResolvedModel?.({ - provider: "openrouter", - modelId: "x-ai/grok-4-1-fast", - model: createModel({ - id: "x-ai/grok-4-1-fast", - provider: "openrouter", - api: "openai-completions", - baseUrl: "https://openrouter.ai/api/v1", - }), - }), - ).toMatchObject({ - compat: { - toolSchemaProfile: "xai", - nativeWebSearchTool: true, - toolCallArgumentsEncoding: "html-entities", - }, + const model = provider.resolveDynamicModel?.({ + provider: "openrouter", + modelId: "x-ai/grok-4-1-fast", + modelRegistry: { + find: () => null, + } as never, + }); + + expect(model).toMatchObject({ + id: "x-ai/grok-4-1-fast", + provider: "openrouter", + api: "openai-completions", + baseUrl: "https://openrouter.ai/api/v1", + maxTokens: 8192, }); }); });