openclaw / src /media-generation /runtime-shared.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
25d91a4 verified
Raw History Blame Contribute Delete
16 kB
// Covers shared media-generation runtime polling and timeout helpers.
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.js";
import {
normalizeDurationToClosestMax,
resolveCapabilityModelCandidates,
resolveClosestAspectRatio,
resolveClosestResolution,
resolveClosestSize,
resolveMediaProviderRequestTimeoutMs,
resolveReferenceImageCapabilityError,
runMediaGenerationCandidates,
throwCapabilityGenerationFailure,
} from "./runtime-shared.js";
function parseModelRef(raw?: string) {
const trimmed = raw?.trim();
if (!trimmed) {
return null;
}
const slash = trimmed.indexOf("/");
if (slash <= 0 || slash === trimmed.length - 1) {
return null;
}
return {
provider: trimmed.slice(0, slash),
model: trimmed.slice(slash + 1),
};
}
describe("media-generation runtime shared candidates", () => {
it.each([
[0, undefined, undefined],
[1, { enabled: false }, "provider/model does not support reference-image edit inputs"],
[
2,
{ enabled: true, maxInputImages: 1 },
"provider/model supports at most 1 reference image, 2 requested",
],
[11, { enabled: true }, "provider/model supports at most 10 reference images, 11 requested"],
] as const)(
"validates finite reference-image capability for %s inputs",
(inputImageCount, edit, error) => {
expect(
resolveReferenceImageCapabilityError({
candidateRef: "provider/model",
inputImageCount,
edit,
}),
).toBe(error);
},
);
it("appends auth-backed provider defaults after explicit refs by default", () => {
const cfg = {
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
},
},
},
} as OpenClawConfig;
const candidates = resolveCapabilityModelCandidates({
cfg,
modelConfig: {
primary: "google/gemini-3.1-flash-image-preview",
fallbacks: ["fal/fal-ai/flux/dev"],
},
parseModelRef,
listProviders: () => [
{
id: "google",
defaultModel: "gemini-3.1-flash-image-preview",
isConfigured: () => true,
},
{
id: "openai",
defaultModel: "gpt-image-1",
isConfigured: () => true,
},
{
id: "minimax",
defaultModel: "image-01",
isConfigured: () => true,
},
],
});
expect(candidates).toEqual([
{ provider: "google", model: "gemini-3.1-flash-image-preview" },
{ provider: "fal", model: "fal-ai/flux/dev" },
{ provider: "openai", model: "gpt-image-1" },
{ provider: "minimax", model: "image-01" },
]);
});
it("auto-detects auth-backed provider defaults when no explicit media model is configured", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {} as OpenClawConfig,
modelConfig: undefined,
parseModelRef,
listProviders: () => [
{
id: "openai",
defaultModel: "gpt-image-1",
isConfigured: () => true,
},
{
id: "fal",
defaultModel: "fal-ai/flux/dev",
isConfigured: () => true,
},
],
});
expect(candidates).toEqual([
{ provider: "openai", model: "gpt-image-1" },
{ provider: "fal", model: "fal-ai/flux/dev" },
]);
});
it("auto-detects config-only providers that do not implement custom readiness", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {
models: {
providers: {
"media-config-only": {
apiKey: "config-only-media-key",
baseUrl: "https://media.example.test/v1",
models: [],
},
},
},
} as OpenClawConfig,
modelConfig: undefined,
parseModelRef,
listProviders: () => [
{
id: "media-config-only",
defaultModel: "configured-video",
},
],
});
expect(candidates).toEqual([{ provider: "media-config-only", model: "configured-video" }]);
});
it("preserves an owner readiness veto even when generic config contains an API key", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {
models: {
providers: {
"media-config-only": {
apiKey: "config-only-media-key",
baseUrl: "https://media.example.test/v1",
models: [],
},
},
},
} as OpenClawConfig,
modelConfig: undefined,
parseModelRef,
listProviders: () => [
{
id: "media-config-only",
defaultModel: "configured-video",
isConfigured: () => false,
},
],
});
expect(candidates).toEqual([]);
});
it("orders auto-detected provider defaults by canonical aliases", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {
agents: {
defaults: {
model: {
primary: "media-alias/gpt-5.5",
},
},
},
} as OpenClawConfig,
modelConfig: undefined,
parseModelRef,
listProviders: () => [
{
id: "fal",
defaultModel: "fal-ai/flux/dev",
isConfigured: () => true,
},
{
id: "openai",
aliases: ["media-alias"],
defaultModel: "gpt-image-2",
isConfigured: () => true,
},
],
});
expect(candidates).toEqual([
{ provider: "openai", model: "gpt-image-2" },
{ provider: "fal", model: "fal-ai/flux/dev" },
]);
});
it("keeps implicit provider expansion enabled when the retired opt-out is present", () => {
let listProviderCalls = 0;
const candidates = resolveCapabilityModelCandidates({
cfg: {
agents: {
defaults: {
mediaGenerationAutoProviderFallback: false,
},
},
} as OpenClawConfig,
modelConfig: {
primary: "google/gemini-3.1-flash-image-preview",
},
parseModelRef,
listProviders: () => {
listProviderCalls += 1;
return [
{
id: "openai",
defaultModel: "gpt-image-1",
isConfigured: () => true,
},
];
},
});
expect(candidates).toEqual([
{ provider: "google", model: "gemini-3.1-flash-image-preview" },
{ provider: "openai", model: "gpt-image-1" },
]);
expect(listProviderCalls).toBe(1);
});
it("treats an explicit model override as exact-only", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {
agents: {
defaults: {
mediaGenerationAutoProviderFallback: false,
},
},
} as OpenClawConfig,
modelConfig: {
primary: "google/gemini-3.1-flash-image-preview",
fallbacks: ["fal/fal-ai/flux/dev"],
},
modelOverride: "openai/gpt-image-2",
parseModelRef,
listProviders: () => [
{
id: "google",
defaultModel: "gemini-3.1-flash-image-preview",
isConfigured: () => true,
},
],
});
expect(candidates).toEqual([{ provider: "openai", model: "gpt-image-2" }]);
});
it("resolves slash-containing provider model IDs from registered provider models", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {} as OpenClawConfig,
modelConfig: {
primary: "openai/gpt-image-2",
},
modelOverride: "fal-ai/flux/dev",
parseModelRef,
listProviders: () => [
{
id: "fal",
defaultModel: "fal-ai/flux/dev",
models: ["fal-ai/flux/dev", "fal-ai/flux/dev/image-to-image"],
isConfigured: () => true,
},
],
});
expect(candidates).toEqual([{ provider: "fal", model: "fal-ai/flux/dev" }]);
});
it("prefers explicit provider refs over colliding slash-containing model IDs", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {} as OpenClawConfig,
modelConfig: {
primary: "google/lyria-3-pro-preview",
},
parseModelRef,
listProviders: () => [
{
id: "google",
defaultModel: "lyria-3-clip-preview",
models: ["lyria-3-clip-preview", "lyria-3-pro-preview"],
isConfigured: () => true,
},
{
id: "openrouter",
defaultModel: "google/lyria-3-clip-preview",
models: ["google/lyria-3-clip-preview", "google/lyria-3-pro-preview"],
isConfigured: () => true,
},
],
});
expect(candidates[0]).toEqual({ provider: "google", model: "lyria-3-pro-preview" });
});
});
describe("media-generation candidate lifecycle", () => {
it("preserves missing, skipped, and failed attempts before the first usable result", async () => {
const calls: string[] = [];
const result = await runMediaGenerationCandidates({
candidates: ["missing", "skipped", "failed", "success", "unused"].map((provider) => ({
provider,
model: "model",
})),
capability: "image",
includeSkipFailureDetails: true,
getProvider(id) {
calls.push(`lookup:${id}`);
return id === "missing" ? undefined : { id };
},
prepareCandidate(candidate) {
if (candidate.provider === "skipped") {
return "reference inputs unsupported";
}
return async (attempts) => {
calls.push(`generate:${candidate.provider}`);
if (candidate.provider === "failed") {
throw new Error("generation failed");
}
return { model: "selected-model", attempts };
};
},
});
expect(result.model).toBe("selected-model");
expect(result.attempts).toStrictEqual([
{
provider: "missing",
model: "model",
error: "No image-generation provider registered for missing",
},
{
provider: "skipped",
model: "model",
error: "reference inputs unsupported",
reason: undefined,
status: undefined,
code: undefined,
},
{
provider: "failed",
model: "model",
error: "generation failed",
reason: undefined,
status: undefined,
code: undefined,
},
]);
expect(calls).toEqual([
"lookup:missing",
"lookup:skipped",
"lookup:failed",
"generate:failed",
"lookup:success",
"generate:success",
]);
});
it.each(["lookup", "prepare", "async prepare"])(
"propagates %s failures without submitting a fallback",
async (stage) => {
const error = new Error("provider registry unavailable");
const lookedUp: string[] = [];
let executions = 0;
const result = runMediaGenerationCandidates({
candidates: [
{ provider: "primary", model: "model" },
{ provider: "fallback", model: "model" },
],
capability: "video",
getProvider(id) {
lookedUp.push(id);
if (stage === "lookup") {
throw error;
}
return { id };
},
prepareCandidate() {
if (stage === "prepare") {
throw error;
}
if (stage === "async prepare") {
return Promise.reject(error);
}
return async () => {
executions += 1;
return "unexpected generation";
};
},
});
await expect(result).rejects.toBe(error);
expect(lookedUp).toEqual(["primary"]);
expect(executions).toBe(0);
},
);
});
describe("media-generation runtime shared normalization", () => {
it("caps media provider timeouts to the timer-safe range", () => {
expect(
resolveMediaProviderRequestTimeoutMs({
timeoutMs: Number.MAX_SAFE_INTEGER,
providerDefaultTimeoutMs: 30_000,
}),
).toBe(MAX_TIMER_TIMEOUT_MS);
expect(
resolveMediaProviderRequestTimeoutMs({
timeoutMs: 0,
providerDefaultTimeoutMs: 45_000,
}),
).toBe(45_000);
});
it("rejects unsafe size dimensions before deriving ratios", () => {
expect(
resolveClosestSize({
requestedSize: "9007199254740993x3",
supportedSizes: ["1024x1024", "1536x1024"],
}),
).toBeUndefined();
});
it("maps unsupported sizes to the closest supported size", () => {
expect(
resolveClosestSize({
requestedSize: "1792x1024",
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
}),
).toBe("1536x1024");
});
it("maps unsupported aspect ratios to the closest supported aspect ratio", () => {
expect(
resolveClosestAspectRatio({
requestedAspectRatio: "17:10",
supportedAspectRatios: ["1:1", "4:3", "16:9"],
}),
).toBe("16:9");
});
it("maps unsupported resolutions to the closest supported resolution", () => {
expect(
resolveClosestResolution({
requestedResolution: "2K",
supportedResolutions: ["1K", "4K"],
}),
).toBe("1K");
});
it("maps video-style resolutions by numeric distance", () => {
expect(
resolveClosestResolution({
requestedResolution: "480P",
supportedResolutions: ["360P", "540P", "720P"],
order: ["360P", "480P", "540P", "720P"],
}),
).toBe("540P");
});
it("does not map across image and video resolution units", () => {
expect(
resolveClosestResolution({
requestedResolution: "4K",
supportedResolutions: ["768P", "1080P"],
order: ["360P", "480P", "540P", "720P", "768P", "1080P"],
}),
).toBeUndefined();
});
it("clamps durations to the closest supported max", () => {
expect(normalizeDurationToClosestMax(12, 8)).toBe(8);
expect(normalizeDurationToClosestMax(6, 8)).toBe(6);
});
});
describe("media-generation runtime shared failure summaries", () => {
it("collapses abort cascades behind the non-abort failure", () => {
expect(() =>
throwCapabilityGenerationFailure({
capabilityLabel: "music generation",
attempts: [
{
provider: "google",
model: "lyria-3-clip-preview",
error: "Manually set deadline 1s is too short. Minimum allowed deadline is 10s.",
},
{
provider: "minimax",
model: "music-2.6",
error: "This operation was aborted",
},
{
provider: "minimax-portal",
model: "music-2.6",
error: "This operation was aborted",
},
],
lastError: new Error("This operation was aborted"),
}),
).toThrow(
"All music generation models failed (3): google/lyria-3-clip-preview: Manually set deadline 1s is too short. Minimum allowed deadline is 10s. | 2 fallback(s) aborted after the request was cancelled or timed out: minimax/music-2.6, minimax-portal/music-2.6",
);
});
it("summarizes all-aborted attempts once", () => {
expect(() =>
throwCapabilityGenerationFailure({
capabilityLabel: "music generation",
attempts: [
{
provider: "minimax",
model: "music-2.6",
error: "This operation was aborted",
},
{
provider: "minimax-portal",
model: "music-2.6",
error: "This operation was aborted",
},
],
lastError: new Error("This operation was aborted"),
}),
).toThrow(
"All music generation models failed (2): 2 fallback(s) aborted after the request was cancelled or timed out: minimax/music-2.6, minimax-portal/music-2.6",
);
});
});