mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 11:41:34 +00:00
* refactor(config): consolidate media model lists * refactor(config): unify memory configuration * refactor(config): consolidate TTS ownership * refactor(config): move typing policy to agents * refactor(config): retire product-level config surfaces * refactor(config): share scoped tool policy type * chore(config): refresh generated baselines * fix(config): honor agent typing overrides * fix(config): migrate sibling config consumers * refactor(infra): keep base64url decoder private * fix(config): strip invalid legacy TTS values * chore(config): refresh rebased baseline hash * fix(doctor): route legacy messages.tts.realtime voice to talk during tts move * refactor(config): polish final layout names * refactor(config): freeze retired tuning defaults * feat(config): add fast mode default symmetry * refactor(config): key agent entries by id * docs(config): update final layout reference * test(config): cover final layout migrations * chore(config): refresh final layout baselines * fix(config): align final layout runtime readers * fix(config): align remaining readers * fix(config): stabilize final layout migrations * fix(config): finalize config projection proof * fix(config): address final layout review * docs(release): preserve historical config names * fix(config): complete keyed agent migration * fix(config): close final migration gaps * fix(config): finish full-branch review * fix(config): complete runtime secret detection * fix(config): close final review findings * fix(config): finish canonical docs and heartbeat migration * fix(config): integrate latest main after rebase * refactor(env): isolate test-only controls * refactor(env): isolate build and development controls * refactor(env): collapse process identity indirection * refactor(env): remove duplicate config and temp aliases * docs(env): define the operator-facing allowlist * ci(env): ratchet production variable count * fix(env): remove stale provider helper import * fix(env): make ratchet sorting explicit * test(env): keep test seam in dead-code audit * test(env): cover ratchet growth and boundary; document surface budgets * docs(config): document tier-eval consolidations * docs(config): clarify speech preference ownership * test(memory): align retired tuning fixtures * refactor(memory): freeze engine heuristics * refactor(config): apply tier-eval tranche * refactor(tts): move persona shaping to providers * refactor(compaction): move prompt policy to providers * test(config): align hookified prompt fixtures * chore(deadcode): classify test-only exports * chore(github): remove unused spawn helper * chore(deadcode): classify queue diagnostics * chore(deadcode): remove unused lane snapshot export * chore(plugin-sdk): ratchet consolidated surface * fix(config): integrate latest main after rebase
373 lines
11 KiB
TypeScript
373 lines
11 KiB
TypeScript
// 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,
|
|
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("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("orders auto-detected provider defaults by canonical aliases", () => {
|
|
const candidates = resolveCapabilityModelCandidates({
|
|
cfg: {
|
|
agents: {
|
|
defaults: {
|
|
model: {
|
|
primary: "openai/gpt-5.5",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
modelConfig: undefined,
|
|
parseModelRef,
|
|
listProviders: () => [
|
|
{
|
|
id: "fal",
|
|
defaultModel: "fal-ai/flux/dev",
|
|
isConfigured: () => true,
|
|
},
|
|
{
|
|
id: "openai",
|
|
aliases: ["openai"],
|
|
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 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",
|
|
);
|
|
});
|
|
});
|