Tests: dedupe contract helper plumbing (#48760)

* Plugins: share contract test helpers

* Channels: collapse inbound contract testkit
This commit is contained in:
Vincent Koc
2026-03-16 22:45:44 -07:00
committed by GitHub
parent 61ccc5bede
commit 64c69c3fc9
13 changed files with 85 additions and 127 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { inboundCtxCapture as capture } from "../../../../src/channels/plugins/contracts/inbound-contract-dispatch-mock.js";
import { inboundCtxCapture as capture } from "../../../../src/channels/plugins/contracts/inbound-testkit.js";
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/suites.js";
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
import { processDiscordMessage } from "./message-handler.process.js";

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { MsgContext } from "../../../../src/auto-reply/templating.js";
import { buildDispatchInboundCaptureMock } from "../../../../src/channels/plugins/contracts/dispatch-inbound-capture.js";
import { buildDispatchInboundCaptureMock } from "../../../../src/channels/plugins/contracts/inbound-testkit.js";
import type { OpenClawConfig } from "../../../../src/config/types.js";
import {
createBaseSignalEventHandlerDeps,

View File

@@ -1,18 +0,0 @@
import { vi } from "vitest";
export function buildDispatchInboundCaptureMock<T extends Record<string, unknown>>(
actual: T,
setCtx: (ctx: unknown) => void,
) {
const dispatchInboundMessage = vi.fn(async (params: { ctx: unknown }) => {
setCtx(params.ctx);
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
});
return {
...actual,
dispatchInboundMessage,
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
};
}

View File

@@ -1,20 +0,0 @@
import type { MsgContext } from "../../../auto-reply/templating.js";
import { buildDispatchInboundCaptureMock } from "./dispatch-inbound-capture.js";
export type InboundContextCapture = {
ctx: MsgContext | undefined;
};
export function createInboundContextCapture(): InboundContextCapture {
return { ctx: undefined };
}
export async function buildDispatchInboundContextCapture(
importOriginal: <T extends Record<string, unknown>>() => Promise<T>,
capture: InboundContextCapture,
) {
const actual = await importOriginal<typeof import("../../../auto-reply/dispatch.js")>();
return buildDispatchInboundCaptureMock(actual, (ctx) => {
capture.ctx = ctx as MsgContext;
});
}

View File

@@ -1,9 +0,0 @@
import { vi } from "vitest";
import { createInboundContextCapture } from "./inbound-contract-capture.js";
import { buildDispatchInboundContextCapture } from "./inbound-contract-capture.js";
export const inboundCtxCapture = createInboundContextCapture();
vi.mock("../../../auto-reply/dispatch.js", async (importOriginal) => {
return await buildDispatchInboundContextCapture(importOriginal, inboundCtxCapture);
});

View File

@@ -0,0 +1,39 @@
import { vi } from "vitest";
import type { MsgContext } from "../../../auto-reply/templating.js";
export type InboundContextCapture = {
ctx: MsgContext | undefined;
};
export function createInboundContextCapture(): InboundContextCapture {
return { ctx: undefined };
}
export function buildDispatchInboundCaptureMock<T extends Record<string, unknown>>(
actual: T,
setCtx: (ctx: unknown) => void,
) {
const dispatchInboundMessage = vi.fn(async (params: { ctx: unknown }) => {
setCtx(params.ctx);
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
});
return {
...actual,
dispatchInboundMessage,
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
};
}
export async function buildDispatchInboundContextCapture(
importOriginal: <T extends Record<string, unknown>>() => Promise<T>,
capture: InboundContextCapture,
) {
const actual = await importOriginal<typeof import("../../../auto-reply/dispatch.js")>();
return buildDispatchInboundCaptureMock(actual, (ctx) => {
capture.ctx = ctx as MsgContext;
});
}
export const inboundCtxCapture = createInboundContextCapture();

View File

@@ -8,7 +8,7 @@ import { createInboundSlackTestContext } from "../../../../extensions/slack/src/
import type { SlackMessageEvent } from "../../../../extensions/slack/src/types.js";
import type { MsgContext } from "../../../auto-reply/templating.js";
import type { OpenClawConfig } from "../../../config/config.js";
import { inboundCtxCapture } from "./inbound-contract-dispatch-mock.js";
import { inboundCtxCapture } from "./inbound-testkit.js";
import { expectChannelInboundContextContract } from "./suites.js";
const signalCapture = vi.hoisted(() => ({ ctx: undefined as MsgContext | undefined }));

View File

@@ -6,13 +6,12 @@ import {
readAuthProfilesForAgent,
requireOpenClawAgentDir,
setupAuthTestEnv,
} from "../../../test/helpers/auth-wizard.js";
} from "../../commands/test-wizard-helpers.js";
import { clearRuntimeAuthProfileStoreSnapshots } from "../../agents/auth-profiles/store.js";
import { applyAuthChoiceLoadedPluginProvider } from "../../plugins/provider-auth-choice.js";
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import { buildProviderPluginMethodChoice } from "../provider-wizard.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
import { requireProviderContractProvider, uniqueProviderContractProviders } from "./registry.js";
import { registerProviders, requireProvider } from "./testkit.js";
type ResolvePluginProviders =
typeof import("../../plugins/provider-auth-choice.runtime.js").resolvePluginProviders;
@@ -67,22 +66,6 @@ type StoredAuthProfile = {
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) {
const captured = createCapturedPluginRegistration();
for (const plugin of plugins) {
plugin.register(captured.api);
}
return captured.providers;
}
function requireProvider(providers: ProviderPlugin[], providerId: string) {
const provider = providers.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider ${providerId} missing`);
}
return provider;
}
describe("provider auth-choice contract", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",

View File

@@ -4,14 +4,13 @@ import {
replaceRuntimeAuthProfileStoreSnapshots,
} from "../../agents/auth-profiles/store.js";
import { createNonExitingRuntime } from "../../runtime.js";
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import type {
WizardMultiSelectParams,
WizardPrompter,
WizardProgress,
WizardSelectParams,
} from "../../wizard/prompts.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
import { registerProviders, requireProvider } from "./testkit.js";
type LoginOpenAICodexOAuth =
(typeof import("../../plugins/provider-openai-codex-oauth.js"))["loginOpenAICodexOAuth"];
@@ -78,22 +77,6 @@ function buildAuthContext() {
};
}
function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) {
const captured = createCapturedPluginRegistration();
for (const plugin of plugins) {
plugin.register(captured.api);
}
return captured.providers;
}
function requireProvider(providers: ProviderPlugin[], providerId: string) {
const provider = providers.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider ${providerId} missing`);
}
return provider;
}
describe("provider auth contract", () => {
afterEach(() => {
loginOpenAICodexOAuthMock.mockReset();

View File

@@ -5,9 +5,8 @@ import {
} from "../../agents/auth-profiles/store.js";
import { QWEN_OAUTH_MARKER } from "../../agents/model-auth-markers.js";
import type { ModelDefinitionConfig } from "../../config/types.models.js";
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import { runProviderCatalog } from "../provider-discovery.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
import { registerProviders, requireProvider } from "./testkit.js";
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
const buildOllamaProviderMock = vi.hoisted(() => vi.fn());
@@ -60,22 +59,6 @@ const cloudflareAiGatewayPlugin = (
await import("../../../extensions/cloudflare-ai-gateway/index.js")
).default;
function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) {
const captured = createCapturedPluginRegistration();
for (const plugin of plugins) {
plugin.register(captured.api);
}
return captured.providers;
}
function requireProvider(providers: ProviderPlugin[], providerId: string) {
const provider = providers.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider ${providerId} missing`);
}
return provider;
}
function createModelConfig(id: string, name = id): ModelDefinitionConfig {
return {
id,

View File

@@ -2,15 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { withBundledPluginAllowlistCompat } from "../bundled-compat.js";
import { __testing as providerTesting } from "../providers.js";
import { resolvePluginWebSearchProviders } from "../web-search-providers.js";
import { providerContractPluginIds, webSearchProviderContractRegistry } from "./registry.js";
function uniqueSortedPluginIds(values: string[]) {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
function normalizeProviderContractPluginId(pluginId: string) {
return pluginId === "kimi-coding" ? "kimi" : pluginId;
}
import { providerContractCompatPluginIds, webSearchProviderContractRegistry } from "./registry.js";
import { uniqueSortedStrings } from "./testkit.js";
describe("plugin loader contract", () => {
beforeEach(() => {
@@ -18,9 +11,7 @@ describe("plugin loader contract", () => {
});
it("keeps bundled provider compatibility wired to the provider registry", () => {
const providerPluginIds = uniqueSortedPluginIds(
providerContractPluginIds.map(normalizeProviderContractPluginId),
);
const providerPluginIds = uniqueSortedStrings(providerContractCompatPluginIds);
const compatPluginIds = providerTesting.resolveBundledProviderCompatPluginIds({
config: {
plugins: {
@@ -38,16 +29,12 @@ describe("plugin loader contract", () => {
pluginIds: compatPluginIds,
});
expect(uniqueSortedPluginIds(compatPluginIds)).toEqual(
expect.arrayContaining(providerPluginIds),
);
expect(uniqueSortedStrings(compatPluginIds)).toEqual(expect.arrayContaining(providerPluginIds));
expect(compatConfig?.plugins?.allow).toEqual(expect.arrayContaining(providerPluginIds));
});
it("keeps vitest bundled provider enablement wired to the provider registry", () => {
const providerPluginIds = uniqueSortedPluginIds(
providerContractPluginIds.map(normalizeProviderContractPluginId),
);
const providerPluginIds = uniqueSortedStrings(providerContractCompatPluginIds);
const compatConfig = providerTesting.withBundledProviderVitestCompat({
config: undefined,
pluginIds: providerPluginIds,
@@ -61,19 +48,19 @@ describe("plugin loader contract", () => {
});
it("keeps bundled web search loading scoped to the web search registry", () => {
const webSearchPluginIds = uniqueSortedPluginIds(
const webSearchPluginIds = uniqueSortedStrings(
webSearchProviderContractRegistry.map((entry) => entry.pluginId),
);
const providers = resolvePluginWebSearchProviders({});
expect(uniqueSortedPluginIds(providers.map((provider) => provider.pluginId))).toEqual(
expect(uniqueSortedStrings(providers.map((provider) => provider.pluginId))).toEqual(
webSearchPluginIds,
);
});
it("keeps bundled web search allowlist compatibility wired to the web search registry", () => {
const webSearchPluginIds = uniqueSortedPluginIds(
const webSearchPluginIds = uniqueSortedStrings(
webSearchProviderContractRegistry.map((entry) => entry.pluginId),
);
@@ -86,7 +73,7 @@ describe("plugin loader contract", () => {
},
});
expect(uniqueSortedPluginIds(providers.map((provider) => provider.pluginId))).toEqual(
expect(uniqueSortedStrings(providers.map((provider) => provider.pluginId))).toEqual(
webSearchPluginIds,
);
});

View File

@@ -160,6 +160,10 @@ export const providerContractPluginIds = [
...new Set(providerContractRegistry.map((entry) => entry.pluginId)),
].toSorted((left, right) => left.localeCompare(right));
export const providerContractCompatPluginIds = providerContractPluginIds.map((pluginId) =>
pluginId === "kimi-coding" ? "kimi" : pluginId,
);
export function requireProviderContractProvider(providerId: string): ProviderPlugin {
const provider = uniqueProviderContractProviders.find((entry) => entry.id === providerId);
if (!provider) {

View File

@@ -0,0 +1,26 @@
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
type RegistrablePlugin = {
register(api: OpenClawPluginApi): void;
};
export function registerProviders(...plugins: RegistrablePlugin[]) {
const captured = createCapturedPluginRegistration();
for (const plugin of plugins) {
plugin.register(captured.api);
}
return captured.providers;
}
export function requireProvider(providers: ProviderPlugin[], providerId: string) {
const provider = providers.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider ${providerId} missing`);
}
return provider;
}
export function uniqueSortedStrings(values: readonly string[]) {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}