diff --git a/config/knip.config.ts b/config/knip.config.ts index 1337b10ec043..a1de837389d8 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -113,9 +113,9 @@ const rootBundledPluginRuntimeDependencies = [ "tokenjuice", ] as const; -function strictBundledPluginWorkspace() { +function strictBundledPluginWorkspace(extraEntries: readonly string[] = []) { return { - entry: strictBundledPluginEntries, + entry: [...strictBundledPluginEntries, ...extraEntries], project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, } as const; @@ -373,19 +373,41 @@ const config = { project: ["index.js!", "scripts/**/*.js!"], }, [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/acpx`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/azure-speech`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/browser`]: strictBundledPluginWorkspace([ + // Core and plugin-SDK facades resolve these shipped Browser surfaces by basename. + "browser-control-auth.ts!", + "browser-config.ts!", + "browser-doctor.ts!", + "browser-host-inspection.ts!", + "browser-maintenance.ts!", + "browser-profiles.ts!", + ]), [`${BUNDLED_PLUGIN_ROOT_DIR}/cloudflare-ai-gateway`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/chutes`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/clawrouter`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/cohere`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/comfy`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/copilot`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/deepgram`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/deepinfra`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/discord`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/elevenlabs`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/featherless`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/fal`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/fireworks`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/google`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/huggingface`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/kilocode`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/kimi-coding`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/microsoft`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/memory-core`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/memory-lancedb`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/microsoft-foundry`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/migrate-claude`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/migrate-hermes`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/minimax`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/mistral`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/moonshot`]: strictBundledPluginWorkspace(), @@ -393,15 +415,17 @@ const config = { [`${BUNDLED_PLUGIN_ROOT_DIR}/pixverse`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/qianfan`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/qwen`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/qa-lab`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/senseaudio`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/tavily`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/tencent`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/vllm`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/voyage`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/xiaomi`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/xai`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/llama-cpp`]: { entry: bundledPluginEntries, - project: ["index.ts!", "src/**/*.{js,mjs,ts}!"], + project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], ignoreDependencies: [ // The provider resolves node-llama-cpp from its own package at runtime // so local embeddings use the plugin-owned native dependency. @@ -416,14 +440,14 @@ const config = { // public surface, so its exports are intentional even where the channel // consumes only a subset. entry: [...bundledPluginEntries, "protocol/index.ts!", "protocol/node.ts!"], - project: ["index.ts!", "src/**/*.{js,mjs,ts}!", "protocol/**/*.ts!"], + project: ["*.ts!", "src/**/*.{js,mjs,ts}!", "protocol/**/*.ts!"], ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, }, [`${BUNDLED_PLUGIN_ROOT_DIR}/*`]: { // Bundled plugins often load their public surface via string specifiers in // `index.ts` contracts, so Knip needs these convention-based entry files. entry: bundledPluginEntries, - project: ["index.ts!", "src/**/*.{js,mjs,ts}!"], + project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, }, }, diff --git a/extensions/amazon-bedrock/embedding-provider.test.ts b/extensions/amazon-bedrock/embedding-provider.test.ts index b4447c87b415..d2a309b8d4f0 100644 --- a/extensions/amazon-bedrock/embedding-provider.test.ts +++ b/extensions/amazon-bedrock/embedding-provider.test.ts @@ -1,6 +1,7 @@ // Amazon Bedrock tests cover embedding provider plugin behavior. import { describe, expect, it, vi } from "vitest"; -import { testing, hasAwsCredentials } from "./embedding-provider.js"; +import { hasAwsCredentials } from "./embedding-provider.js"; +import { embeddingTesting as testing } from "./test-support.js"; describe("hasAwsCredentials", () => { it("accepts static AWS key credentials without loading the credential chain", async () => { diff --git a/extensions/amazon-bedrock/embedding-provider.ts b/extensions/amazon-bedrock/embedding-provider.ts index 655b3a2ccebf..761a11ebe653 100644 --- a/extensions/amazon-bedrock/embedding-provider.ts +++ b/extensions/amazon-bedrock/embedding-provider.ts @@ -288,12 +288,16 @@ function parseCohereBatch(family: Family, raw: string): number[][] { return asNumberArrayBatch(embeddings); } -export const testing = { +const testing = { parseCohereBatch, parseSingle, stripInferenceProfilePrefix, }; +if (process.env.VITEST === "true") { + Reflect.set(globalThis, Symbol.for("openclaw.amazonBedrockEmbeddingTestApi"), testing); +} + // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- @@ -449,4 +453,3 @@ export async function hasAwsCredentials( return false; } } -export { testing as __testing }; diff --git a/extensions/amazon-bedrock/stream.runtime.test.ts b/extensions/amazon-bedrock/stream.runtime.test.ts index c46168fcba21..3e73b1d90496 100644 --- a/extensions/amazon-bedrock/stream.runtime.test.ts +++ b/extensions/amazon-bedrock/stream.runtime.test.ts @@ -2,7 +2,8 @@ import { BedrockRuntimeClient, ConversationRole } from "@aws-sdk/client-bedrock-runtime"; import { onLlmRequestActivity } from "openclaw/plugin-sdk/provider-stream-shared"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { streamBedrock, streamSimpleBedrock, testing } from "./stream.runtime.js"; +import { streamBedrock, streamSimpleBedrock } from "./stream.runtime.js"; +import { streamTesting as testing } from "./test-support.js"; function bedrockModel(overrides: Record) { return { diff --git a/extensions/amazon-bedrock/stream.runtime.ts b/extensions/amazon-bedrock/stream.runtime.ts index cebddd6cceb9..c10ef92ab6fa 100644 --- a/extensions/amazon-bedrock/stream.runtime.ts +++ b/extensions/amazon-bedrock/stream.runtime.ts @@ -1173,7 +1173,7 @@ function createImageBlock(mimeType: string, data: string) { } /** Test-only hooks for Bedrock runtime conversion and endpoint policy. */ -export const testing = { +const testing = { buildAdditionalModelRequestFields, convertMessages, getConfiguredBedrockRegion, @@ -1182,4 +1182,8 @@ export const testing = { resolveSimpleBedrockOptions, shouldUseExplicitBedrockEndpoint, }; + +if (process.env.VITEST === "true") { + Reflect.set(globalThis, Symbol.for("openclaw.amazonBedrockStreamTestApi"), testing); +} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/extensions/amazon-bedrock/test-support.ts b/extensions/amazon-bedrock/test-support.ts new file mode 100644 index 000000000000..400b23c06cae --- /dev/null +++ b/extensions/amazon-bedrock/test-support.ts @@ -0,0 +1,45 @@ +type BedrockEmbeddingTestApi = { + parseCohereBatch: (family: string, raw: string) => number[][]; + parseSingle: (family: string, raw: string) => number[]; + stripInferenceProfilePrefix: (modelId: string) => string; +}; + +type BedrockStreamTestApi = { + buildAdditionalModelRequestFields: ( + model: unknown, + options: Record, + ) => Record | undefined; + convertMessages: (...args: unknown[]) => Array<{ content?: unknown; role?: unknown }>; + getConfiguredBedrockRegion: (params: unknown) => string | undefined; + hasConfiguredBedrockProfile: (params: unknown) => boolean; + mapThinkingLevelToEffort: (model: unknown, level: unknown) => string; + resolveSimpleBedrockOptions: ( + model: unknown, + options: Record, + ) => Record; + shouldUseExplicitBedrockEndpoint: (...args: unknown[]) => boolean; +}; + +function requireTestApi(key: string): object { + const api = Reflect.get(globalThis, Symbol.for(key)); + if (!api) { + throw new Error(`${key} is unavailable`); + } + return api as object; +} + +function lazyTestApi(key: string): object { + return new Proxy( + {}, + { + get: (_target, property) => Reflect.get(requireTestApi(key), property), + }, + ); +} + +export const embeddingTesting = lazyTestApi( + "openclaw.amazonBedrockEmbeddingTestApi", +) as BedrockEmbeddingTestApi; +export const streamTesting = lazyTestApi( + "openclaw.amazonBedrockStreamTestApi", +) as BedrockStreamTestApi; diff --git a/extensions/chutes/models.test.ts b/extensions/chutes/models.test.ts index 272e2b4d17a1..3eadad7b9755 100644 --- a/extensions/chutes/models.test.ts +++ b/extensions/chutes/models.test.ts @@ -1,10 +1,10 @@ // Chutes tests cover models plugin behavior. import { expectDefined } from "@openclaw/normalization-core"; +import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildChutesModelDefinition, CHUTES_MODEL_CATALOG, - clearChutesModelCacheForTests, discoverChutesModels, } from "./models.js"; @@ -86,7 +86,7 @@ function requireChutesModel( describe("chutes-models", () => { beforeEach(() => { - clearChutesModelCacheForTests(); + clearLiveCatalogCacheForTests(); }); it("buildChutesModelDefinition returns config with required fields", () => { diff --git a/extensions/chutes/models.ts b/extensions/chutes/models.ts index 9dca8a7fae20..f77e184ea667 100644 --- a/extensions/chutes/models.ts +++ b/extensions/chutes/models.ts @@ -2,7 +2,6 @@ * Chutes model catalog, static model definitions, and dynamic model discovery. */ import { - clearLiveCatalogCacheForTests, getCachedLiveProviderModelRows, LiveModelCatalogHttpError, } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; @@ -489,11 +488,6 @@ interface ChutesModelEntry { const CACHE_TTL = 5 * 60 * 1000; -/** Clears the dynamic Chutes model discovery cache for tests. */ -export function clearChutesModelCacheForTests(): void { - clearLiveCatalogCacheForTests(); -} - async function fetchChutesModelRows(accessToken?: string): Promise { return await getCachedLiveProviderModelRows({ providerId: "chutes", diff --git a/extensions/clawrouter/usage.test.ts b/extensions/clawrouter/usage.test.ts index 4bb1371d450b..bda007650485 100644 --- a/extensions/clawrouter/usage.test.ts +++ b/extensions/clawrouter/usage.test.ts @@ -2,7 +2,7 @@ import { createServer, type Server } from "node:http"; import { connect, type AddressInfo } from "node:net"; import type { Duplex } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi, type MockedFunction } from "vitest"; -import { fetchClawRouterUsage, type ClawRouterUsageFetchGuard } from "./usage.js"; +import { fetchClawRouterUsage } from "./usage.js"; const runningServers: Server[] = []; const runningSockets = new Set(); @@ -107,6 +107,10 @@ afterEach(async () => { savedProxyEnv.clear(); }); +type ClawRouterUsageFetchGuard = NonNullable< + Parameters[0]["fetchGuard"] +>; + function mockFetchGuard(response: Response): MockedFunction { return vi.fn(async ({ url }) => ({ response, diff --git a/extensions/clawrouter/usage.ts b/extensions/clawrouter/usage.ts index b31ec66989ab..55fda3e5abaa 100644 --- a/extensions/clawrouter/usage.ts +++ b/extensions/clawrouter/usage.ts @@ -9,7 +9,7 @@ import { normalizeClawRouterRootUrl } from "./provider-catalog.js"; const CLAWROUTER_USAGE_RESPONSE_MAX_BYTES = 1024 * 1024; -export type ClawRouterUsageFetchGuard = typeof fetchWithSsrFGuard; +type ClawRouterUsageFetchGuard = typeof fetchWithSsrFGuard; type ClawRouterBudget = { configured?: unknown; diff --git a/extensions/comfy/comfy.live.test.ts b/extensions/comfy/comfy.live.test.ts index 3b4597fcbbb8..d40f9466d45e 100644 --- a/extensions/comfy/comfy.live.test.ts +++ b/extensions/comfy/comfy.live.test.ts @@ -6,7 +6,8 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot"; import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-env"; import { beforeAll, describe, expect, it } from "vitest"; import plugin from "./index.js"; -import { getComfyConfig, isComfyCapabilityConfigured } from "./workflow-runtime.js"; +import { getComfyConfigForTesting } from "./test-support.js"; +import { isComfyCapabilityConfigured } from "./workflow-runtime.js"; const LIVE = isLiveTestEnabled(["COMFY_LIVE_TEST"]) && (process.env.COMFY_LIVE_TEST ?? "").trim() === "1"; @@ -123,7 +124,7 @@ describeLive("comfy live", () => { ); it("documents the effective comfy config shape for live debugging", () => { - const comfyConfig = getComfyConfig(cfg as never); + const comfyConfig = getComfyConfigForTesting(cfg as never); expect(typeof comfyConfig).toBe("object"); }); }); diff --git a/extensions/comfy/image-generation-provider.test.ts b/extensions/comfy/image-generation-provider.test.ts index 763c89108bd1..cc3a0aa7f759 100644 --- a/extensions/comfy/image-generation-provider.test.ts +++ b/extensions/comfy/image-generation-provider.test.ts @@ -3,10 +3,7 @@ import type { LookupAddress } from "node:dns"; import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - setComfyFetchGuardForTesting, - buildComfyImageGenerationProvider, -} from "./image-generation-provider.js"; +import { buildComfyImageGenerationProvider } from "./image-generation-provider.js"; import { buildComfyConfig, buildLegacyComfyConfig, @@ -14,6 +11,7 @@ import { mockComfyProviderApiKey, parseComfyJsonBody, } from "./test-helpers.js"; +import { setComfyFetchGuardForTesting } from "./test-support.js"; const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ fetchWithSsrFGuardMock: vi.fn(), diff --git a/extensions/comfy/image-generation-provider.ts b/extensions/comfy/image-generation-provider.ts index e298df3994de..8cf23bf71d68 100644 --- a/extensions/comfy/image-generation-provider.ts +++ b/extensions/comfy/image-generation-provider.ts @@ -5,13 +5,10 @@ import type { } from "openclaw/plugin-sdk/image-generation"; import { DEFAULT_COMFY_MODEL, - setComfyFetchGuardForTesting, isComfyCapabilityConfigured, runComfyWorkflow, } from "./workflow-runtime.js"; -export { setComfyFetchGuardForTesting }; - export function buildComfyImageGenerationProvider(): ImageGenerationProvider { return { id: "comfy", diff --git a/extensions/comfy/music-generation-provider.test.ts b/extensions/comfy/music-generation-provider.test.ts index 102e7d8d0796..7e2d3eaeefe3 100644 --- a/extensions/comfy/music-generation-provider.test.ts +++ b/extensions/comfy/music-generation-provider.test.ts @@ -2,7 +2,7 @@ import { expectExplicitMusicGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts"; import { afterEach, describe, expect, it, vi } from "vitest"; import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js"; -import { setComfyFetchGuardForTesting } from "./workflow-runtime.js"; +import { setComfyFetchGuardForTesting } from "./test-support.js"; const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ fetchWithSsrFGuardMock: vi.fn(), diff --git a/extensions/comfy/test-support.ts b/extensions/comfy/test-support.ts new file mode 100644 index 000000000000..c66478d10da1 --- /dev/null +++ b/extensions/comfy/test-support.ts @@ -0,0 +1,22 @@ +import type { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; + +type ComfyTestApi = { + getConfig: (cfg?: unknown) => Record; + setFetchGuard: (impl: typeof fetchWithSsrFGuard | null) => void; +}; + +function getComfyTestApi(): ComfyTestApi { + const api = Reflect.get(globalThis, Symbol.for("openclaw.comfyTestApi")); + if (!api) { + throw new Error("Comfy test API is unavailable"); + } + return api as ComfyTestApi; +} + +export function setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { + getComfyTestApi().setFetchGuard(impl); +} + +export function getComfyConfigForTesting(cfg?: unknown): Record { + return getComfyTestApi().getConfig(cfg); +} diff --git a/extensions/comfy/video-generation-provider.test.ts b/extensions/comfy/video-generation-provider.test.ts index 1c6fdee5dc1c..1bb2bd7a25bf 100644 --- a/extensions/comfy/video-generation-provider.test.ts +++ b/extensions/comfy/video-generation-provider.test.ts @@ -7,10 +7,8 @@ import { mockComfyProviderApiKey, parseComfyJsonBody, } from "./test-helpers.js"; -import { - setComfyFetchGuardForTesting, - buildComfyVideoGenerationProvider, -} from "./video-generation-provider.js"; +import { setComfyFetchGuardForTesting } from "./test-support.js"; +import { buildComfyVideoGenerationProvider } from "./video-generation-provider.js"; const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ fetchWithSsrFGuardMock: vi.fn(), diff --git a/extensions/comfy/video-generation-provider.ts b/extensions/comfy/video-generation-provider.ts index 23f1dc1271dc..70cc75667151 100644 --- a/extensions/comfy/video-generation-provider.ts +++ b/extensions/comfy/video-generation-provider.ts @@ -6,13 +6,10 @@ import type { } from "openclaw/plugin-sdk/video-generation"; import { DEFAULT_COMFY_MODEL, - setComfyFetchGuardForTesting, isComfyCapabilityConfigured, runComfyWorkflow, } from "./workflow-runtime.js"; -export { setComfyFetchGuardForTesting }; - function toComfyInputImage(inputImage?: VideoGenerationSourceAsset) { if (!inputImage) { return undefined; diff --git a/extensions/comfy/workflow-runtime.test.ts b/extensions/comfy/workflow-runtime.test.ts index 7a030bb8046e..b02f9b069790 100644 --- a/extensions/comfy/workflow-runtime.test.ts +++ b/extensions/comfy/workflow-runtime.test.ts @@ -1,6 +1,7 @@ // Comfy tests cover workflow-runtime bounded-read delegation. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { readJsonResponseForTest, setComfyFetchGuardForTesting } from "./workflow-runtime.js"; +import { setComfyFetchGuardForTesting } from "./test-support.js"; +import { readJsonResponseForTest } from "./workflow-runtime.js"; describe("readJsonResponse bounded read (readProviderJsonResponse delegation)", () => { const fetchMock = vi.fn(); diff --git a/extensions/comfy/workflow-runtime.ts b/extensions/comfy/workflow-runtime.ts index d63f1610dbdd..2f82bab8cc8a 100644 --- a/extensions/comfy/workflow-runtime.ts +++ b/extensions/comfy/workflow-runtime.ts @@ -114,10 +114,17 @@ type ComfyWorkflowResult = { let comfyFetchGuard = fetchWithSsrFGuard; -export function setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { +function setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { comfyFetchGuard = impl ?? fetchWithSsrFGuard; } +if (process.env.VITEST === "true") { + Reflect.set(globalThis, Symbol.for("openclaw.comfyTestApi"), { + getConfig: getComfyConfig, + setFetchGuard: setComfyFetchGuardForTesting, + }); +} + function resolveComfyGeneratedOutputMaxBytes(params: { cfg: OpenClawConfig; capability: ComfyCapability; @@ -140,7 +147,7 @@ function readConfigInteger(config: ComfyProviderConfig, key: string): number | u return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined; } -export function getComfyConfig(cfg?: OpenClawConfig): ComfyProviderConfig { +function getComfyConfig(cfg?: OpenClawConfig): ComfyProviderConfig { const pluginConfig = cfg?.plugins?.entries?.comfy?.config; if (isRecord(pluginConfig)) { return pluginConfig; diff --git a/extensions/copilot/harness.test.ts b/extensions/copilot/harness.test.ts index b5fe14f8f453..993fb26a15ef 100644 --- a/extensions/copilot/harness.test.ts +++ b/extensions/copilot/harness.test.ts @@ -12,10 +12,9 @@ import { } from "openclaw/plugin-sdk/hook-runtime"; import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { CopilotClientPool } from "./harness.js"; import { createCopilotAgentHarness, type CopilotSessionBinding } from "./harness.js"; import type { resolvePoolAcquire } from "./src/attempt.js"; -import type { PoolKey } from "./src/runtime.js"; +import type { CopilotClientPool, PoolKey } from "./src/runtime.js"; const COPILOT_BYOK_PROVIDER_ERROR = "[copilot-attempt] BYOK requires an OpenAI-compatible or Anthropic model api and a non-empty baseUrl"; diff --git a/extensions/copilot/harness.ts b/extensions/copilot/harness.ts index d71c996e1cc3..c2eb859104df 100644 --- a/extensions/copilot/harness.ts +++ b/extensions/copilot/harness.ts @@ -31,8 +31,6 @@ import type { PoolKey, } from "./src/runtime.js"; -export type { CopilotClientPool, CopilotClientPoolOptions }; - const COPILOT_PROVIDER_IDS: ReadonlySet = new Set(["github-copilot"]); interface CreateCopilotAgentHarnessOptions { diff --git a/extensions/copilot/src/attempt.live.test.ts b/extensions/copilot/src/attempt.live.test.ts index 51b6ca57e2b3..8caaa2e349cf 100644 --- a/extensions/copilot/src/attempt.live.test.ts +++ b/extensions/copilot/src/attempt.live.test.ts @@ -6,7 +6,8 @@ import { CopilotClient, approveAll } from "@github/copilot-sdk"; import type { AgentHarnessAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime"; import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it, vi } from "vitest"; -import { createCopilotAgentHarness, type CopilotClientPool } from "../harness.js"; +import { createCopilotAgentHarness } from "../harness.js"; +import type { CopilotClientPool } from "./runtime.js"; const liveToolState = vi.hoisted(() => ({ calls: [] as string[], diff --git a/extensions/deepinfra/index.test.ts b/extensions/deepinfra/index.test.ts index 861e38b95277..87f84c75f4c9 100644 --- a/extensions/deepinfra/index.test.ts +++ b/extensions/deepinfra/index.test.ts @@ -3,14 +3,14 @@ import { createCapturedPluginRegistration, registerSingleProviderPlugin, } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared"; import { describe, expect, it, vi } from "vitest"; import deepinfraPlugin from "./index.js"; -import { - DEEPINFRA_MODEL_CATALOG, - DEEPINFRA_MODELS_URL, - resetDeepInfraModelCacheForTest, -} from "./provider-models.js"; +import { DEEPINFRA_MODEL_CATALOG } from "./provider-models.js"; + +const DEEPINFRA_MODELS_URL = + "https://api.deepinfra.com/v1/openai/models?sort_by=openclaw&filter=with_meta"; function buildSyntheticDeepInfraEntries(count: number) { return Array.from({ length: count }, (_unused, index) => ({ @@ -83,7 +83,7 @@ async function withLiveDiscoveryTestEnv( describe("deepinfra augmentModelCatalog", () => { it("returns the discovered (static under VITEST) catalog when nothing is configured", async () => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); const provider = await registerSingleProviderPlugin(deepinfraPlugin); const entries = (await provider.augmentModelCatalog?.({ entries: [] } as never)) ?? []; @@ -97,7 +97,7 @@ describe("deepinfra augmentModelCatalog", () => { }); it("preserves configured entries and appends discovered entries that are not already configured", async () => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); const provider = await registerSingleProviderPlugin(deepinfraPlugin); const entries = @@ -129,7 +129,7 @@ describe("deepinfra augmentModelCatalog", () => { }); it("uses config-backed API keys to enable live model catalog augmentation", async () => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); const mockFetch = vi .fn() .mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry("config/live-model")] })); @@ -157,7 +157,7 @@ describe("deepinfra augmentModelCatalog", () => { }); it("still runs live discovery when ctx.entries includes custom DeepInfra rows", async () => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); const mockFetch = vi .fn() .mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry("custom/live-model")] })); @@ -205,7 +205,7 @@ describe("deepinfra augmentModelCatalog", () => { }); it("still fetches when ctx.entries has exactly the static catalog length (static-fallback case)", async () => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); const provider = await registerSingleProviderPlugin(deepinfraPlugin); const entries = @@ -235,7 +235,7 @@ describe("deepinfra capability registration", () => { }); it("uses profile-resolved API keys for live text catalog discovery", async () => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); const mockFetch = vi.fn().mockResolvedValue(jsonResponse({ data: [makeAgentModelEntry()] })); const captured = createCapturedPluginRegistration(); deepinfraPlugin.register(captured.api); diff --git a/extensions/deepinfra/media-understanding-provider.test.ts b/extensions/deepinfra/media-understanding-provider.test.ts index a6f4e552ea9c..c4825f30bf9b 100644 --- a/extensions/deepinfra/media-understanding-provider.test.ts +++ b/extensions/deepinfra/media-understanding-provider.test.ts @@ -4,10 +4,7 @@ import { describeImageWithModel, } from "openclaw/plugin-sdk/media-understanding"; import { afterAll, describe, expect, it, vi } from "vitest"; -import { - deepinfraMediaUnderstandingProvider, - transcribeDeepInfraAudio, -} from "./media-understanding-provider.js"; +import { deepinfraMediaUnderstandingProvider } from "./media-understanding-provider.js"; const { transcribeOpenAiCompatibleAudioMock } = vi.hoisted(() => ({ transcribeOpenAiCompatibleAudioMock: vi.fn(async () => ({ text: "hello", model: "whisper" })), @@ -41,7 +38,7 @@ describe("deepinfra media understanding provider", () => { image: 45, audio: 45, }, - transcribeAudio: transcribeDeepInfraAudio, + transcribeAudio: expect.any(Function), describeImage: describeImageWithModel, describeImages: describeImagesWithModel, }); @@ -49,7 +46,7 @@ describe("deepinfra media understanding provider", () => { it("routes audio transcription through the OpenAI-compatible DeepInfra endpoint", async () => { const buffer = Buffer.from("audio"); - const result = await transcribeDeepInfraAudio({ + const result = await deepinfraMediaUnderstandingProvider.transcribeAudio!({ buffer, fileName: "clip.mp3", apiKey: "deepinfra-key", diff --git a/extensions/deepinfra/media-understanding-provider.ts b/extensions/deepinfra/media-understanding-provider.ts index 517804cc7ab5..ad4183868645 100644 --- a/extensions/deepinfra/media-understanding-provider.ts +++ b/extensions/deepinfra/media-understanding-provider.ts @@ -21,7 +21,7 @@ function resolveDefault( return first ?? fallback[0] ?? ""; } -export async function transcribeDeepInfraAudio(params: AudioTranscriptionRequest) { +async function transcribeDeepInfraAudio(params: AudioTranscriptionRequest) { return await transcribeOpenAiCompatibleAudio({ ...params, provider: "deepinfra", diff --git a/extensions/deepinfra/onboard.test.ts b/extensions/deepinfra/onboard.test.ts index 348515397568..4a5360636ceb 100644 --- a/extensions/deepinfra/onboard.test.ts +++ b/extensions/deepinfra/onboard.test.ts @@ -9,12 +9,8 @@ import { } from "openclaw/plugin-sdk/provider-onboard"; import { captureEnv } from "openclaw/plugin-sdk/test-env"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - applyDeepInfraConfig, - DEEPINFRA_BASE_URL, - DEEPINFRA_DEFAULT_MODEL_REF, -} from "./onboard.js"; -import { DEEPINFRA_DEFAULT_MODEL_ID } from "./provider-models.js"; +import { applyDeepInfraConfig } from "./onboard.js"; +import { DEEPINFRA_BASE_URL, DEEPINFRA_DEFAULT_MODEL_REF } from "./provider-models.js"; const { resolveEnvApiKey } = providerAuth; @@ -29,10 +25,6 @@ describe("DeepInfra provider config", () => { it("DEEPINFRA_DEFAULT_MODEL_REF includes provider prefix", () => { expect(DEEPINFRA_DEFAULT_MODEL_REF).toBe("deepinfra/deepseek-ai/DeepSeek-V4-Flash"); }); - - it("DEEPINFRA_DEFAULT_MODEL_ID is deepseek-ai/DeepSeek-V4-Flash", () => { - expect(DEEPINFRA_DEFAULT_MODEL_ID).toBe("deepseek-ai/DeepSeek-V4-Flash"); - }); }); describe("applyDeepInfraConfig", () => { diff --git a/extensions/deepinfra/onboard.ts b/extensions/deepinfra/onboard.ts index d90c2ea67e84..a8319ac0624a 100644 --- a/extensions/deepinfra/onboard.ts +++ b/extensions/deepinfra/onboard.ts @@ -3,9 +3,7 @@ import { applyAgentDefaultModelPrimary, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; -import { DEEPINFRA_BASE_URL, DEEPINFRA_DEFAULT_MODEL_REF } from "./provider-models.js"; - -export { DEEPINFRA_BASE_URL, DEEPINFRA_DEFAULT_MODEL_REF }; +import { DEEPINFRA_DEFAULT_MODEL_REF } from "./provider-models.js"; export function applyDeepInfraConfig( cfg: OpenClawConfig, diff --git a/extensions/deepinfra/provider-models.test.ts b/extensions/deepinfra/provider-models.test.ts index 09df5ebc48f3..1d0253698c94 100644 --- a/extensions/deepinfra/provider-models.test.ts +++ b/extensions/deepinfra/provider-models.test.ts @@ -1,3 +1,4 @@ +import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; // Deepinfra tests cover provider models plugin behavior. import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -13,17 +14,18 @@ vi.mock("openclaw/plugin-sdk/provider-auth", async () => { }); import { - DEEPINFRA_MODELS_URL, DEEPINFRA_DEFAULT_MODEL_REF, DEEPINFRA_MODEL_CATALOG, discoverDeepInfraModels, discoverDeepInfraSurfaces, hasDeepInfraApiKey, - resetDeepInfraModelCacheForTest, } from "./provider-models.js"; +const DEEPINFRA_MODELS_URL = + "https://api.deepinfra.com/v1/openai/models?sort_by=openclaw&filter=with_meta"; + beforeEach(() => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); isProviderApiKeyConfiguredMock.mockReset(); isProviderApiKeyConfiguredMock.mockReturnValue(false); }); diff --git a/extensions/deepinfra/provider-models.ts b/extensions/deepinfra/provider-models.ts index a8f4f9025d99..dec4f1b7d075 100644 --- a/extensions/deepinfra/provider-models.ts +++ b/extensions/deepinfra/provider-models.ts @@ -1,7 +1,6 @@ // Deepinfra provider module implements model/runtime integration. import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth"; import { - clearLiveCatalogCacheForTests, getCachedLiveProviderModelRows, LiveModelCatalogHttpError, } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; @@ -20,9 +19,9 @@ const DEEPINFRA_MANIFEST_PROVIDER = buildManifestModelProviderConfig({ }); export const DEEPINFRA_BASE_URL = DEEPINFRA_MANIFEST_PROVIDER.baseUrl; -export const DEEPINFRA_MODELS_URL = `${DEEPINFRA_BASE_URL}/models?sort_by=openclaw&filter=with_meta`; +const DEEPINFRA_MODELS_URL = `${DEEPINFRA_BASE_URL}/models?sort_by=openclaw&filter=with_meta`; -export const DEEPINFRA_DEFAULT_MODEL_ID = "deepseek-ai/DeepSeek-V4-Flash"; +const DEEPINFRA_DEFAULT_MODEL_ID = "deepseek-ai/DeepSeek-V4-Flash"; export const DEEPINFRA_DEFAULT_MODEL_REF = `deepinfra/${DEEPINFRA_DEFAULT_MODEL_ID}`; const DEEPINFRA_DEFAULT_CONTEXT_WINDOW = 128000; @@ -97,10 +96,6 @@ interface DeepInfraDiscoveredCatalog { live: boolean; } -export function resetDeepInfraModelCacheForTest(): void { - clearLiveCatalogCacheForTests(); -} - const SURFACE_FOR_TAG: Record = { chat: "chat", vlm: "vlm", diff --git a/extensions/deepinfra/surface-model-catalogs.test.ts b/extensions/deepinfra/surface-model-catalogs.test.ts index 06f865a829b1..28976ba60b40 100644 --- a/extensions/deepinfra/surface-model-catalogs.test.ts +++ b/extensions/deepinfra/surface-model-catalogs.test.ts @@ -1,6 +1,6 @@ +import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; // Deepinfra tests cover surface model catalogs plugin behavior. import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resetDeepInfraModelCacheForTest } from "./provider-models.js"; import { listDeepInfraImageGenCatalog, listDeepInfraVideoGenCatalog, @@ -8,7 +8,7 @@ import { } from "./surface-model-catalogs.js"; beforeEach(() => { - resetDeepInfraModelCacheForTest(); + clearLiveCatalogCacheForTests(); }); function makeCtx(overrides: Partial[0]> = {}) { diff --git a/extensions/discord/runtime-api.threads.ts b/extensions/discord/runtime-api.threads.ts index 346559cf9ca2..9b4061f72523 100644 --- a/extensions/discord/runtime-api.threads.ts +++ b/extensions/discord/runtime-api.threads.ts @@ -1,6 +1,5 @@ // Discord plugin module implements runtime api.threads behavior. export { - testing as __testing, testing, autoBindSpawnedDiscordSubagent, createNoopThreadBindingManager, diff --git a/extensions/fal/image-generation-provider.test.ts b/extensions/fal/image-generation-provider.test.ts index 4350aa457564..36c1c038e21a 100644 --- a/extensions/fal/image-generation-provider.test.ts +++ b/extensions/fal/image-generation-provider.test.ts @@ -6,10 +6,8 @@ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ fetchWithSsrFGuardMock: vi.fn(), })); -import { - setFalFetchGuardForTesting, - buildFalImageGenerationProvider, -} from "./image-generation-provider.js"; +import { buildFalImageGenerationProvider } from "./image-generation-provider.js"; +import { setFalFetchGuardForTesting } from "./test-support.js"; function expectFalJsonPost(params: { call: number; url: string; body: Record }) { const request = fetchWithSsrFGuardMock.mock.calls[params.call - 1]?.[0]; diff --git a/extensions/fal/image-generation-provider.ts b/extensions/fal/image-generation-provider.ts index 37f27ccbff17..2b846ae13b81 100644 --- a/extensions/fal/image-generation-provider.ts +++ b/extensions/fal/image-generation-provider.ts @@ -155,10 +155,16 @@ type FalNetworkPolicy = { let falFetchGuard = fetchWithSsrFGuard; -export function setFalFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { +function setFalFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { falFetchGuard = impl ?? fetchWithSsrFGuard; } +if (process.env.VITEST === "true") { + const key = Symbol.for("openclaw.falTestApi"); + const api = (Reflect.get(globalThis, key) as Record | undefined) ?? {}; + Reflect.set(globalThis, key, { ...api, setImageFetchGuard: setFalFetchGuardForTesting }); +} + function matchesTrustedHostSuffix(hostname: string, trustedSuffix: string): boolean { const normalizedHost = normalizeLowercaseStringOrEmpty(hostname); const normalizedSuffix = normalizeLowercaseStringOrEmpty(trustedSuffix); diff --git a/extensions/fal/test-support.ts b/extensions/fal/test-support.ts new file mode 100644 index 000000000000..b1a9bd6673b7 --- /dev/null +++ b/extensions/fal/test-support.ts @@ -0,0 +1,22 @@ +import type { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; + +type FalTestApi = { + setImageFetchGuard: (impl: typeof fetchWithSsrFGuard | null) => void; + setVideoFetchGuard: (impl: typeof fetchWithSsrFGuard | null) => void; +}; + +function getFalTestApi(): FalTestApi { + const api = Reflect.get(globalThis, Symbol.for("openclaw.falTestApi")); + if (!api) { + throw new Error("Fal test API is unavailable"); + } + return api as FalTestApi; +} + +export function setFalFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { + getFalTestApi().setImageFetchGuard(impl); +} + +export function setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { + getFalTestApi().setVideoFetchGuard(impl); +} diff --git a/extensions/fal/video-generation-provider.test.ts b/extensions/fal/video-generation-provider.test.ts index 5fcba5da0b4f..6a753eb021c7 100644 --- a/extensions/fal/video-generation-provider.test.ts +++ b/extensions/fal/video-generation-provider.test.ts @@ -4,10 +4,8 @@ import * as providerAuth from "openclaw/plugin-sdk/provider-auth-runtime"; import * as providerHttp from "openclaw/plugin-sdk/provider-http"; import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - setFalVideoFetchGuardForTesting, - buildFalVideoGenerationProvider, -} from "./video-generation-provider.js"; +import { setFalVideoFetchGuardForTesting } from "./test-support.js"; +import { buildFalVideoGenerationProvider } from "./video-generation-provider.js"; function createMockRequestConfig() { return {} as ReturnType["requestConfig"]; diff --git a/extensions/fal/video-generation-provider.ts b/extensions/fal/video-generation-provider.ts index c7186445239e..0697762035d5 100644 --- a/extensions/fal/video-generation-provider.ts +++ b/extensions/fal/video-generation-provider.ts @@ -99,10 +99,16 @@ type FalQueueResponse = { let falFetchGuard = fetchWithSsrFGuard; -export function setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { +function setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void { falFetchGuard = impl ?? fetchWithSsrFGuard; } +if (process.env.VITEST === "true") { + const key = Symbol.for("openclaw.falTestApi"); + const api = (Reflect.get(globalThis, key) as Record | undefined) ?? {}; + Reflect.set(globalThis, key, { ...api, setVideoFetchGuard: setFalVideoFetchGuardForTesting }); +} + function normalizeFalVideoUrl(value: unknown): string | undefined { const normalized = normalizeOptionalString(value); if (!normalized && value !== undefined && value !== null) { diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts index 3beffbafee19..e7200f27789f 100644 --- a/extensions/memory-lancedb/index.test.ts +++ b/extensions/memory-lancedb/index.test.ts @@ -35,7 +35,7 @@ import memoryPlugin, { shouldCapture, testing, } from "./index.js"; -import { createLanceDbRuntimeLoader } from "./lancedb-runtime.js"; +import { createLanceDbRuntimeLoader } from "./lancedb-runtime.test-support.js"; import { installTmpDirHarness } from "./test-helpers.js"; const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "test-key"; diff --git a/extensions/memory-lancedb/lancedb-runtime.test-support.ts b/extensions/memory-lancedb/lancedb-runtime.test-support.ts new file mode 100644 index 000000000000..839811abeac0 --- /dev/null +++ b/extensions/memory-lancedb/lancedb-runtime.test-support.ts @@ -0,0 +1,16 @@ +type LanceDbModule = typeof import("@lancedb/lancedb"); + +type LanceDbRuntimeTestApi = { + createRuntimeLoader: (overrides?: { + platform?: NodeJS.Platform; + arch?: NodeJS.Architecture; + importBundled?: () => Promise; + }) => { load: () => Promise }; +}; + +const api = Reflect.get(globalThis, Symbol.for("openclaw.memoryLanceDbRuntimeTestApi")); +if (!api) { + throw new Error("Memory LanceDB runtime test API is unavailable"); +} + +export const createLanceDbRuntimeLoader = (api as LanceDbRuntimeTestApi).createRuntimeLoader; diff --git a/extensions/memory-lancedb/lancedb-runtime.ts b/extensions/memory-lancedb/lancedb-runtime.ts index 4eae6fac538b..b84af4a6a90d 100644 --- a/extensions/memory-lancedb/lancedb-runtime.ts +++ b/extensions/memory-lancedb/lancedb-runtime.ts @@ -1,7 +1,7 @@ // Memory Lancedb plugin module implements lancedb runtime behavior. type LanceDbModule = typeof import("@lancedb/lancedb"); -export type LanceDbRuntimeLogger = { +type LanceDbRuntimeLogger = { info?: (message: string) => void; warn?: (message: string) => void; }; @@ -38,7 +38,7 @@ function buildUnsupportedNativePlatformMessage(params: { ].join(" "); } -export function createLanceDbRuntimeLoader(overrides: Partial = {}): { +function createLanceDbRuntimeLoader(overrides: Partial = {}): { load: (loggerInstance?: LanceDbRuntimeLogger) => Promise; } { const deps: LanceDbRuntimeLoaderDeps = { @@ -71,6 +71,12 @@ export function createLanceDbRuntimeLoader(overrides: Partial { diff --git a/extensions/microsoft-foundry/auth.ts b/extensions/microsoft-foundry/auth.ts index b4af568c8878..4882f2d333c5 100644 --- a/extensions/microsoft-foundry/auth.ts +++ b/extensions/microsoft-foundry/auth.ts @@ -35,7 +35,7 @@ import { resolveFoundryApi, } from "./shared.js"; -export function shouldTestFoundryTextConnection(params: { +function shouldTestFoundryTextConnection(params: { modelId: string; modelNameHint?: string | null; }): boolean { @@ -44,6 +44,12 @@ export function shouldTestFoundryTextConnection(params: { ); } +if (process.env.VITEST === "true") { + const key = Symbol.for("openclaw.microsoftFoundryTestApi"); + const api = (Reflect.get(globalThis, key) as Record | undefined) ?? {}; + Reflect.set(globalThis, key, { ...api, shouldTestFoundryTextConnection }); +} + export const entraIdAuthMethod: ProviderAuthMethod = { id: "entra-id", label: "Entra ID (az login)", diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index d008a747ff53..0f16fbdbc07b 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -3,33 +3,37 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { shouldTestFoundryTextConnection } from "./auth.js"; import { getAccessTokenResultAsync } from "./cli.js"; import plugin from "./index.js"; import { - buildFoundryConnectionTest, - isValidTenantIdentifier, promptApiKeyEndpointAndModel, promptEndpointAndModelManually, selectFoundryDeployment, } from "./onboard.js"; -import { resetFoundryRuntimeAuthCaches } from "./runtime.js"; import { COGNITIVE_SERVICES_RESOURCE, FOUNDRY_ANTHROPIC_SCOPE, buildFoundryAuthResult, extractFoundryEndpoint, formatFoundryApiLabel, - isAnthropicFoundryDeployment, isFoundryMaiImageModel, normalizeFoundryEndpoint, requiresFoundryMaxCompletionTokens, requiresFoundryEntraIdClaudeAuth, - supportsFoundryReasoningContent, - supportsFoundryReasoningEffort, - supportsFoundryImageInput, usesFoundryResponsesByDefault, } from "./shared.js"; +import { microsoftFoundryTesting } from "./test-support.js"; + +const { + buildFoundryConnectionTest, + isAnthropicFoundryDeployment, + isValidTenantIdentifier, + resetFoundryRuntimeAuthCaches, + shouldTestFoundryTextConnection, + supportsFoundryImageInput, + supportsFoundryReasoningContent, + supportsFoundryReasoningEffort, +} = microsoftFoundryTesting; const execFileMock = vi.hoisted(() => vi.fn()); const execFileSyncMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index 6dee8dead522..f63e754f7f6a 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -426,7 +426,7 @@ export async function promptApiKeyEndpointAndModel( }); } -export function buildFoundryConnectionTest(params: { +function buildFoundryConnectionTest(params: { endpoint: string; modelId: string; modelNameHint?: string | null; @@ -489,7 +489,7 @@ function extractTenantSuggestions(rawMessage: string): Array<{ id: string; label return suggestions; } -export function isValidTenantIdentifier(value: string): boolean { +function isValidTenantIdentifier(value: string): boolean { const trimmed = normalizeOptionalString(value) ?? ""; if (!trimmed) { return false; @@ -503,6 +503,12 @@ export function isValidTenantIdentifier(value: string): boolean { return isTenantUuid || isTenantDomain; } +if (process.env.VITEST === "true") { + const key = Symbol.for("openclaw.microsoftFoundryTestApi"); + const api = (Reflect.get(globalThis, key) as Record | undefined) ?? {}; + Reflect.set(globalThis, key, { ...api, buildFoundryConnectionTest, isValidTenantIdentifier }); +} + export async function promptTenantId( ctx: ProviderAuthContext, params?: { diff --git a/extensions/microsoft-foundry/runtime.ts b/extensions/microsoft-foundry/runtime.ts index 4b9a45c0cf02..3d61534e3285 100644 --- a/extensions/microsoft-foundry/runtime.ts +++ b/extensions/microsoft-foundry/runtime.ts @@ -28,11 +28,17 @@ const cachedTokens = new Map(); const refreshPromises = new Map>(); const FOUNDRY_TOKEN_FALLBACK_LIFETIME_MS = 55 * 60 * 1000; -export function resetFoundryRuntimeAuthCaches(): void { +function resetFoundryRuntimeAuthCaches(): void { cachedTokens.clear(); refreshPromises.clear(); } +if (process.env.VITEST === "true") { + const key = Symbol.for("openclaw.microsoftFoundryTestApi"); + const api = (Reflect.get(globalThis, key) as Record | undefined) ?? {}; + Reflect.set(globalThis, key, { ...api, resetFoundryRuntimeAuthCaches }); +} + async function refreshEntraToken(params?: { scope?: string; subscriptionId?: string; diff --git a/extensions/microsoft-foundry/shared.ts b/extensions/microsoft-foundry/shared.ts index fc33b05210f8..9dc2312762f4 100644 --- a/extensions/microsoft-foundry/shared.ts +++ b/extensions/microsoft-foundry/shared.ts @@ -144,7 +144,7 @@ function normalizeFoundryModelName(value?: string | null): string | undefined { return trimmed || undefined; } -export function isAnthropicFoundryDeployment(modelName?: string | null): boolean { +function isAnthropicFoundryDeployment(modelName?: string | null): boolean { const normalized = normalizeFoundryModelName(modelName); return normalized ? normalized.startsWith("claude") : false; } @@ -182,12 +182,12 @@ export function isFoundryMaiImageModel(value?: string | null): boolean { ); } -export function supportsFoundryReasoningContent(value?: string | null): boolean { +function supportsFoundryReasoningContent(value?: string | null): boolean { const normalized = normalizeFoundryModelName(value); return normalized === "mai-ds-r1" || normalized === "mai-thinking-1"; } -export function supportsFoundryImageInput(value?: string | null): boolean { +function supportsFoundryImageInput(value?: string | null): boolean { const normalized = normalizeFoundryModelName(value); if (!normalized) { return false; @@ -267,7 +267,7 @@ export function requiresFoundryMaxCompletionTokens(value?: string | null): boole ); } -export function supportsFoundryReasoningEffort(value?: string | null): boolean { +function supportsFoundryReasoningEffort(value?: string | null): boolean { const normalized = normalizeFoundryModelName(value); if ( !normalized || @@ -284,6 +284,18 @@ export function supportsFoundryReasoningEffort(value?: string | null): boolean { ); } +if (process.env.VITEST === "true") { + const key = Symbol.for("openclaw.microsoftFoundryTestApi"); + const api = (Reflect.get(globalThis, key) as Record | undefined) ?? {}; + Reflect.set(globalThis, key, { + ...api, + isAnthropicFoundryDeployment, + supportsFoundryImageInput, + supportsFoundryReasoningContent, + supportsFoundryReasoningEffort, + }); +} + function resolveFoundryReasoningEfforts(value?: string | null): string[] | undefined { const normalized = normalizeFoundryModelName(value); if (!normalized || !supportsFoundryReasoningEffort(normalized)) { diff --git a/extensions/microsoft-foundry/test-support.ts b/extensions/microsoft-foundry/test-support.ts new file mode 100644 index 000000000000..452416cb3f54 --- /dev/null +++ b/extensions/microsoft-foundry/test-support.ts @@ -0,0 +1,27 @@ +import type { FoundryProviderApi } from "./shared.js"; + +type MicrosoftFoundryTestApi = { + buildFoundryConnectionTest: (params: { + endpoint: string; + modelId: string; + modelNameHint?: string | null; + api: FoundryProviderApi; + }) => { url: string; body: Record }; + isAnthropicFoundryDeployment: (value?: string | null) => boolean; + isValidTenantIdentifier: (value: string) => boolean; + resetFoundryRuntimeAuthCaches: () => void; + shouldTestFoundryTextConnection: (params: { + modelId: string; + modelNameHint?: string | null; + }) => boolean; + supportsFoundryImageInput: (value?: string | null) => boolean; + supportsFoundryReasoningContent: (value?: string | null) => boolean; + supportsFoundryReasoningEffort: (value?: string | null) => boolean; +}; + +const api = Reflect.get(globalThis, Symbol.for("openclaw.microsoftFoundryTestApi")); +if (!api) { + throw new Error("Microsoft Foundry test API is unavailable"); +} + +export const microsoftFoundryTesting = api as MicrosoftFoundryTestApi; diff --git a/extensions/migrate-claude/source.ts b/extensions/migrate-claude/source.ts index 3e4fb734afb1..1b9426af21f1 100644 --- a/extensions/migrate-claude/source.ts +++ b/extensions/migrate-claude/source.ts @@ -12,7 +12,7 @@ type ClaudeArchivePath = { relativePath: string; }; -export type ClaudeAutoMemorySource = { +type ClaudeAutoMemorySource = { id: string; label: string; path: string; diff --git a/extensions/migrate-hermes/auth-config.ts b/extensions/migrate-hermes/auth-config.ts index 7dd0146dcc48..6b2ce2bce4d4 100644 --- a/extensions/migrate-hermes/auth-config.ts +++ b/extensions/migrate-hermes/auth-config.ts @@ -10,7 +10,7 @@ export type HermesAuthProfileConfig = { displayName?: string; }; -export type HermesAuthConfigApplyResult = "configured" | "conflict" | "unavailable"; +type HermesAuthConfigApplyResult = "configured" | "conflict" | "unavailable"; class HermesAuthConfigConflict extends Error {} diff --git a/extensions/migrate-hermes/config-providers.ts b/extensions/migrate-hermes/config-providers.ts index b732bdf66858..8743e58e2cc9 100644 --- a/extensions/migrate-hermes/config-providers.ts +++ b/extensions/migrate-hermes/config-providers.ts @@ -18,7 +18,7 @@ import { import { childRecord, isRecord, readString, sanitizeName } from "./helpers.js"; import { normalizeHermesCustomProviderId, resolveHermesConfiguredProviderId } from "./model.js"; -export type HermesProviderSecretBinding = { +type HermesProviderSecretBinding = { envVar: string; provider: string; }; diff --git a/extensions/migrate-hermes/files-and-skills.test.ts b/extensions/migrate-hermes/files-and-skills.test.ts index 5104ec8c3b6b..535518181fec 100644 --- a/extensions/migrate-hermes/files-and-skills.test.ts +++ b/extensions/migrate-hermes/files-and-skills.test.ts @@ -7,7 +7,7 @@ import { MIGRATION_REASON_TARGET_EXISTS } from "openclaw/plugin-sdk/migration"; import { afterEach, describe, expect, it, vi } from "vitest"; import { buildAuthItems } from "./auth.js"; import { buildHermesMigrationProvider } from "./provider.js"; -import { discoverHermesSource, resolveImplicitHermesRoot } from "./source.js"; +import { discoverHermesSource } from "./source.js"; import { resolveTargets } from "./targets.js"; import { cleanupTempRoots, makeContext, makeTempRoot, writeFile } from "./test/provider-helpers.js"; @@ -95,14 +95,23 @@ describe("Hermes migration file and skill items", () => { await writeFile(path.join(defaultRoot, "active_profile"), "coder\n"); await writeFile(path.join(profileRoot, "config.yaml"), "model: openai/gpt-5.6\n"); - expect(await resolveImplicitHermesRoot({ HERMES_HOME: defaultRoot }, "darwin")).toBe( - defaultRoot, - ); - expect(await resolveImplicitHermesRoot({ HOME: home }, "darwin")).toBe(profileRoot); + expect( + ( + await discoverHermesSource(undefined, { + env: { HERMES_HOME: defaultRoot }, + platform: "darwin", + }) + ).root, + ).toBe(defaultRoot); + expect( + (await discoverHermesSource(undefined, { env: { HOME: home }, platform: "darwin" })).root, + ).toBe(profileRoot); expect((await discoverHermesSource(profileRoot)).root).toBe(profileRoot); await writeFile(path.join(defaultRoot, "active_profile"), "../escape\n"); - expect(await resolveImplicitHermesRoot({ HOME: home }, "darwin")).toBe(defaultRoot); + expect( + (await discoverHermesSource(undefined, { env: { HOME: home }, platform: "darwin" })).root, + ).toBe(defaultRoot); const windowsHome = path.join(root, "windows-home"); const localAppData = path.join(windowsHome, "AppData", "Local"); @@ -110,17 +119,21 @@ describe("Hermes migration file and skill items", () => { const legacyWindowsRoot = path.join(windowsHome, ".hermes"); await writeFile(path.join(legacyWindowsRoot, "config.yaml"), "model: legacy\n"); expect( - await resolveImplicitHermesRoot( - { LOCALAPPDATA: localAppData, USERPROFILE: windowsHome }, - "win32", - ), + ( + await discoverHermesSource(undefined, { + env: { LOCALAPPDATA: localAppData, USERPROFILE: windowsHome }, + platform: "win32", + }) + ).root, ).toBe(legacyWindowsRoot); await writeFile(path.join(nativeWindowsRoot, "config.yaml"), "model: current\n"); expect( - await resolveImplicitHermesRoot( - { LOCALAPPDATA: localAppData, USERPROFILE: windowsHome }, - "win32", - ), + ( + await discoverHermesSource(undefined, { + env: { LOCALAPPDATA: localAppData, USERPROFILE: windowsHome }, + platform: "win32", + }) + ).root, ).toBe(nativeWindowsRoot); const personaHome = path.join(root, "persona-home"); @@ -128,10 +141,12 @@ describe("Hermes migration file and skill items", () => { const personaLegacyRoot = path.join(personaHome, ".hermes"); await writeFile(path.join(personaLegacyRoot, "SOUL.md"), "Legacy persona\n"); expect( - await resolveImplicitHermesRoot( - { LOCALAPPDATA: personaLocalAppData, USERPROFILE: personaHome }, - "win32", - ), + ( + await discoverHermesSource(undefined, { + env: { LOCALAPPDATA: personaLocalAppData, USERPROFILE: personaHome }, + platform: "win32", + }) + ).root, ).toBe(personaLegacyRoot); }); diff --git a/extensions/migrate-hermes/source.ts b/extensions/migrate-hermes/source.ts index 77f94082088f..c31350b55ed3 100644 --- a/extensions/migrate-hermes/source.ts +++ b/extensions/migrate-hermes/source.ts @@ -179,7 +179,7 @@ export async function discoverHermesSource( }; } -export async function resolveImplicitHermesRoot( +async function resolveImplicitHermesRoot( env: NodeJS.ProcessEnv, platform: NodeJS.Platform, ): Promise { diff --git a/extensions/qa-lab/src/model-selection.ts b/extensions/qa-lab/src/model-selection.ts index 9772ceafb794..4d10f11709e7 100644 --- a/extensions/qa-lab/src/model-selection.ts +++ b/extensions/qa-lab/src/model-selection.ts @@ -7,7 +7,7 @@ import { export type { QaProviderMode, QaProviderModeInput } from "./providers/index.js"; -export type QaModelSelection = { +type QaModelSelection = { primaryModel: string; alternateModel: string; }; diff --git a/extensions/voyage/embedding-batch.test.ts b/extensions/voyage/embedding-batch.test.ts index b469627da86b..8c3fbe8b12d1 100644 --- a/extensions/voyage/embedding-batch.test.ts +++ b/extensions/voyage/embedding-batch.test.ts @@ -1,7 +1,8 @@ // Voyage batch tests cover bounded status/error response reads. import { describe, expect, it } from "vitest"; -import { runVoyageEmbeddingBatches, testing } from "./embedding-batch.js"; +import { runVoyageEmbeddingBatches } from "./embedding-batch.js"; import type { VoyageEmbeddingClient } from "./embedding-provider.js"; +import { voyageEmbeddingBatchTesting as testing } from "./test-support.js"; const { fetchVoyageBatchStatus, readVoyageBatchError, VOYAGE_BATCH_RESPONSE_MAX_BYTES } = testing; diff --git a/extensions/voyage/embedding-batch.ts b/extensions/voyage/embedding-batch.ts index 39687c0ffd60..30e990207205 100644 --- a/extensions/voyage/embedding-batch.ts +++ b/extensions/voyage/embedding-batch.ts @@ -339,8 +339,12 @@ export async function runVoyageEmbeddingBatches( }); } -export const testing = { +const testing = { fetchVoyageBatchStatus, readVoyageBatchError, VOYAGE_BATCH_RESPONSE_MAX_BYTES, } as const; + +if (process.env.VITEST === "true") { + Reflect.set(globalThis, Symbol.for("openclaw.voyageEmbeddingBatchTestApi"), testing); +} diff --git a/extensions/voyage/test-support.ts b/extensions/voyage/test-support.ts new file mode 100644 index 000000000000..01a148fca818 --- /dev/null +++ b/extensions/voyage/test-support.ts @@ -0,0 +1,22 @@ +import type { VoyageEmbeddingClient } from "./embedding-provider.js"; + +type VoyageBatchTestParams = { + client: VoyageEmbeddingClient; + deps: Record; + maxResponseBytes?: number; +}; + +type VoyageEmbeddingBatchTestApi = { + fetchVoyageBatchStatus: (params: VoyageBatchTestParams & { batchId: string }) => Promise; + readVoyageBatchError: ( + params: VoyageBatchTestParams & { errorFileId: string }, + ) => Promise; + VOYAGE_BATCH_RESPONSE_MAX_BYTES: number; +}; + +const api = Reflect.get(globalThis, Symbol.for("openclaw.voyageEmbeddingBatchTestApi")); +if (!api) { + throw new Error("Voyage embedding batch test API is unavailable"); +} + +export const voyageEmbeddingBatchTesting = api as VoyageEmbeddingBatchTestApi; diff --git a/test/scripts/check-deadcode-exports.test.ts b/test/scripts/check-deadcode-exports.test.ts index 2396fc2b5956..4250807bc91b 100644 --- a/test/scripts/check-deadcode-exports.test.ts +++ b/test/scripts/check-deadcode-exports.test.ts @@ -40,56 +40,46 @@ describe("check-deadcode-exports", () => { expect(knipConfig.workspaces["."].entry).toContain("src/mcp/openclaw-tools-serve.ts!"); }); - it.each([ - "acpx", - "amazon-bedrock-mantle", - "azure-speech", - "cloudflare-ai-gateway", - "cohere", - "deepgram", - "elevenlabs", - "featherless", - "fireworks", - "google", - "huggingface", - "kilocode", - "kimi-coding", - "lmstudio", - "microsoft", - "minimax", - "mistral", - "moonshot", - "nvidia", - "pixverse", - "qianfan", - "qwen", - "senseaudio", - "tavily", - "tencent", - "vllm", - "xiaomi", - "xai", - ])("removes the bundled-plugin root catch-all from migrated %s workspace", (pluginId) => { - const workspace = ( - knipConfig.workspaces as Record - )[`extensions/${pluginId}`]; - if (!workspace) { - throw new Error(`missing Knip workspace for ${pluginId}`); + it("keeps every migrated bundled-plugin workspace strict", () => { + const extensionWorkspaces = Object.entries(knipConfig.workspaces).filter( + ([workspace]) => + workspace.startsWith("extensions/") && + !["extensions/*", "extensions/llama-cpp", "extensions/reef"].includes(workspace), + ); + expect(extensionWorkspaces.length).toBeGreaterThan(1); + for (const [workspace, settings] of extensionWorkspaces) { + expect(settings.entry, workspace).not.toContain("*.ts!"); + expect(settings.project, workspace).toContain("*.ts!"); + expect(settings.entry, workspace).toEqual( + expect.arrayContaining([ + "index.ts!", + "setup-entry.ts!", + "*-api.ts!", + "cli-metadata.ts!", + "channel-entry.ts!", + "provider-discovery.ts!", + "{web-search,web-fetch}-provider.ts!", + ]), + ); } - const entries = workspace.entry; - expect(entries).not.toContain("*.ts!"); - expect(entries).toEqual( + expect(knipConfig.workspaces["extensions/*"].entry).toContain("*.ts!"); + expect(knipConfig.workspaces["extensions/*"].project).toContain("*.ts!"); + expect(knipConfig.workspaces["extensions/llama-cpp"].entry).toContain("*.ts!"); + expect(knipConfig.workspaces["extensions/reef"].entry).toContain("*.ts!"); + }); + + it("models the Browser facades loaded by basename", () => { + const workspace = knipConfig.workspaces["extensions/browser"]; + expect(workspace.entry).toEqual( expect.arrayContaining([ - "index.ts!", - "setup-entry.ts!", - "*-api.ts!", - "cli-metadata.ts!", - "channel-entry.ts!", - "provider-discovery.ts!", - "{web-search,web-fetch}-provider.ts!", + "browser-control-auth.ts!", + "browser-config.ts!", + "browser-doctor.ts!", + "browser-host-inspection.ts!", + "browser-maintenance.ts!", + "browser-profiles.ts!", ]), ); - expect(knipConfig.workspaces["extensions/*"].entry).toContain("*.ts!"); }); it.each([