mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
test: simplify media runtime coverage
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user