fix: stabilize extensions surface test gate

This commit is contained in:
Peter Steinberger
2026-03-30 07:47:01 +09:00
parent 07c6981c70
commit 1efef8205c
14 changed files with 236 additions and 217 deletions

View File

@@ -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<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
loadConfig: configMocks.loadConfig,
};
});
const sessionTabRegistryMocks = vi.hoisted(() => ({
trackSessionBrowserTab: vi.fn(),

View File

@@ -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<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
loadConfig: loadConfigMock,
};
});
vi.mock("../../../../src/gateway/node-command-policy.js", () => ({
isNodeCommandAllowed: isNodeCommandAllowedMock,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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<string, unknown>;
};
const { monitorSingleAccount } = loadBundledPluginPublicSurfaceSync<{
monitorSingleAccount: (params: {
cfg: ClawdbotConfig;
account: ResolvedFeishuAccount;
runtime: RuntimeEnv;
botOpenIdSource: typeof FEISHU_PREFETCHED_BOT_OPEN_ID_SOURCE;
}) => Promise<void>;
}>({
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<T> = {
onFlush?: (items: T[]) => Promise<void>;
@@ -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<T extends RuntimeEnv>(params: {
createEventDispatcherMock: {
mockReturnValue: (value: unknown) => unknown;
@@ -422,6 +396,7 @@ export async function setupFeishuLifecycleHandler<T extends RuntimeEnv>(params:
params.createEventDispatcherMock.mockReturnValue({ register });
}
const monitorSingleAccount = await loadMonitorSingleAccount();
await monitorSingleAccount({
cfg: params.cfg,
account: params.account,

View File

@@ -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<FacadeModule>({
@@ -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"];

View File

@@ -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."

View File

@@ -1,5 +1,4 @@
import { vi } from "vitest";
import { resolveRelativeBundledPluginPublicModuleId } from "../../src/test-utils/bundled-plugin-public-surface.js";
export type SearchImpl = () => Promise<unknown[]>;
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,

View File

@@ -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<LoginOpenAICodexOAuth>(
const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn<GithubCopilotLoginCommand>());
const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn<EnsureAuthProfileStore>());
const listProfilesForProviderMock = vi.hoisted(() => vi.fn<ListProfilesForProvider>());
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<typeof import("openclaw/plugin-sdk/provider-auth-login")>();
@@ -45,18 +51,9 @@ vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
};
});
const { default: githubCopilotPlugin } = loadBundledPluginPublicSurfaceSync<{
default: Parameters<typeof registerProviders>[0];
}>({
pluginId: "github-copilot",
artifactBasename: "index.js",
});
const { default: openAIPlugin } = loadBundledPluginPublicSurfaceSync<{
default: Parameters<typeof registerProviders>[0];
}>({
pluginId: "openai",
artifactBasename: "index.js",
});
async function importBundledProviderPlugin<T>(moduleUrl: string): Promise<T> {
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<typeof registerProviders>[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<typeof registerProviders>[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<typeof registerProviders>[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");

View File

@@ -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<ReturnType<typeof requireProvider>>;
@@ -108,28 +131,12 @@ function runCatalog(
});
}
async function importBundledProviderPlugin<T>(moduleUrl: string): Promise<T> {
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<object>(githubCopilotTokenModuleId);
vi.doMock(bundledProviderModules.githubCopilotTokenModuleId, async () => {
const actual = await vi.importActual<object>(
bundledProviderModules.githubCopilotTokenModuleId,
);
return {
...actual,
resolveCopilotApiToken: resolveCopilotApiTokenMock,
};
});
vi.doMock(ollamaApiModuleId, async () => {
const actual = await vi.importActual<object>(ollamaApiModuleId);
vi.doMock(bundledProviderModules.ollamaApiModuleId, async () => {
const actual = await vi.importActual<object>(bundledProviderModules.ollamaApiModuleId);
return {
...actual,
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
};
});
vi.doMock(vllmApiModuleId, async () => {
const actual = await vi.importActual<object>(vllmApiModuleId);
vi.doMock(bundledProviderModules.vllmApiModuleId, async () => {
const actual = await vi.importActual<object>(bundledProviderModules.vllmApiModuleId);
return {
...actual,
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
};
});
vi.doMock(sglangApiModuleId, async () => {
const actual = await vi.importActual<object>(sglangApiModuleId);
vi.doMock(bundledProviderModules.sglangApiModuleId, async () => {
const actual = await vi.importActual<object>(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<typeof registerProviders>[0];
}>({ pluginId: "github-copilot", artifactBasename: "index.js" }),
loadBundledPluginPublicSurfaceSync<{
}>(bundledProviderModules.githubCopilotIndexModuleUrl),
importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>({ pluginId: "ollama", artifactBasename: "index.js" }),
loadBundledPluginPublicSurfaceSync<{
}>(bundledProviderModules.ollamaIndexModuleUrl),
importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>({ pluginId: "vllm", artifactBasename: "index.js" }),
loadBundledPluginPublicSurfaceSync<{
}>(bundledProviderModules.vllmIndexModuleUrl),
importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>({ pluginId: "sglang", artifactBasename: "index.js" }),
loadBundledPluginPublicSurfaceSync<{
}>(bundledProviderModules.sglangIndexModuleUrl),
importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>({ pluginId: "minimax", artifactBasename: "index.js" }),
loadBundledPluginPublicSurfaceSync<{
}>(bundledProviderModules.minimaxIndexModuleUrl),
importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>({ pluginId: "modelstudio", artifactBasename: "index.js" }),
loadBundledPluginPublicSurfaceSync<{
}>(bundledProviderModules.modelStudioIndexModuleUrl),
importBundledProviderPlugin<{
default: Parameters<typeof registerProviders>[0];
}>({ pluginId: "cloudflare-ai-gateway", artifactBasename: "index.js" }),
}>(bundledProviderModules.cloudflareAiGatewayIndexModuleUrl),
]);
state.githubCopilotProvider = requireProvider(
registerProviders(githubCopilotPlugin),

View File

@@ -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<typeof import("@mariozechner/pi-ai/oauth")>(
@@ -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<T>(moduleUrl: string): Promise<T> {
return (await import(`${moduleUrl}?t=${Date.now()}`)) as T;
}
function createModel(overrides: Partial<ProviderRuntimeModel> & Pick<ProviderRuntimeModel, "id">) {
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<typeof registerProviderPlugin>[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<typeof registerProviderPlugin>[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<typeof registerProviderPlugin>[0]["plugin"];
}>({
pluginId: "google",
artifactBasename: "index.js",
}),
}>(providerRuntimeContractModules.googleIndexModuleUrl),
},
{
providerIds: ["openai", "openai-codex"],
pluginId: "openai",
name: "OpenAI",
load: async () =>
loadBundledPluginPublicSurfaceSync<{
await importBundledProviderPlugin<{
default: Parameters<typeof registerProviderPlugin>[0]["plugin"];
}>({
pluginId: "openai",
artifactBasename: "index.js",
}),
}>(providerRuntimeContractModules.openAIIndexModuleUrl),
},
{
providerIds: ["openrouter"],
pluginId: "openrouter",
name: "OpenRouter",
load: async () =>
loadBundledPluginPublicSurfaceSync<{
await importBundledProviderPlugin<{
default: Parameters<typeof registerProviderPlugin>[0]["plugin"];
}>({
pluginId: "openrouter",
artifactBasename: "index.js",
}),
}>(providerRuntimeContractModules.openRouterIndexModuleUrl),
},
{
providerIds: ["venice"],
pluginId: "venice",
name: "Venice",
load: async () =>
loadBundledPluginPublicSurfaceSync<{
await importBundledProviderPlugin<{
default: Parameters<typeof registerProviderPlugin>[0]["plugin"];
}>({
pluginId: "venice",
artifactBasename: "index.js",
}),
}>(providerRuntimeContractModules.veniceIndexModuleUrl),
},
{
providerIds: ["xai"],
pluginId: "xai",
name: "xAI",
load: async () =>
loadBundledPluginPublicSurfaceSync<{
await importBundledProviderPlugin<{
default: Parameters<typeof registerProviderPlugin>[0]["plugin"];
}>({
pluginId: "xai",
artifactBasename: "index.js",
}),
}>(providerRuntimeContractModules.xAIIndexModuleUrl),
},
{
providerIds: ["zai"],
pluginId: "zai",
name: "Z.AI",
load: async () =>
loadBundledPluginPublicSurfaceSync<{
await importBundledProviderPlugin<{
default: Parameters<typeof registerProviderPlugin>[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,
});
});
});