refactor(plugins): remove media test globals (#122636)

This commit is contained in:
Peter Steinberger
2026-08-12 07:07:19 -07:00
committed by GitHub
parent 85230cf76c
commit c5ba4efbd7
11 changed files with 57 additions and 134 deletions
-6
View File
@@ -5,7 +5,6 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { isLiveTestEnabled, readLiveTestConfig } from "openclaw/plugin-sdk/test-live"; import { isLiveTestEnabled, readLiveTestConfig } from "openclaw/plugin-sdk/test-live";
import { beforeAll, describe, expect, it } from "vitest"; import { beforeAll, describe, expect, it } from "vitest";
import plugin from "./index.js"; import plugin from "./index.js";
import { getComfyConfigForTesting } from "./test-support.js";
import { isComfyCapabilityConfigured } from "./workflow-runtime.js"; import { isComfyCapabilityConfigured } from "./workflow-runtime.js";
const LIVE = const LIVE =
@@ -123,9 +122,4 @@ describeLive("comfy live", () => {
expect(result.tracks[0]?.mimeType.startsWith("audio/")).toBe(true); expect(result.tracks[0]?.mimeType.startsWith("audio/")).toBe(true);
expect(result.tracks[0]?.buffer.byteLength).toBeGreaterThan(512); expect(result.tracks[0]?.buffer.byteLength).toBeGreaterThan(512);
}, 180_000); }, 180_000);
it("documents the effective comfy config shape for live debugging", () => {
const comfyConfig = getComfyConfigForTesting(cfg as never);
expect(typeof comfyConfig).toBe("object");
});
}); });
@@ -1,7 +1,6 @@
// Comfy tests cover image generation provider plugin behavior. // Comfy tests cover image generation provider plugin behavior.
import type { LookupAddress } from "node:dns"; import type { LookupAddress } from "node:dns";
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { buildComfyImageGenerationProvider } from "./image-generation-provider.js"; import { buildComfyImageGenerationProvider } from "./image-generation-provider.js";
import { import {
@@ -11,12 +10,23 @@ import {
mockComfyProviderApiKey, mockComfyProviderApiKey,
parseComfyJsonBody, parseComfyJsonBody,
} from "./test-helpers.js"; } from "./test-helpers.js";
import { setComfyFetchGuardForTesting } from "./test-support.js";
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ type FetchWithSsrFGuard = (typeof import("openclaw/plugin-sdk/ssrf-runtime"))["fetchWithSsrFGuard"];
const { fetchWithSsrFGuardMock, ssrfGuardState } = vi.hoisted(() => ({
fetchWithSsrFGuardMock: vi.fn(), fetchWithSsrFGuardMock: vi.fn(),
ssrfGuardState: {} as { actual?: FetchWithSsrFGuard },
})); }));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>();
ssrfGuardState.actual = actual.fetchWithSsrFGuard;
return {
...actual,
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
};
});
type FetchGuardRequest = { type FetchGuardRequest = {
url?: unknown; url?: unknown;
auditContext?: unknown; auditContext?: unknown;
@@ -28,7 +38,7 @@ type FetchGuardRequest = {
body?: BodyInit | null; body?: BodyInit | null;
}; };
}; };
type RealGuardParams = Parameters<typeof fetchWithSsrFGuard>[0]; type RealGuardParams = Parameters<FetchWithSsrFGuard>[0];
type RealGuardFetchImpl = NonNullable<RealGuardParams["fetchImpl"]>; type RealGuardFetchImpl = NonNullable<RealGuardParams["fetchImpl"]>;
type RealGuardLookupFn = NonNullable<RealGuardParams["lookupFn"]>; type RealGuardLookupFn = NonNullable<RealGuardParams["lookupFn"]>;
type RealGuardHarness = { type RealGuardHarness = {
@@ -206,9 +216,13 @@ function installRealComfyFetchGuard(options: RealComfyFetchOptions): RealGuardHa
}); });
}; };
setComfyFetchGuardForTesting(async (params) => { const actualFetchWithSsrFGuard = ssrfGuardState.actual;
if (!actualFetchWithSsrFGuard) {
throw new Error("expected actual SSRF guard");
}
fetchWithSsrFGuardMock.mockImplementation(async (params) => {
guardCalls.push(params); guardCalls.push(params);
return await fetchWithSsrFGuard({ return await actualFetchWithSsrFGuard({
...params, ...params,
fetchImpl, fetchImpl,
lookupFn, lookupFn,
@@ -219,11 +233,12 @@ function installRealComfyFetchGuard(options: RealComfyFetchOptions): RealGuardHa
describe("comfy image-generation provider", () => { describe("comfy image-generation provider", () => {
beforeEach(() => { beforeEach(() => {
fetchWithSsrFGuardMock.mockReset();
vi.clearAllMocks(); vi.clearAllMocks();
}); });
afterEach(() => { afterEach(() => {
setComfyFetchGuardForTesting(null); fetchWithSsrFGuardMock.mockReset();
vi.unstubAllEnvs(); vi.unstubAllEnvs();
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
@@ -353,7 +368,6 @@ describe("comfy image-generation provider", () => {
}); });
it("submits a local workflow, waits for history, and downloads images", async () => { it("submits a local workflow, waits for history, and downloads images", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: "local-prompt-1" }), { response: new Response(JSON.stringify({ prompt_id: "local-prompt-1" }), {
@@ -441,7 +455,6 @@ describe("comfy image-generation provider", () => {
}); });
it("honors local private-network access for service-discovery hostnames", async () => { it("honors local private-network access for service-discovery hostnames", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalImageResponses("compose-prompt-1"); mockLocalImageResponses("compose-prompt-1");
const provider = buildComfyImageGenerationProvider(); const provider = buildComfyImageGenerationProvider();
@@ -468,7 +481,6 @@ describe("comfy image-generation provider", () => {
}); });
it("keeps local public-looking hostnames strict without explicit private-network access", async () => { it("keeps local public-looking hostnames strict without explicit private-network access", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalImageResponses("public-host-prompt-1"); mockLocalImageResponses("public-host-prompt-1");
const provider = buildComfyImageGenerationProvider(); const provider = buildComfyImageGenerationProvider();
@@ -492,7 +504,6 @@ describe("comfy image-generation provider", () => {
}); });
it("keeps cloud service-discovery hostnames strict without explicit private-network access", async () => { it("keeps cloud service-discovery hostnames strict without explicit private-network access", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, { mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"), body: Buffer.from("cloud-data"),
contentType: "image/png", contentType: "image/png",
@@ -525,7 +536,6 @@ describe("comfy image-generation provider", () => {
}); });
it("honors explicit cloud private-network access for service-discovery hostnames", async () => { it("honors explicit cloud private-network access for service-discovery hostnames", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, { mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"), body: Buffer.from("cloud-data"),
contentType: "image/png", contentType: "image/png",
@@ -787,7 +797,6 @@ describe("comfy image-generation provider", () => {
}); });
it("caps oversized local workflow timeouts", async () => { it("caps oversized local workflow timeouts", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
const nowSpy = vi.spyOn(Date, "now"); const nowSpy = vi.spyOn(Date, "now");
nowSpy nowSpy
.mockReturnValueOnce(0) .mockReturnValueOnce(0)
@@ -836,7 +845,6 @@ describe("comfy image-generation provider", () => {
}); });
it("rejects generated image downloads that exceed the configured media cap", async () => { it("rejects generated image downloads that exceed the configured media cap", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: "local-prompt-1" }), { response: new Response(JSON.stringify({ prompt_id: "local-prompt-1" }), {
@@ -893,7 +901,6 @@ describe("comfy image-generation provider", () => {
}); });
it("reports malformed local workflow submit JSON as a provider error", async () => { it("reports malformed local workflow submit JSON as a provider error", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
const release = vi.fn(async () => {}); const release = vi.fn(async () => {});
fetchWithSsrFGuardMock.mockResolvedValueOnce({ fetchWithSsrFGuardMock.mockResolvedValueOnce({
response: new Response("{ nope", { response: new Response("{ nope", {
@@ -923,7 +930,6 @@ describe("comfy image-generation provider", () => {
}); });
it("bounds oversized local workflow submit responses and releases the request", async () => { it("bounds oversized local workflow submit responses and releases the request", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
const chunk = new Uint8Array(1024 * 1024); const chunk = new Uint8Array(1024 * 1024);
const totalBytes = 32 * chunk.length; const totalBytes = 32 * chunk.length;
let bytesPulled = 0; let bytesPulled = 0;
@@ -971,7 +977,6 @@ describe("comfy image-generation provider", () => {
}); });
it("uploads reference images for local edit workflows", async () => { it("uploads reference images for local edit workflows", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ name: "upload.png" }), { response: new Response(JSON.stringify({ name: "upload.png" }), {
@@ -1059,7 +1064,6 @@ describe("comfy image-generation provider", () => {
it("uses cloud endpoints, auth headers, and partner-node extra_data", async () => { it("uses cloud endpoints, auth headers, and partner-node extra_data", async () => {
mockComfyProviderApiKey(); mockComfyProviderApiKey();
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, { mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"), body: Buffer.from("cloud-data"),
contentType: "image/png", contentType: "image/png",
@@ -1120,7 +1124,6 @@ describe("comfy image-generation provider", () => {
it("uses plugin config env SecretRef auth for cloud workflows", async () => { it("uses plugin config env SecretRef auth for cloud workflows", async () => {
vi.stubEnv("COMFY_TEST_API_KEY", "comfy-secret-ref-key"); vi.stubEnv("COMFY_TEST_API_KEY", "comfy-secret-ref-key");
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, { mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"), body: Buffer.from("cloud-data"),
contentType: "image/png", contentType: "image/png",
@@ -1158,7 +1161,6 @@ describe("comfy image-generation provider", () => {
it("uses provider auth fallback for cloud workflows without plugin config API keys", async () => { it("uses provider auth fallback for cloud workflows without plugin config API keys", async () => {
vi.stubEnv("COMFY_API_KEY", "stale-env-key"); vi.stubEnv("COMFY_API_KEY", "stale-env-key");
mockComfyProviderApiKey("profile-key"); mockComfyProviderApiKey("profile-key");
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, { mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"), body: Buffer.from("cloud-data"),
contentType: "image/png", contentType: "image/png",
@@ -2,15 +2,19 @@
import { expectExplicitMusicGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts"; import { expectExplicitMusicGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js"; import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js";
import { setComfyFetchGuardForTesting } from "./test-support.js";
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
fetchWithSsrFGuardMock: vi.fn(), fetchWithSsrFGuardMock: vi.fn(),
})); }));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => ({
...(await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>()),
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
}));
describe("comfy music-generation provider", () => { describe("comfy music-generation provider", () => {
afterEach(() => { afterEach(() => {
setComfyFetchGuardForTesting(null); fetchWithSsrFGuardMock.mockReset();
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@@ -23,7 +27,6 @@ describe("comfy music-generation provider", () => {
}); });
it("runs a music workflow and returns audio outputs", async () => { it("runs a music workflow and returns audio outputs", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: "music-job-1" }), { response: new Response(JSON.stringify({ prompt_id: "music-job-1" }), {
@@ -101,7 +104,6 @@ describe("comfy music-generation provider", () => {
}); });
it("rejects generated music downloads that exceed the configured media cap", async () => { it("rejects generated music downloads that exceed the configured media cap", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: "music-job-1" }), { response: new Response(JSON.stringify({ prompt_id: "music-job-1" }), {
-22
View File
@@ -1,22 +0,0 @@
import type { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
type ComfyTestApi = {
getConfig: (cfg?: unknown) => Record<string, unknown>;
setFetchGuard: (impl: typeof fetchWithSsrFGuard | null) => void;
};
function getComfyTestApi(): ComfyTestApi {
const api = Reflect.get(globalThis, Symbol.for("openclaw.comfyTestApi"));
if (!api) {
throw new Error("Comfy test API is unavailable");
}
return api as ComfyTestApi;
}
export function setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
getComfyTestApi().setFetchGuard(impl);
}
export function getComfyConfigForTesting(cfg?: unknown): Record<string, unknown> {
return getComfyTestApi().getConfig(cfg);
}
@@ -7,13 +7,17 @@ import {
mockComfyProviderApiKey, mockComfyProviderApiKey,
parseComfyJsonBody, parseComfyJsonBody,
} from "./test-helpers.js"; } from "./test-helpers.js";
import { setComfyFetchGuardForTesting } from "./test-support.js";
import { buildComfyVideoGenerationProvider } from "./video-generation-provider.js"; import { buildComfyVideoGenerationProvider } from "./video-generation-provider.js";
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
fetchWithSsrFGuardMock: vi.fn(), fetchWithSsrFGuardMock: vi.fn(),
})); }));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => ({
...(await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>()),
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
}));
function parseJsonBody(call: number): Record<string, unknown> { function parseJsonBody(call: number): Record<string, unknown> {
return parseComfyJsonBody(fetchWithSsrFGuardMock, call); return parseComfyJsonBody(fetchWithSsrFGuardMock, call);
} }
@@ -89,11 +93,12 @@ function generateLocalVideo(outputNodeId?: string) {
describe("comfy video-generation provider", () => { describe("comfy video-generation provider", () => {
beforeEach(() => { beforeEach(() => {
fetchWithSsrFGuardMock.mockReset();
vi.clearAllMocks(); vi.clearAllMocks();
}); });
afterEach(() => { afterEach(() => {
setComfyFetchGuardForTesting(null); fetchWithSsrFGuardMock.mockReset();
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
@@ -118,7 +123,6 @@ describe("comfy video-generation provider", () => {
}); });
it("submits a local workflow, waits for history, and downloads videos", async () => { it("submits a local workflow, waits for history, and downloads videos", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: "local-video-1" }), { response: new Response(JSON.stringify({ prompt_id: "local-video-1" }), {
@@ -205,7 +209,6 @@ describe("comfy video-generation provider", () => {
}); });
it("returns only MP4 video entries from mixed images buckets", async () => { it("returns only MP4 video entries from mixed images buckets", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalVideoResponses({ mockLocalVideoResponses({
promptId: "local-video-mixed", promptId: "local-video-mixed",
outputs: { outputs: {
@@ -243,7 +246,6 @@ describe("comfy video-generation provider", () => {
}); });
it("accepts uppercase WEBM names from the images bucket", async () => { it("accepts uppercase WEBM names from the images bucket", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalVideoResponses({ mockLocalVideoResponses({
promptId: "local-video-webm", promptId: "local-video-webm",
outputs: { outputs: {
@@ -272,7 +274,6 @@ describe("comfy video-generation provider", () => {
}); });
it("rejects images-only workflow output for video generation", async () => { it("rejects images-only workflow output for video generation", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalVideoResponses({ mockLocalVideoResponses({
promptId: "local-video-images-only", promptId: "local-video-images-only",
outputs: { outputs: {
@@ -292,7 +293,6 @@ describe("comfy video-generation provider", () => {
}); });
it("preserves legacy videos bucket output without filename filtering", async () => { it("preserves legacy videos bucket output without filename filtering", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalVideoResponses({ mockLocalVideoResponses({
promptId: "local-video-legacy", promptId: "local-video-legacy",
outputs: { outputs: {
@@ -318,7 +318,6 @@ describe("comfy video-generation provider", () => {
}); });
it("rejects generated video downloads that exceed the configured media cap", async () => { it("rejects generated video downloads that exceed the configured media cap", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
fetchWithSsrFGuardMock fetchWithSsrFGuardMock
.mockResolvedValueOnce({ .mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: "local-video-1" }), { response: new Response(JSON.stringify({ prompt_id: "local-video-1" }), {
@@ -378,7 +377,6 @@ describe("comfy video-generation provider", () => {
it("uses cloud endpoints for video workflows", async () => { it("uses cloud endpoints for video workflows", async () => {
mockComfyProviderApiKey(); mockComfyProviderApiKey();
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, { mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-video-data"), body: Buffer.from("cloud-video-data"),
contentType: "video/mp4", contentType: "video/mp4",
+2 -15
View File
@@ -111,19 +111,6 @@ type ComfyWorkflowResult = {
outputNodeIds: string[]; outputNodeIds: string[];
}; };
let comfyFetchGuard = fetchWithSsrFGuard;
function setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
comfyFetchGuard = impl ?? fetchWithSsrFGuard;
}
if (process.env.VITEST === "true") {
Reflect.set(globalThis, Symbol.for("openclaw.comfyTestApi"), {
getConfig: getComfyConfig,
setFetchGuard: setComfyFetchGuardForTesting,
});
}
function readConfigInteger(config: ComfyProviderConfig, key: string): number | undefined { function readConfigInteger(config: ComfyProviderConfig, key: string): number | undefined {
const value = config[key]; const value = config[key];
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined; return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
@@ -327,7 +314,7 @@ async function readJsonResponse<T>(params: {
auditContext: string; auditContext: string;
errorPrefix: string; errorPrefix: string;
}): Promise<T> { }): Promise<T> {
const { response, release } = await comfyFetchGuard({ const { response, release } = await fetchWithSsrFGuard({
url: params.url, url: params.url,
init: params.init, init: params.init,
timeoutMs: params.timeoutMs, timeoutMs: params.timeoutMs,
@@ -581,7 +568,7 @@ async function downloadOutputFile(params: {
const viewPath = params.mode === "cloud" ? "/api/view" : "/view"; const viewPath = params.mode === "cloud" ? "/api/view" : "/view";
const auditContext = `comfy-${params.capability}-download`; const auditContext = `comfy-${params.capability}-download`;
const firstResponse = await comfyFetchGuard({ const firstResponse = await fetchWithSsrFGuard({
url: `${params.baseUrl}${viewPath}?${query.toString()}`, url: `${params.baseUrl}${viewPath}?${query.toString()}`,
init: { init: {
method: "GET", method: "GET",
@@ -8,8 +8,12 @@ const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
fetchWithSsrFGuardMock: vi.fn(), fetchWithSsrFGuardMock: vi.fn(),
})); }));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => ({
...(await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>()),
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
}));
import { buildFalImageGenerationProvider } from "./image-generation-provider.js"; import { buildFalImageGenerationProvider } from "./image-generation-provider.js";
import { setFalFetchGuardForTesting } from "./test-support.js";
const falApiKey = { apiKey: "fal-test-key", source: "env", mode: "api-key" } as const; const falApiKey = { apiKey: "fal-test-key", source: "env", mode: "api-key" } as const;
@@ -75,14 +79,14 @@ describe("fal image-generation provider", () => {
} }
beforeEach(() => { beforeEach(() => {
fetchWithSsrFGuardMock.mockReset();
vi.clearAllMocks(); vi.clearAllMocks();
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue(falApiKey); vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue(falApiKey);
setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
provider = buildFalImageGenerationProvider(); provider = buildFalImageGenerationProvider();
}); });
afterEach(() => { afterEach(() => {
setFalFetchGuardForTesting(null); fetchWithSsrFGuardMock.mockReset();
vi.useRealTimers(); vi.useRealTimers();
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
+2 -14
View File
@@ -154,18 +154,6 @@ type FalNetworkPolicy = {
trustedDownloadPolicy?: SsrFPolicy; trustedDownloadPolicy?: SsrFPolicy;
}; };
let falFetchGuard = fetchWithSsrFGuard;
function setFalFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
falFetchGuard = impl ?? fetchWithSsrFGuard;
}
if (process.env.VITEST === "true") {
const key = Symbol.for("openclaw.falTestApi");
const api = (Reflect.get(globalThis, key) as Record<string, unknown> | undefined) ?? {};
Reflect.set(globalThis, key, { ...api, setImageFetchGuard: setFalFetchGuardForTesting });
}
function matchesTrustedHostSuffix(hostname: string, trustedSuffix: string): boolean { function matchesTrustedHostSuffix(hostname: string, trustedSuffix: string): boolean {
const normalizedHost = normalizeLowercaseStringOrEmpty(hostname); const normalizedHost = normalizeLowercaseStringOrEmpty(hostname);
const normalizedSuffix = normalizeLowercaseStringOrEmpty(trustedSuffix); const normalizedSuffix = normalizeLowercaseStringOrEmpty(trustedSuffix);
@@ -609,7 +597,7 @@ async function fetchImageBuffer(
return undefined; return undefined;
} }
})(); })();
const { response, release } = await falFetchGuard({ const { response, release } = await fetchWithSsrFGuard({
url, url,
timeoutMs: resolveProviderOperationTimeoutMs({ timeoutMs: resolveProviderOperationTimeoutMs({
deadline, deadline,
@@ -782,7 +770,7 @@ export function buildFalImageGenerationProvider(): ImageGenerationProvider {
inputImages: req.inputImages ?? [], inputImages: req.inputImages ?? [],
}); });
} }
const { response, release } = await falFetchGuard({ const { response, release } = await fetchWithSsrFGuard({
url: `${baseUrl}/${model}`, url: `${baseUrl}/${model}`,
init: { init: {
method: "POST", method: "POST",
-22
View File
@@ -1,22 +0,0 @@
import type { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
type FalTestApi = {
setImageFetchGuard: (impl: typeof fetchWithSsrFGuard | null) => void;
setVideoFetchGuard: (impl: typeof fetchWithSsrFGuard | null) => void;
};
function getFalTestApi(): FalTestApi {
const api = Reflect.get(globalThis, Symbol.for("openclaw.falTestApi"));
if (!api) {
throw new Error("Fal test API is unavailable");
}
return api as FalTestApi;
}
export function setFalFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
getFalTestApi().setImageFetchGuard(impl);
}
export function setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
getFalTestApi().setVideoFetchGuard(impl);
}
@@ -4,15 +4,21 @@ import * as providerAuth from "openclaw/plugin-sdk/provider-auth-runtime";
import * as providerHttp from "openclaw/plugin-sdk/provider-http"; import * as providerHttp from "openclaw/plugin-sdk/provider-http";
import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts"; import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { setFalVideoFetchGuardForTesting } from "./test-support.js";
import { buildFalVideoGenerationProvider } from "./video-generation-provider.js"; import { buildFalVideoGenerationProvider } from "./video-generation-provider.js";
const { fetchGuardMock } = vi.hoisted(() => ({
fetchGuardMock: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => ({
...(await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>()),
fetchWithSsrFGuard: fetchGuardMock,
}));
function createMockRequestConfig() { function createMockRequestConfig() {
return {} as ReturnType<typeof providerHttp.resolveProviderHttpRequestConfig>["requestConfig"]; return {} as ReturnType<typeof providerHttp.resolveProviderHttpRequestConfig>["requestConfig"];
} }
describe("fal video generation provider", () => { describe("fal video generation provider", () => {
const fetchGuardMock = vi.fn();
function mockFalProviderRuntime() { function mockFalProviderRuntime() {
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({ vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "fal-key", apiKey: "fal-key",
@@ -30,7 +36,6 @@ describe("fal video generation provider", () => {
requestConfig: createMockRequestConfig(), requestConfig: createMockRequestConfig(),
}); });
vi.spyOn(providerHttp, "assertOkOrThrowHttpError").mockResolvedValue(undefined); vi.spyOn(providerHttp, "assertOkOrThrowHttpError").mockResolvedValue(undefined);
setFalVideoFetchGuardForTesting(fetchGuardMock as never);
} }
function releasedJson(value: unknown) { function releasedJson(value: unknown) {
@@ -109,7 +114,6 @@ describe("fal video generation provider", () => {
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
fetchGuardMock.mockReset(); fetchGuardMock.mockReset();
setFalVideoFetchGuardForTesting(null);
}); });
it("declares explicit mode capabilities", () => { it("declares explicit mode capabilities", () => {
+2 -14
View File
@@ -97,18 +97,6 @@ type FalQueueResponse = {
}; };
}; };
let falFetchGuard = fetchWithSsrFGuard;
function setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | null): void {
falFetchGuard = impl ?? fetchWithSsrFGuard;
}
if (process.env.VITEST === "true") {
const key = Symbol.for("openclaw.falTestApi");
const api = (Reflect.get(globalThis, key) as Record<string, unknown> | undefined) ?? {};
Reflect.set(globalThis, key, { ...api, setVideoFetchGuard: setFalVideoFetchGuardForTesting });
}
function normalizeFalVideoUrl(value: unknown): string | undefined { function normalizeFalVideoUrl(value: unknown): string | undefined {
const normalized = normalizeOptionalString(value); const normalized = normalizeOptionalString(value);
if (!normalized && value !== undefined && value !== null) { if (!normalized && value !== undefined && value !== null) {
@@ -208,7 +196,7 @@ async function downloadFalVideo(
policy: SsrFPolicy | undefined, policy: SsrFPolicy | undefined,
maxBytes: number, maxBytes: number,
): Promise<GeneratedVideoAsset> { ): Promise<GeneratedVideoAsset> {
const { response, release } = await falFetchGuard({ const { response, release } = await fetchWithSsrFGuard({
url, url,
timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
policy, policy,
@@ -468,7 +456,7 @@ async function fetchFalJson(params: {
auditContext: string; auditContext: string;
errorContext: string; errorContext: string;
}): Promise<unknown> { }): Promise<unknown> {
const { response, release } = await falFetchGuard({ const { response, release } = await fetchWithSsrFGuard({
url: params.url, url: params.url,
init: params.init, init: params.init,
timeoutMs: params.timeoutMs, timeoutMs: params.timeoutMs,