From fed337b164c8d26a7b9d9c5dc6a4195a5da50c7e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 04:53:21 +0100 Subject: [PATCH] test: speed media runtime specs --- src/image-generation/runtime.test.ts | 343 +++++++++--------- src/video-generation/runtime.test.ts | 517 ++++++++++++--------------- 2 files changed, 389 insertions(+), 471 deletions(-) diff --git a/src/image-generation/runtime.test.ts b/src/image-generation/runtime.test.ts index a5d0dfebe34..2ffeafacf5e 100644 --- a/src/image-generation/runtime.test.ts +++ b/src/image-generation/runtime.test.ts @@ -1,24 +1,48 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { - getMediaGenerationRuntimeMocks, - resetImageGenerationRuntimeMocks, -} from "../../test/helpers/media-generation/runtime-module-mocks.js"; import type { OpenClawConfig } from "../config/config.js"; -import { generateImage, listRuntimeImageGenerationProviders } from "./runtime.js"; +import { + generateImage, + listRuntimeImageGenerationProviders, + type GenerateImageParams, + type ImageGenerationRuntimeDeps, +} from "./runtime.js"; import type { ImageGenerationProvider } from "./types.js"; -const mocks = getMediaGenerationRuntimeMocks(); +let providers: ImageGenerationProvider[] = []; +let listedConfigs: Array = []; +let providerEnvVars: Record = {}; +let warnings: string[] = []; + +const runtimeDeps: ImageGenerationRuntimeDeps = { + getProvider: (providerId) => providers.find((provider) => provider.id === providerId), + listProviders: (config) => { + listedConfigs.push(config); + return providers; + }, + getProviderEnvVars: (providerId) => providerEnvVars[providerId] ?? [], + log: { + warn: (message) => { + warnings.push(message); + }, + }, +}; + +function runGenerateImage(params: GenerateImageParams) { + return generateImage(params, runtimeDeps); +} describe("image-generation runtime", () => { beforeEach(() => { - resetImageGenerationRuntimeMocks(); + providers = []; + listedConfigs = []; + providerEnvVars = {}; + warnings = []; }); it("generates images through the active image-generation provider", async () => { const authStore = { version: 1, profiles: {} } as const; let seenAuthStore: unknown; let seenTimeoutMs: number | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("image-plugin/img-v1"); const provider: ImageGenerationProvider = { id: "image-plugin", capabilities: { @@ -40,9 +64,9 @@ describe("image-generation runtime", () => { }; }, }; - mocks.getImageGenerationProvider.mockReturnValue(provider); + providers = [provider]; - const result = await generateImage({ + const result = await runGenerateImage({ cfg: { agents: { defaults: { @@ -73,7 +97,6 @@ describe("image-generation runtime", () => { it("uses configured image-generation timeout when the call omits timeoutMs", async () => { let seenTimeoutMs: number | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("image-plugin/img-v1"); const provider: ImageGenerationProvider = { id: "image-plugin", capabilities: { @@ -94,9 +117,9 @@ describe("image-generation runtime", () => { }; }, }; - mocks.getImageGenerationProvider.mockReturnValue(provider); + providers = [provider]; - await generateImage({ + await runGenerateImage({ cfg: { agents: { defaults: { @@ -114,41 +137,7 @@ describe("image-generation runtime", () => { }); it("auto-detects and falls through to another configured image-generation provider by default", async () => { - mocks.getImageGenerationProvider.mockImplementation((providerId: string) => { - if (providerId === "openai") { - return { - id: "openai", - defaultModel: "gpt-image-1", - capabilities: { - generate: {}, - edit: { enabled: true }, - }, - isConfigured: () => true, - async generateImage() { - throw new Error("OpenAI API key missing"); - }, - }; - } - if (providerId === "google") { - return { - id: "google", - defaultModel: "gemini-3.1-flash-image-preview", - capabilities: { - generate: {}, - edit: { enabled: true }, - }, - isConfigured: () => true, - async generateImage() { - return { - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - model: "gemini-3.1-flash-image-preview", - }; - }, - }; - } - return undefined; - }); - mocks.listImageGenerationProviders.mockReturnValue([ + providers = [ { id: "openai", defaultModel: "gpt-image-1", @@ -157,7 +146,9 @@ describe("image-generation runtime", () => { edit: { enabled: true }, }, isConfigured: () => true, - generateImage: async () => ({ images: [] }), + async generateImage() { + throw new Error("OpenAI API key missing"); + }, }, { id: "google", @@ -167,11 +158,16 @@ describe("image-generation runtime", () => { edit: { enabled: true }, }, isConfigured: () => true, - generateImage: async () => ({ images: [] }), + async generateImage() { + return { + images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], + model: "gemini-3.1-flash-image-preview", + }; + }, }, - ]); + ]; - const result = await generateImage({ + const result = await runGenerateImage({ cfg: {} as OpenClawConfig, prompt: "draw a cat", }); @@ -185,7 +181,7 @@ describe("image-generation runtime", () => { error: "OpenAI API key missing", }, ]); - expect(mocks.warn).toHaveBeenCalledWith( + expect(warnings).toContain( "image-generation candidate failed: openai/gpt-image-1: OpenAI API key missing", ); }); @@ -198,38 +194,39 @@ describe("image-generation runtime", () => { resolution?: string; } | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/gpt-image-1"); - mocks.getImageGenerationProvider.mockReturnValue({ - id: "openai", - capabilities: { - generate: { - supportsSize: true, - supportsAspectRatio: false, - supportsResolution: false, + providers = [ + { + id: "openai", + capabilities: { + generate: { + supportsSize: true, + supportsAspectRatio: false, + supportsResolution: false, + }, + edit: { + enabled: true, + supportsSize: true, + supportsAspectRatio: false, + supportsResolution: false, + }, + geometry: { + sizes: ["1024x1024", "1024x1536", "1536x1024"], + }, }, - edit: { - enabled: true, - supportsSize: true, - supportsAspectRatio: false, - supportsResolution: false, - }, - geometry: { - sizes: ["1024x1024", "1024x1536", "1536x1024"], + async generateImage(req) { + seenRequest = { + size: req.size, + aspectRatio: req.aspectRatio, + resolution: req.resolution, + }; + return { + images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], + }; }, }, - async generateImage(req) { - seenRequest = { - size: req.size, - aspectRatio: req.aspectRatio, - resolution: req.resolution, - }; - return { - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - }; - }, - }); + ]; - const result = await generateImage({ + const result = await runGenerateImage({ cfg: { agents: { defaults: { @@ -263,37 +260,38 @@ describe("image-generation runtime", () => { providerOptions?: unknown; } | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/gpt-image-2"); - mocks.getImageGenerationProvider.mockReturnValue({ - id: "openai", - capabilities: { - generate: { - supportsSize: true, + providers = [ + { + id: "openai", + capabilities: { + generate: { + supportsSize: true, + }, + edit: { + enabled: true, + supportsSize: true, + }, + output: { + qualities: ["low", "medium", "high", "auto"], + formats: ["png", "jpeg", "webp"], + backgrounds: ["transparent", "opaque", "auto"], + }, }, - edit: { - enabled: true, - supportsSize: true, - }, - output: { - qualities: ["low", "medium", "high", "auto"], - formats: ["png", "jpeg", "webp"], - backgrounds: ["transparent", "opaque", "auto"], + async generateImage(req) { + seenRequest = { + quality: req.quality, + outputFormat: req.outputFormat, + background: req.background, + providerOptions: req.providerOptions, + }; + return { + images: [{ buffer: Buffer.from("jpeg-bytes"), mimeType: "image/jpeg" }], + }; }, }, - async generateImage(req) { - seenRequest = { - quality: req.quality, - outputFormat: req.outputFormat, - background: req.background, - providerOptions: req.providerOptions, - }; - return { - images: [{ buffer: Buffer.from("jpeg-bytes"), mimeType: "image/jpeg" }], - }; - }, - }); + ]; - const result = await generateImage({ + const result = await runGenerateImage({ cfg: { agents: { defaults: { @@ -339,28 +337,29 @@ describe("image-generation runtime", () => { background?: string; } | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("vydra/grok-imagine"); - mocks.getImageGenerationProvider.mockReturnValue({ - id: "vydra", - capabilities: { - generate: {}, - edit: { - enabled: false, + providers = [ + { + id: "vydra", + capabilities: { + generate: {}, + edit: { + enabled: false, + }, + }, + async generateImage(req) { + seenRequest = { + quality: req.quality, + outputFormat: req.outputFormat, + background: req.background, + }; + return { + images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], + }; }, }, - async generateImage(req) { - seenRequest = { - quality: req.quality, - outputFormat: req.outputFormat, - background: req.background, - }; - return { - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - }; - }, - }); + ]; - const result = await generateImage({ + const result = await runGenerateImage({ cfg: { agents: { defaults: { @@ -394,39 +393,40 @@ describe("image-generation runtime", () => { resolution?: string; } | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("minimax/image-01"); - mocks.getImageGenerationProvider.mockReturnValue({ - id: "minimax", - capabilities: { - generate: { - supportsSize: false, - supportsAspectRatio: true, - supportsResolution: false, + providers = [ + { + id: "minimax", + capabilities: { + generate: { + supportsSize: false, + supportsAspectRatio: true, + supportsResolution: false, + }, + edit: { + enabled: true, + supportsSize: false, + supportsAspectRatio: true, + supportsResolution: false, + }, + geometry: { + aspectRatios: ["1:1", "16:9"], + }, }, - edit: { - enabled: true, - supportsSize: false, - supportsAspectRatio: true, - supportsResolution: false, - }, - geometry: { - aspectRatios: ["1:1", "16:9"], + async generateImage(req) { + seenRequest = { + size: req.size, + aspectRatio: req.aspectRatio, + resolution: req.resolution, + }; + return { + images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], + model: "image-01", + }; }, }, - async generateImage(req) { - seenRequest = { - size: req.size, - aspectRatio: req.aspectRatio, - resolution: req.resolution, - }; - return { - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - model: "image-01", - }; - }, - }); + ]; - const result = await generateImage({ + const result = await runGenerateImage({ cfg: { agents: { defaults: { @@ -458,7 +458,7 @@ describe("image-generation runtime", () => { }); it("lists runtime image-generation providers through the provider registry", () => { - const providers: ImageGenerationProvider[] = [ + const registryProviders: ImageGenerationProvider[] = [ { id: "image-plugin", defaultModel: "img-v1", @@ -480,16 +480,16 @@ describe("image-generation runtime", () => { }), }, ]; - mocks.listImageGenerationProviders.mockReturnValue(providers); + providers = registryProviders; - expect(listRuntimeImageGenerationProviders({ config: {} as OpenClawConfig })).toEqual( - providers, - ); - expect(mocks.listImageGenerationProviders).toHaveBeenCalledWith({} as OpenClawConfig); + expect( + listRuntimeImageGenerationProviders({ config: {} as OpenClawConfig }, runtimeDeps), + ).toEqual(registryProviders); + expect(listedConfigs).toEqual([{} as OpenClawConfig]); }); it("builds a generic config hint without hardcoded provider ids", async () => { - mocks.listImageGenerationProviders.mockReturnValue([ + providers = [ { id: "vision-one", defaultModel: "paint-v1", @@ -514,19 +514,14 @@ describe("image-generation runtime", () => { images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], }), }, - ]); - mocks.getProviderEnvVars.mockImplementation((providerId: string) => { - if (providerId === "vision-one") { - return ["VISION_ONE_API_KEY"]; - } - if (providerId === "vision-two") { - return ["VISION_TWO_API_KEY"]; - } - return []; - }); + ]; + providerEnvVars = { + "vision-one": ["VISION_ONE_API_KEY"], + "vision-two": ["VISION_TWO_API_KEY"], + }; await expect( - generateImage({ cfg: {} as OpenClawConfig, prompt: "draw a cat" }), + runGenerateImage({ cfg: {} as OpenClawConfig, prompt: "draw a cat" }), ).rejects.toThrow( 'No image-generation model configured. Set agents.defaults.imageGenerationModel.primary to a provider/model like "vision-one/paint-v1". If you want a specific provider, also configure that provider\'s auth/API key first (vision-one: VISION_ONE_API_KEY; vision-two: VISION_TWO_API_KEY).', ); diff --git a/src/video-generation/runtime.test.ts b/src/video-generation/runtime.test.ts index 992a91ea2b2..487ce2a1457 100644 --- a/src/video-generation/runtime.test.ts +++ b/src/video-generation/runtime.test.ts @@ -1,13 +1,33 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { - getMediaGenerationRuntimeMocks, - resetVideoGenerationRuntimeMocks, -} from "../../test/helpers/media-generation/runtime-module-mocks.js"; import type { OpenClawConfig } from "../config/types.js"; -import { generateVideo, listRuntimeVideoGenerationProviders } from "./runtime.js"; +import { + generateVideo, + listRuntimeVideoGenerationProviders, + type GenerateVideoParams, + type VideoGenerationRuntimeDeps, +} from "./runtime.js"; import type { VideoGenerationProvider, VideoGenerationProviderOptionType } from "./types.js"; -const mocks = getMediaGenerationRuntimeMocks(); +let providers: VideoGenerationProvider[] = []; +let listedConfigs: Array = []; +let providerEnvVars: Record = {}; + +const runtimeDeps: VideoGenerationRuntimeDeps = { + getProvider: (providerId) => providers.find((provider) => provider.id === providerId), + listProviders: (config) => { + listedConfigs.push(config); + return providers; + }, + getProviderEnvVars: (providerId) => providerEnvVars[providerId] ?? [], + log: { + debug: () => {}, + warn: () => {}, + }, +}; + +function runGenerateVideo(params: GenerateVideoParams) { + return generateVideo(params, runtimeDeps); +} function createProviderOptionsCaptureProvider( capabilities: VideoGenerationProvider["capabilities"], @@ -28,14 +48,15 @@ function createProviderOptionsCaptureProvider( describe("video-generation runtime", () => { beforeEach(() => { - resetVideoGenerationRuntimeMocks(); + providers = []; + listedConfigs = []; + providerEnvVars = {}; }); it("generates videos through the active video-generation provider", async () => { const authStore = { version: 1, profiles: {} } as const; let seenAuthStore: unknown; let seenTimeoutMs: number | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); const provider: VideoGenerationProvider = { id: "video-plugin", capabilities: {}, @@ -54,9 +75,9 @@ describe("video-generation runtime", () => { }; }, }; - mocks.getVideoGenerationProvider.mockReturnValue(provider); + providers = [provider]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -86,52 +107,31 @@ describe("video-generation runtime", () => { }); it("auto-detects and falls through to another configured video-generation provider by default", async () => { - mocks.getVideoGenerationProvider.mockImplementation((providerId: string) => { - if (providerId === "openai") { - return { - id: "openai", - defaultModel: "sora-2", - capabilities: {}, - isConfigured: () => true, - async generateVideo() { - throw new Error("Your request was blocked by our moderation system."); - }, - }; - } - if (providerId === "runway") { - return { - id: "runway", - defaultModel: "gen4.5", - capabilities: {}, - isConfigured: () => true, - async generateVideo() { - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "gen4.5", - }; - }, - }; - } - return undefined; - }); - mocks.listVideoGenerationProviders.mockReturnValue([ + providers = [ { id: "openai", defaultModel: "sora-2", capabilities: {}, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo() { + throw new Error("Your request was blocked by our moderation system."); + }, }, { id: "runway", defaultModel: "gen4.5", capabilities: {}, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo() { + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "gen4.5", + }; + }, }, - ]); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: {} as OpenClawConfig, prompt: "animate a cat", }); @@ -148,7 +148,6 @@ describe("video-generation runtime", () => { }); it("forwards providerOptions to providers that declare the matching schema", async () => { - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); const { provider, getSeenProviderOptions } = createProviderOptionsCaptureProvider({ providerOptions: { seed: "number", @@ -156,9 +155,9 @@ describe("video-generation runtime", () => { camera_fixed: "boolean", }, }); - mocks.getVideoGenerationProvider.mockReturnValue(provider); + providers = [provider]; - await generateVideo({ + await runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "video-plugin/vid-v1" } } }, } as OpenClawConfig, @@ -172,11 +171,10 @@ describe("video-generation runtime", () => { it("passes providerOptions through to providers that do not declare any schema", async () => { // Undeclared schema = backward-compatible pass-through: the provider receives the // options and can handle or ignore them. No skip occurs. - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); const { provider, getSeenProviderOptions } = createProviderOptionsCaptureProvider({}); - mocks.getVideoGenerationProvider.mockReturnValue(provider); + providers = [provider]; - await generateVideo({ + await runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "video-plugin/vid-v1" } } }, } as OpenClawConfig, @@ -189,7 +187,6 @@ describe("video-generation runtime", () => { it("skips candidates that explicitly declare an empty providerOptions schema", async () => { // Explicitly declared empty schema ({}) = provider has opted in and supports no options. - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); const provider: VideoGenerationProvider = { id: "video-plugin", capabilities: { @@ -201,10 +198,10 @@ describe("video-generation runtime", () => { throw new Error("should not be called"); }, }; - mocks.getVideoGenerationProvider.mockReturnValue(provider); + providers = [provider]; await expect( - generateVideo({ + runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "video-plugin/vid-v1" } } }, } as OpenClawConfig, @@ -215,7 +212,6 @@ describe("video-generation runtime", () => { }); it("skips candidates that declare a providerOptions schema missing the requested key", async () => { - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); const provider: VideoGenerationProvider = { id: "video-plugin", capabilities: { @@ -225,10 +221,10 @@ describe("video-generation runtime", () => { throw new Error("should not be called"); }, }; - mocks.getVideoGenerationProvider.mockReturnValue(provider); + providers = [provider]; await expect( - generateVideo({ + runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "video-plugin/vid-v1" } } }, } as OpenClawConfig, @@ -239,7 +235,6 @@ describe("video-generation runtime", () => { }); it("skips candidates when providerOptions values do not match the declared type", async () => { - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); const provider: VideoGenerationProvider = { id: "video-plugin", capabilities: { @@ -249,10 +244,10 @@ describe("video-generation runtime", () => { throw new Error("should not be called"); }, }; - mocks.getVideoGenerationProvider.mockReturnValue(provider); + providers = [provider]; await expect( - generateVideo({ + runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "video-plugin/vid-v1" } } }, } as OpenClawConfig, @@ -265,57 +260,32 @@ describe("video-generation runtime", () => { it("falls over from a provider with explicitly empty providerOptions schema to one that has it", async () => { // Explicitly empty schema ({}) causes a skip; undeclared schema passes through. // Here "openai" declares {} to signal it has been audited and truly accepts no options. - mocks.getVideoGenerationProvider.mockImplementation((providerId: string) => { - if (providerId === "openai") { - return { - id: "openai", - defaultModel: "sora-2", - capabilities: { - providerOptions: {} as Record, - }, // explicitly empty: accepts no options - isConfigured: () => true, - async generateVideo() { - throw new Error("should not be called"); - }, - }; - } - if (providerId === "byteplus") { - return { - id: "byteplus", - defaultModel: "seedance-1-0-pro-250528", - capabilities: { - providerOptions: { seed: "number" }, - }, - isConfigured: () => true, - async generateVideo(req) { - expect(req.providerOptions).toEqual({ seed: 42 }); - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "seedance-1-0-pro-250528", - }; - }, - }; - } - return undefined; - }); - mocks.listVideoGenerationProviders.mockReturnValue([ + providers = [ { id: "openai", defaultModel: "sora-2", capabilities: { providerOptions: {} as Record }, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo() { + throw new Error("should not be called"); + }, }, { id: "byteplus", defaultModel: "seedance-1-0-pro-250528", capabilities: { providerOptions: { seed: "number" } }, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo(req) { + expect(req.providerOptions).toEqual({ seed: 42 }); + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "seedance-1-0-pro-250528", + }; + }, }, - ]); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: {} as OpenClawConfig, prompt: "animate a cat", providerOptions: { seed: 42 }, @@ -328,57 +298,34 @@ describe("video-generation runtime", () => { }); it("skips providers that cannot satisfy reference audio inputs and falls back", async () => { - mocks.getVideoGenerationProvider.mockImplementation((providerId: string) => { - if (providerId === "openai") { - return { - id: "openai", - defaultModel: "sora-2", - capabilities: {}, - isConfigured: () => true, - async generateVideo() { - throw new Error("should not be called"); - }, - }; - } - if (providerId === "byteplus") { - return { - id: "byteplus", - defaultModel: "seedance-1-0-pro-250528", - capabilities: { - maxInputAudios: 1, - }, - isConfigured: () => true, - async generateVideo(req) { - expect(req.inputAudios).toEqual([ - { url: "https://example.com/reference-audio.mp3", role: "reference_audio" }, - ]); - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "seedance-1-0-pro-250528", - }; - }, - }; - } - return undefined; - }); - mocks.listVideoGenerationProviders.mockReturnValue([ + providers = [ { id: "openai", defaultModel: "sora-2", capabilities: {}, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo() { + throw new Error("should not be called"); + }, }, { id: "byteplus", defaultModel: "seedance-1-0-pro-250528", capabilities: { maxInputAudios: 1 }, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo(req) { + expect(req.inputAudios).toEqual([ + { url: "https://example.com/reference-audio.mp3", role: "reference_audio" }, + ]); + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "seedance-1-0-pro-250528", + }; + }, }, - ]); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -402,31 +349,30 @@ describe("video-generation runtime", () => { inputVideos?: unknown; inputAudios?: unknown; } = {}; - mocks.resolveAgentModelPrimaryValue.mockReturnValue( - "fal/bytedance/seedance-2.0/fast/reference-to-video", - ); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "fal", - capabilities: { - videoToVideo: { - enabled: true, - maxInputImages: 9, - maxInputVideos: 3, - maxInputAudios: 3, + providers = [ + { + id: "fal", + capabilities: { + videoToVideo: { + enabled: true, + maxInputImages: 9, + maxInputVideos: 3, + maxInputAudios: 3, + }, + }, + async generateVideo(req) { + seenRequest.inputImages = req.inputImages; + seenRequest.inputVideos = req.inputVideos; + seenRequest.inputAudios = req.inputAudios; + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "bytedance/seedance-2.0/fast/reference-to-video", + }; }, }, - async generateVideo(req) { - seenRequest.inputImages = req.inputImages; - seenRequest.inputVideos = req.inputVideos; - seenRequest.inputAudios = req.inputAudios; - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "bytedance/seedance-2.0/fast/reference-to-video", - }; - }, - }); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -452,17 +398,18 @@ describe("video-generation runtime", () => { }); it("fails when every candidate is skipped for unsupported reference audio inputs", async () => { - mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/sora-2"); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "openai", - capabilities: {}, - async generateVideo() { - throw new Error("should not be called"); + providers = [ + { + id: "openai", + capabilities: {}, + async generateVideo() { + throw new Error("should not be called"); + }, }, - }); + ]; await expect( - generateVideo({ + runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "openai/sora-2" } } }, } as OpenClawConfig, @@ -474,61 +421,32 @@ describe("video-generation runtime", () => { it("skips providers whose hard duration cap is below the request and falls back", async () => { let seenDurationSeconds: number | undefined; - mocks.getVideoGenerationProvider.mockImplementation((providerId: string) => { - if (providerId === "openai") { - return { - id: "openai", - defaultModel: "sora-2", - capabilities: { - generate: { - maxDurationSeconds: 4, - }, - }, - isConfigured: () => true, - async generateVideo() { - throw new Error("should not be called"); - }, - }; - } - if (providerId === "runway") { - return { - id: "runway", - defaultModel: "gen4.5", - capabilities: { - generate: { - maxDurationSeconds: 8, - }, - }, - isConfigured: () => true, - async generateVideo(req) { - seenDurationSeconds = req.durationSeconds; - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "gen4.5", - }; - }, - }; - } - return undefined; - }); - mocks.listVideoGenerationProviders.mockReturnValue([ + providers = [ { id: "openai", defaultModel: "sora-2", capabilities: { generate: { maxDurationSeconds: 4 } }, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo() { + throw new Error("should not be called"); + }, }, { id: "runway", defaultModel: "gen4.5", capabilities: { generate: { maxDurationSeconds: 8 } }, isConfigured: () => true, - generateVideo: async () => ({ videos: [] }), + async generateVideo(req) { + seenDurationSeconds = req.durationSeconds; + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "gen4.5", + }; + }, }, - ]); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -548,21 +466,22 @@ describe("video-generation runtime", () => { }); it("fails when every candidate is skipped for exceeding hard duration caps", async () => { - mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/sora-2"); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "openai", - capabilities: { - generate: { - maxDurationSeconds: 4, + providers = [ + { + id: "openai", + capabilities: { + generate: { + maxDurationSeconds: 4, + }, + }, + async generateVideo() { + throw new Error("should not be called"); }, }, - async generateVideo() { - throw new Error("should not be called"); - }, - }); + ]; await expect( - generateVideo({ + runGenerateVideo({ cfg: { agents: { defaults: { videoGenerationModel: { primary: "openai/sora-2" } } }, } as OpenClawConfig, @@ -573,17 +492,18 @@ describe("video-generation runtime", () => { }); it("rejects provider results that contain undeliverable assets", async () => { - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "video-plugin", - capabilities: {}, - generateVideo: async () => ({ - videos: [{ mimeType: "video/mp4" }], - }), - }); + providers = [ + { + id: "video-plugin", + capabilities: {}, + generateVideo: async () => ({ + videos: [{ mimeType: "video/mp4" }], + }), + }, + ]; await expect( - generateVideo({ + runGenerateVideo({ cfg: { agents: { defaults: { @@ -597,7 +517,7 @@ describe("video-generation runtime", () => { }); it("lists runtime video-generation providers through the provider registry", () => { - const providers: VideoGenerationProvider[] = [ + const registryProviders: VideoGenerationProvider[] = [ { id: "video-plugin", defaultModel: "vid-v1", @@ -612,34 +532,35 @@ describe("video-generation runtime", () => { }), }, ]; - mocks.listVideoGenerationProviders.mockReturnValue(providers); + providers = registryProviders; - expect(listRuntimeVideoGenerationProviders({ config: {} as OpenClawConfig })).toEqual( - providers, - ); - expect(mocks.listVideoGenerationProviders).toHaveBeenCalledWith({} as OpenClawConfig); + expect( + listRuntimeVideoGenerationProviders({ config: {} as OpenClawConfig }, runtimeDeps), + ).toEqual(registryProviders); + expect(listedConfigs).toEqual([{} as OpenClawConfig]); }); it("normalizes requested durations to supported provider values", async () => { let seenDurationSeconds: number | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("video-plugin/vid-v1"); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "video-plugin", - capabilities: { - generate: { - supportedDurationSeconds: [4, 6, 8], + providers = [ + { + id: "video-plugin", + capabilities: { + generate: { + supportedDurationSeconds: [4, 6, 8], + }, + }, + generateVideo: async (req) => { + seenDurationSeconds = req.durationSeconds; + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "vid-v1", + }; }, }, - generateVideo: async (req) => { - seenDurationSeconds = req.durationSeconds; - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "vid-v1", - }; - }, - }); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -677,30 +598,31 @@ describe("video-generation runtime", () => { watermark?: boolean; } | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("openai/sora-2"); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "openai", - capabilities: { - generate: { - supportsSize: true, + providers = [ + { + id: "openai", + capabilities: { + generate: { + supportsSize: true, + }, + }, + generateVideo: async (req) => { + seenRequest = { + size: req.size, + aspectRatio: req.aspectRatio, + resolution: req.resolution, + audio: req.audio, + watermark: req.watermark, + }; + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "sora-2", + }; }, }, - generateVideo: async (req) => { - seenRequest = { - size: req.size, - aspectRatio: req.aspectRatio, - resolution: req.resolution, - audio: req.audio, - watermark: req.watermark, - }; - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "sora-2", - }; - }, - }); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -739,35 +661,36 @@ describe("video-generation runtime", () => { resolution?: string; } | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("runway/gen4.5"); - mocks.getVideoGenerationProvider.mockReturnValue({ - id: "runway", - capabilities: { - generate: { - supportsSize: true, - supportsAspectRatio: false, + providers = [ + { + id: "runway", + capabilities: { + generate: { + supportsSize: true, + supportsAspectRatio: false, + }, + imageToVideo: { + enabled: true, + maxInputImages: 1, + supportsSize: false, + supportsAspectRatio: true, + }, }, - imageToVideo: { - enabled: true, - maxInputImages: 1, - supportsSize: false, - supportsAspectRatio: true, + generateVideo: async (req) => { + seenRequest = { + size: req.size, + aspectRatio: req.aspectRatio, + resolution: req.resolution, + }; + return { + videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], + model: "gen4.5", + }; }, }, - generateVideo: async (req) => { - seenRequest = { - size: req.size, - aspectRatio: req.aspectRatio, - resolution: req.resolution, - }; - return { - videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], - model: "gen4.5", - }; - }, - }); + ]; - const result = await generateVideo({ + const result = await runGenerateVideo({ cfg: { agents: { defaults: { @@ -800,7 +723,7 @@ describe("video-generation runtime", () => { }); it("builds a generic config hint without hardcoded provider ids", async () => { - mocks.listVideoGenerationProviders.mockReturnValue([ + providers = [ { id: "motion-one", defaultModel: "animate-v1", @@ -809,11 +732,11 @@ describe("video-generation runtime", () => { videos: [{ buffer: Buffer.from("mp4-bytes"), mimeType: "video/mp4" }], }), }, - ]); - mocks.getProviderEnvVars.mockReturnValue(["MOTION_ONE_API_KEY"]); + ]; + providerEnvVars = { "motion-one": ["MOTION_ONE_API_KEY"] }; await expect( - generateVideo({ cfg: {} as OpenClawConfig, prompt: "animate a cat" }), + runGenerateVideo({ cfg: {} as OpenClawConfig, prompt: "animate a cat" }), ).rejects.toThrow( 'No video-generation model configured. Set agents.defaults.videoGenerationModel.primary to a provider/model like "motion-one/animate-v1". If you want a specific provider, also configure that provider\'s auth/API key first (motion-one: MOTION_ONE_API_KEY).', );