diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 0e7186cfa6e..42847905ea1 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -1,4 +1,4 @@ -import type { AcpRuntimeHandle, AcpRuntimeOptions, AcpSessionStore } from "acpx/runtime"; +import type { AcpRuntimeHandle, AcpRuntimeOptions, AcpSessionStore } from "acpx/dist/runtime.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => { @@ -54,7 +54,7 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock("acpx/runtime", () => ({ +vi.mock("acpx/dist/runtime.js", () => ({ ACPX_BACKEND_ID: "acpx", AcpxRuntime: mocks.MockAcpxRuntime, createAcpRuntime: vi.fn(), diff --git a/extensions/image-generation-core/src/runtime.test.ts b/extensions/image-generation-core/src/runtime.test.ts index a649307fcb7..1d748980628 100644 --- a/extensions/image-generation-core/src/runtime.test.ts +++ b/extensions/image-generation-core/src/runtime.test.ts @@ -1,290 +1,24 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import type { ImageGenerationProvider } from "../api.js"; +import { describe, expect, it, vi } from "vitest"; + +const sdkExports = vi.hoisted(() => ({ + generateImage: vi.fn(), + listRuntimeImageGenerationProviders: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/image-generation-runtime", () => sdkExports); + +import { + generateImage as sdkGenerateImage, + listRuntimeImageGenerationProviders as sdkListRuntimeImageGenerationProviders, +} from "openclaw/plugin-sdk/image-generation-runtime"; import { generateImage, listRuntimeImageGenerationProviders } from "./runtime.js"; -const mocks = vi.hoisted(() => { - const debug = vi.fn(); - return { - createSubsystemLogger: vi.fn(() => ({ debug })), - describeFailoverError: vi.fn(), - getImageGenerationProvider: vi.fn< - (providerId: string, config?: OpenClawConfig) => ImageGenerationProvider | undefined - >(() => undefined), - getProviderEnvVars: vi.fn<(providerId: string) => string[]>(() => []), - isFailoverError: vi.fn<(err: unknown) => boolean>(() => false), - listImageGenerationProviders: vi.fn<(config?: OpenClawConfig) => ImageGenerationProvider[]>( - () => [], - ), - parseImageGenerationModelRef: vi.fn< - (raw?: string) => { provider: string; model: string } | undefined - >((raw?: string) => { - const trimmed = raw?.trim(); - if (!trimmed) { - return undefined; - } - const slash = trimmed.indexOf("/"); - if (slash <= 0 || slash === trimmed.length - 1) { - return undefined; - } - return { - provider: trimmed.slice(0, slash), - model: trimmed.slice(slash + 1), - }; - }), - resolveAgentModelFallbackValues: vi.fn<(value: unknown) => string[]>(() => []), - resolveAgentModelPrimaryValue: vi.fn<(value: unknown) => string | undefined>(() => undefined), - debug, - }; -}); - -vi.mock("../api.js", () => ({ - createSubsystemLogger: mocks.createSubsystemLogger, - describeFailoverError: mocks.describeFailoverError, - getImageGenerationProvider: mocks.getImageGenerationProvider, - getProviderEnvVars: mocks.getProviderEnvVars, - isFailoverError: mocks.isFailoverError, - listImageGenerationProviders: mocks.listImageGenerationProviders, - parseImageGenerationModelRef: mocks.parseImageGenerationModelRef, - resolveAgentModelFallbackValues: mocks.resolveAgentModelFallbackValues, - resolveAgentModelPrimaryValue: mocks.resolveAgentModelPrimaryValue, -})); -vi.mock( - "openclaw/plugin-sdk/media-generation-runtime-shared", - async () => import("../../../src/media-generation/runtime-shared.js"), -); - -describe("image-generation runtime", () => { - beforeEach(() => { - mocks.createSubsystemLogger.mockClear(); - mocks.describeFailoverError.mockReset(); - mocks.getImageGenerationProvider.mockReset(); - mocks.getProviderEnvVars.mockReset(); - mocks.getProviderEnvVars.mockReturnValue([]); - mocks.isFailoverError.mockReset(); - mocks.isFailoverError.mockReturnValue(false); - mocks.listImageGenerationProviders.mockReset(); - mocks.listImageGenerationProviders.mockReturnValue([]); - mocks.parseImageGenerationModelRef.mockClear(); - mocks.resolveAgentModelFallbackValues.mockReset(); - mocks.resolveAgentModelFallbackValues.mockReturnValue([]); - mocks.resolveAgentModelPrimaryValue.mockReset(); - mocks.resolveAgentModelPrimaryValue.mockReturnValue(undefined); - mocks.debug.mockReset(); +describe("image-generation-core runtime", () => { + it("re-exports generateImage from the plugin sdk runtime", () => { + expect(generateImage).toBe(sdkGenerateImage); }); - it("generates images through the active image-generation provider", async () => { - const authStore = { version: 1, profiles: {} } as const; - let seenAuthStore: unknown; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("image-plugin/img-v1"); - const provider: ImageGenerationProvider = { - id: "image-plugin", - capabilities: { - generate: {}, - edit: { enabled: false }, - }, - async generateImage(req: { authStore?: unknown }) { - seenAuthStore = req.authStore; - return { - images: [ - { - buffer: Buffer.from("png-bytes"), - mimeType: "image/png", - fileName: "sample.png", - }, - ], - model: "img-v1", - }; - }, - }; - mocks.getImageGenerationProvider.mockReturnValue(provider); - - const result = await generateImage({ - cfg: { - agents: { - defaults: { - imageGenerationModel: { primary: "image-plugin/img-v1" }, - }, - }, - } as OpenClawConfig, - prompt: "draw a cat", - agentDir: "/tmp/agent", - authStore, - }); - - expect(result.provider).toBe("image-plugin"); - expect(result.model).toBe("img-v1"); - expect(result.attempts).toEqual([]); - expect(seenAuthStore).toEqual(authStore); - expect(result.images).toEqual([ - { - buffer: Buffer.from("png-bytes"), - mimeType: "image/png", - fileName: "sample.png", - }, - ]); - }); - - it("lists runtime image-generation providers through the owner runtime", () => { - const providers: ImageGenerationProvider[] = [ - { - id: "image-plugin", - defaultModel: "img-v1", - models: ["img-v1", "img-v2"], - capabilities: { - generate: { - supportsResolution: true, - }, - edit: { - enabled: true, - maxInputImages: 3, - }, - geometry: { - resolutions: ["1K", "2K"], - }, - }, - generateImage: async () => ({ - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - }), - }, - ]; - mocks.listImageGenerationProviders.mockReturnValue(providers); - - expect(listRuntimeImageGenerationProviders({ config: {} as OpenClawConfig })).toEqual( - providers, - ); - expect(mocks.listImageGenerationProviders).toHaveBeenCalledWith({} as OpenClawConfig); - }); - - it("explains native image-generation config and provider auth when no model is configured", async () => { - mocks.listImageGenerationProviders.mockReturnValue([ - { - id: "google", - defaultModel: "gemini-3-pro-image-preview", - isConfigured: () => false, - capabilities: { - generate: {}, - edit: { enabled: false }, - }, - generateImage: async () => ({ - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - }), - }, - { - id: "openai", - defaultModel: "gpt-image-1", - isConfigured: () => false, - capabilities: { - generate: {}, - edit: { enabled: false }, - }, - generateImage: async () => ({ - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - }), - }, - ]); - mocks.getProviderEnvVars.mockImplementation((providerId: string) => { - if (providerId === "google") { - return ["GEMINI_API_KEY", "GOOGLE_API_KEY"]; - } - if (providerId === "openai") { - return ["OPENAI_API_KEY"]; - } - return []; - }); - - const promise = generateImage({ cfg: {} as OpenClawConfig, prompt: "draw a cat" }); - - await expect(promise).rejects.toThrow("No image-generation model configured."); - await expect(promise).rejects.toThrow( - 'Set agents.defaults.imageGenerationModel.primary to a provider/model like "', - ); - await expect(promise).rejects.toThrow("google: GEMINI_API_KEY / GOOGLE_API_KEY"); - await expect(promise).rejects.toThrow("openai: OPENAI_API_KEY"); - }); - - it("does not crash on prototype-like provider ids in auth hints", async () => { - mocks.listImageGenerationProviders.mockReturnValue([ - { - id: "__proto__", - defaultModel: "proto-v1", - isConfigured: () => false, - capabilities: { - generate: {}, - edit: { enabled: false }, - }, - generateImage: async () => ({ - images: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }], - }), - }, - ]); - - await expect( - generateImage({ cfg: {} as OpenClawConfig, prompt: "draw a cat" }), - ).rejects.toThrow("No image-generation model configured."); - }); - - it("maps requested size to the closest supported fallback geometry", async () => { - let seenRequest: - | { - size?: string; - aspectRatio?: string; - resolution?: string; - } - | undefined; - mocks.resolveAgentModelPrimaryValue.mockReturnValue("minimax/image-01"); - mocks.getImageGenerationProvider.mockReturnValue({ - 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"], - }, - }, - 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({ - cfg: { - agents: { - defaults: { - imageGenerationModel: { primary: "minimax/image-01" }, - }, - }, - } as OpenClawConfig, - prompt: "draw a cat", - size: "1280x720", - }); - - expect(seenRequest).toEqual({ - size: undefined, - aspectRatio: "16:9", - resolution: undefined, - }); - expect(result.metadata).toMatchObject({ - requestedSize: "1280x720", - normalizedAspectRatio: "16:9", - aspectRatioDerivedFromSize: "16:9", - }); + it("re-exports listRuntimeImageGenerationProviders from the plugin sdk runtime", () => { + expect(listRuntimeImageGenerationProviders).toBe(sdkListRuntimeImageGenerationProviders); }); }); diff --git a/src/agents/tools/image-generate-tool.test.ts b/src/agents/tools/image-generate-tool.test.ts index 6d40a570d1e..36fab10e1b3 100644 --- a/src/agents/tools/image-generate-tool.test.ts +++ b/src/agents/tools/image-generate-tool.test.ts @@ -745,6 +745,12 @@ describe("createImageGenerateTool", () => { fileName: "generated.png", }, ], + normalization: { + aspectRatio: { + applied: "16:9", + derivedFrom: "size", + }, + }, metadata: { requestedSize: "1280x720", normalizedAspectRatio: "16:9", @@ -765,6 +771,12 @@ describe("createImageGenerateTool", () => { expect(result.details).toMatchObject({ aspectRatio: "16:9", + normalization: { + aspectRatio: { + applied: "16:9", + derivedFrom: "size", + }, + }, metadata: { requestedSize: "1280x720", normalizedAspectRatio: "16:9", diff --git a/src/agents/tools/music-generate-tool.test.ts b/src/agents/tools/music-generate-tool.test.ts index e56babd0e90..2bb79692ec6 100644 --- a/src/agents/tools/music-generate-tool.test.ts +++ b/src/agents/tools/music-generate-tool.test.ts @@ -369,6 +369,12 @@ describe("createMusicGenerateTool", () => { fileName: "night-drive.mp3", }, ], + normalization: { + durationSeconds: { + requested: 45, + applied: 30, + }, + }, metadata: { requestedDurationSeconds: 45, normalizedDurationSeconds: 30, @@ -404,6 +410,12 @@ describe("createMusicGenerateTool", () => { expect(result.details).toMatchObject({ durationSeconds: 30, requestedDurationSeconds: 45, + normalization: { + durationSeconds: { + requested: 45, + applied: 30, + }, + }, }); }); }); diff --git a/src/agents/tools/video-generate-tool.test.ts b/src/agents/tools/video-generate-tool.test.ts index eaeccd1ecc1..a71420dd032 100644 --- a/src/agents/tools/video-generate-tool.test.ts +++ b/src/agents/tools/video-generate-tool.test.ts @@ -258,6 +258,13 @@ describe("createVideoGenerateTool", () => { fileName: "lobster.mp4", }, ], + normalization: { + durationSeconds: { + requested: 5, + applied: 6, + supportedValues: [4, 6, 8], + }, + }, metadata: { requestedDurationSeconds: 5, normalizedDurationSeconds: 6, @@ -295,6 +302,13 @@ describe("createVideoGenerateTool", () => { durationSeconds: 6, requestedDurationSeconds: 5, supportedDurationSeconds: [4, 6, 8], + normalization: { + durationSeconds: { + requested: 5, + applied: 6, + supportedValues: [4, 6, 8], + }, + }, }); }); @@ -311,6 +325,12 @@ describe("createVideoGenerateTool", () => { fileName: "lobster.mp4", }, ], + normalization: { + aspectRatio: { + applied: "16:9", + derivedFrom: "size", + }, + }, metadata: { requestedSize: "1280x720", normalizedAspectRatio: "16:9", @@ -343,6 +363,12 @@ describe("createVideoGenerateTool", () => { expect(result.details).toMatchObject({ aspectRatio: "16:9", + normalization: { + aspectRatio: { + applied: "16:9", + derivedFrom: "size", + }, + }, metadata: { requestedSize: "1280x720", normalizedAspectRatio: "16:9", diff --git a/src/image-generation/runtime.test.ts b/src/image-generation/runtime.test.ts index bb4c858b814..fe465a3d822 100644 --- a/src/image-generation/runtime.test.ts +++ b/src/image-generation/runtime.test.ts @@ -336,6 +336,12 @@ describe("image-generation runtime", () => { resolution: undefined, }); expect(result.ignoredOverrides).toEqual([]); + expect(result.normalization).toMatchObject({ + aspectRatio: { + applied: "16:9", + derivedFrom: "size", + }, + }); expect(result.metadata).toMatchObject({ requestedSize: "1280x720", normalizedAspectRatio: "16:9", diff --git a/src/music-generation/runtime.test.ts b/src/music-generation/runtime.test.ts index fcfe0013062..5412f571edf 100644 --- a/src/music-generation/runtime.test.ts +++ b/src/music-generation/runtime.test.ts @@ -398,6 +398,12 @@ describe("music-generation runtime", () => { durationSeconds: 30, }); expect(result.ignoredOverrides).toEqual([]); + expect(result.normalization).toMatchObject({ + durationSeconds: { + requested: 45, + applied: 30, + }, + }); expect(result.metadata).toMatchObject({ requestedDurationSeconds: 45, normalizedDurationSeconds: 30, diff --git a/src/video-generation/runtime.test.ts b/src/video-generation/runtime.test.ts index 4cb4571cc35..e870c94e531 100644 --- a/src/video-generation/runtime.test.ts +++ b/src/video-generation/runtime.test.ts @@ -255,6 +255,13 @@ describe("video-generation runtime", () => { }); expect(seenDurationSeconds).toBe(6); + expect(result.normalization).toMatchObject({ + durationSeconds: { + requested: 5, + applied: 6, + supportedValues: [4, 6, 8], + }, + }); expect(result.metadata).toMatchObject({ requestedDurationSeconds: 5, normalizedDurationSeconds: 6, @@ -382,6 +389,12 @@ describe("video-generation runtime", () => { resolution: undefined, }); expect(result.ignoredOverrides).toEqual([]); + expect(result.normalization).toMatchObject({ + aspectRatio: { + applied: "16:9", + derivedFrom: "size", + }, + }); expect(result.metadata).toMatchObject({ requestedSize: "1280x720", normalizedAspectRatio: "16:9",