From 0999fec19b411776a12407c955e5a099d3ee13b7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 23:47:37 +0100 Subject: [PATCH] perf: slim slack media test imports --- extensions/slack/src/monitor/media.runtime.ts | 4 + extensions/slack/src/monitor/media.test.ts | 128 ++++++++++++------ extensions/slack/src/monitor/media.ts | 25 ++-- 3 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 extensions/slack/src/monitor/media.runtime.ts diff --git a/extensions/slack/src/monitor/media.runtime.ts b/extensions/slack/src/monitor/media.runtime.ts new file mode 100644 index 00000000000..fd4d2a7d5e2 --- /dev/null +++ b/extensions/slack/src/monitor/media.runtime.ts @@ -0,0 +1,4 @@ +export { fetchWithRuntimeDispatcher } from "openclaw/plugin-sdk/infra-runtime"; +export type { FetchLike, SavedMedia } from "openclaw/plugin-sdk/media-runtime"; +export { fetchRemoteMedia, saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime"; +export { logVerbose } from "openclaw/plugin-sdk/runtime-env"; diff --git a/extensions/slack/src/monitor/media.test.ts b/extensions/slack/src/monitor/media.test.ts index 871afeaeab0..c0944976da6 100644 --- a/extensions/slack/src/monitor/media.test.ts +++ b/extensions/slack/src/monitor/media.test.ts @@ -1,11 +1,4 @@ -import * as ssrf from "openclaw/plugin-sdk/infra-runtime"; -import * as mediaFetch from "openclaw/plugin-sdk/media-runtime"; -import type { SavedMedia } from "openclaw/plugin-sdk/media-runtime"; -import * as mediaStore from "openclaw/plugin-sdk/media-runtime"; -import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { type FetchMock, withFetchPreconnect } from "openclaw/plugin-sdk/testing"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { mockPinnedHostnameResolution } from "../../../../src/test-helpers/ssrf.js"; import { fetchWithSlackAuth, resolveSlackAttachmentContent, @@ -14,16 +7,84 @@ import { resolveSlackThreadStarter, resetSlackThreadStarterCacheForTest, } from "./media.js"; +import type { FetchLike, SavedMedia } from "./media.runtime.js"; +import * as mediaRuntime from "./media.runtime.js"; +import { logVerbose } from "./media.runtime.js"; -vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ - logVerbose: vi.fn(), - danger: (message: string) => message, - shouldLogVerbose: () => false, +type FetchMock = (input: RequestInfo | URL, init?: RequestInit) => Promise; + +const fetchRemoteMediaMock = vi.hoisted(() => + vi.fn( + async (params: { + url: string; + fetchImpl: FetchLike; + filePathHint?: string; + requestInit?: RequestInit; + }) => { + let response = await params.fetchImpl(params.url, { + ...params.requestInit, + dispatcher: {}, + } as RequestInit & { dispatcher: unknown }); + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (location) { + const source = new URL(params.url); + const redirect = new URL(location, source); + const sameOrigin = redirect.origin === source.origin; + response = await params.fetchImpl(redirect.toString(), { + ...(sameOrigin ? params.requestInit : {}), + redirect: "follow", + dispatcher: {}, + } as RequestInit & { dispatcher: unknown }); + } + } + if (response.status < 200 || response.status >= 300) { + throw new Error(`fetch failed: ${response.status}`); + } + return { + buffer: Buffer.from(await response.arrayBuffer()), + contentType: response.headers.get("content-type") ?? undefined, + fileName: params.filePathHint ?? new URL(params.url).pathname.split("/").at(-1), + }; + }, + ), +); +const saveMediaBufferMock = vi.hoisted(() => + vi.fn(async (_buffer: Buffer, contentType?: string) => ({ + id: "saved-media-id", + path: "/tmp/test.bin", + size: _buffer.byteLength, + contentType, + })), +); +const fetchWithRuntimeDispatcherMock = vi.hoisted(() => vi.fn()); +const logVerboseMock = vi.hoisted(() => vi.fn()); + +vi.mock("./media.runtime.js", () => ({ + fetchRemoteMedia: fetchRemoteMediaMock, + fetchWithRuntimeDispatcher: fetchWithRuntimeDispatcherMock, + logVerbose: logVerboseMock, + saveMediaBuffer: saveMediaBufferMock, })); +function withFetchPreconnect(fetchMock: ReturnType>): typeof fetch { + return Object.assign( + ((input: RequestInfo | URL, init?: RequestInit) => fetchMock(input, init)) as typeof fetch, + { mock: fetchMock.mock }, + ); +} + // Store original fetch const originalFetch = globalThis.fetch; let mockFetch: ReturnType>; + +beforeEach(() => { + fetchRemoteMediaMock.mockClear(); + fetchWithRuntimeDispatcherMock.mockClear(); + logVerboseMock.mockClear(); + saveMediaBufferMock.mockClear(); +}); + const createSavedMedia = (filePath: string, contentType: string): SavedMedia => ({ id: "saved-media-id", path: filePath, @@ -41,7 +102,7 @@ async function expectPrivateDownloadRedirect(params: { redirectedUrl: string; secondAuthorization: string | null; }) { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/test.jpg", "image/jpeg"), ); @@ -225,7 +286,6 @@ describe("resolveSlackMedia", () => { beforeEach(() => { mockFetch = vi.fn(); globalThis.fetch = mockFetch as unknown as typeof fetch; - mockPinnedHostnameResolution(); }); afterEach(() => { @@ -234,7 +294,7 @@ describe("resolveSlackMedia", () => { }); it("prefers url_private_download over url_private", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/test.jpg", "image/jpeg"), ); @@ -313,7 +373,7 @@ describe("resolveSlackMedia", () => { }); it("rejects HTML auth pages for non-HTML files", async () => { - const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer"); + const saveMediaBufferMock = vi.spyOn(mediaRuntime, "saveMediaBuffer"); mockFetch.mockResolvedValueOnce( new Response("login", { status: 200, @@ -332,7 +392,7 @@ describe("resolveSlackMedia", () => { }); it("allows expected HTML uploads", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/page.html", "text/html"), ); mockFetch.mockResolvedValueOnce( @@ -363,7 +423,7 @@ describe("resolveSlackMedia", () => { // video/mp4 for MP4 containers. Verify resolveSlackMedia preserves // the overridden audio/* type in its return value despite this. const saveMediaBufferMock = vi - .spyOn(mediaStore, "saveMediaBuffer") + .spyOn(mediaRuntime, "saveMediaBuffer") .mockResolvedValue(createSavedMedia("/tmp/voice.mp4", "video/mp4")); const mockResponse = new Response(Buffer.from("audio data"), { @@ -401,7 +461,7 @@ describe("resolveSlackMedia", () => { it("preserves original MIME for non-voice Slack files", async () => { const saveMediaBufferMock = vi - .spyOn(mediaStore, "saveMediaBuffer") + .spyOn(mediaRuntime, "saveMediaBuffer") .mockResolvedValue(createSavedMedia("/tmp/video.mp4", "video/mp4")); const mockResponse = new Response(Buffer.from("video data"), { @@ -434,7 +494,7 @@ describe("resolveSlackMedia", () => { }); it("falls through to next file when first file returns error", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/test.jpg", "image/jpeg"), ); @@ -463,7 +523,7 @@ describe("resolveSlackMedia", () => { }); it("returns all successfully downloaded files as an array", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockImplementation(async (buffer, _contentType) => { + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockImplementation(async (buffer, _contentType) => { const text = Buffer.from(buffer).toString("utf8"); if (text.includes("image a")) { return createSavedMedia("/tmp/a.jpg", "image/jpeg"); @@ -510,7 +570,7 @@ describe("resolveSlackMedia", () => { it("caps downloads to 8 files for large multi-attachment messages", async () => { const saveMediaBufferMock = vi - .spyOn(mediaStore, "saveMediaBuffer") + .spyOn(mediaRuntime, "saveMediaBuffer") .mockResolvedValue(createSavedMedia("/tmp/x.jpg", "image/jpeg")); mockFetch.mockImplementation(async () => { @@ -539,14 +599,14 @@ describe("resolveSlackMedia", () => { }); it("routes dispatcher-backed Slack media requests through runtime fetch", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/test.jpg", "image/jpeg"), ); globalThis.fetch = (async () => { throw new Error("global fetch should not receive dispatcher-backed Slack media requests"); }) as typeof fetch; const runtimeFetchSpy = vi - .spyOn(ssrf, "fetchWithRuntimeDispatcher") + .spyOn(mediaRuntime, "fetchWithRuntimeDispatcher") .mockImplementation(async () => { return new Response(Buffer.from("image data"), { status: 200, @@ -578,7 +638,6 @@ describe("Slack media SSRF policy", () => { beforeEach(() => { mockFetch = vi.fn(); globalThis.fetch = withFetchPreconnect(mockFetch); - mockPinnedHostnameResolution(); }); afterEach(() => { @@ -587,14 +646,14 @@ describe("Slack media SSRF policy", () => { }); it("passes ssrfPolicy with Slack CDN allowedHostnames and allowRfc2544BenchmarkRange to file downloads", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/test.jpg", "image/jpeg"), ); mockFetch.mockResolvedValueOnce( new Response(Buffer.from("img"), { status: 200, headers: { "content-type": "image/jpeg" } }), ); - const spy = vi.spyOn(mediaFetch, "fetchRemoteMedia"); + const spy = vi.spyOn(mediaRuntime, "fetchRemoteMedia"); await resolveSlackMedia({ files: [{ url_private: "https://files.slack.com/test.jpg", name: "test.jpg" }], @@ -615,22 +674,14 @@ describe("Slack media SSRF policy", () => { }); it("passes ssrfPolicy to forwarded attachment image downloads", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/fwd.jpg", "image/jpeg"), ); - vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => { - const normalized = hostname.trim().toLowerCase().replace(/\.$/, ""); - return { - hostname: normalized, - addresses: ["93.184.216.34"], - lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses: ["93.184.216.34"] }), - }; - }); mockFetch.mockResolvedValueOnce( new Response(Buffer.from("fwd"), { status: 200, headers: { "content-type": "image/jpeg" } }), ); - const spy = vi.spyOn(mediaFetch, "fetchRemoteMedia"); + const spy = vi.spyOn(mediaRuntime, "fetchRemoteMedia"); await resolveSlackAttachmentContent({ attachments: [{ is_share: true, image_url: "https://files.slack.com/forwarded.jpg" }], @@ -650,7 +701,6 @@ describe("resolveSlackAttachmentContent", () => { beforeEach(() => { mockFetch = vi.fn(); globalThis.fetch = mockFetch as unknown as typeof fetch; - mockPinnedHostnameResolution(); }); afterEach(() => { @@ -696,7 +746,7 @@ describe("resolveSlackAttachmentContent", () => { }); it("skips forwarded image URLs on non-Slack hosts", async () => { - const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer"); + const saveMediaBufferMock = vi.spyOn(mediaRuntime, "saveMediaBuffer"); const result = await resolveSlackAttachmentContent({ attachments: [{ is_share: true, image_url: "https://example.com/forwarded.jpg" }], @@ -710,7 +760,7 @@ describe("resolveSlackAttachmentContent", () => { }); it("downloads Slack-hosted images from forwarded shared attachments", async () => { - vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue( + vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue( createSavedMedia("/tmp/forwarded.jpg", "image/jpeg"), ); diff --git a/extensions/slack/src/monitor/media.ts b/extensions/slack/src/monitor/media.ts index 2c67cc065e1..79df40ec2cb 100644 --- a/extensions/slack/src/monitor/media.ts +++ b/extensions/slack/src/monitor/media.ts @@ -2,17 +2,24 @@ import type { WebClient as SlackWebClient } from "@slack/web-api"; import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeHostname } from "openclaw/plugin-sdk/host-runtime"; -import { fetchWithRuntimeDispatcher } from "openclaw/plugin-sdk/infra-runtime"; -import type { FetchLike } from "openclaw/plugin-sdk/media-runtime"; -import { fetchRemoteMedia } from "openclaw/plugin-sdk/media-runtime"; -import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime"; import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url"; -import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalLowercaseString, -} from "openclaw/plugin-sdk/text-runtime"; import type { SlackAttachment, SlackFile } from "../types.js"; +import { + type FetchLike, + fetchRemoteMedia, + fetchWithRuntimeDispatcher, + logVerbose, + saveMediaBuffer, +} from "./media.runtime.js"; + +function normalizeLowercaseStringOrEmpty(value: unknown): string { + return typeof value === "string" ? value.trim().toLowerCase() : ""; +} + +function normalizeOptionalLowercaseString(value: unknown): string | undefined { + const normalized = normalizeLowercaseStringOrEmpty(value); + return normalized || undefined; +} function isSlackHostname(hostname: string): boolean { const normalized = normalizeHostname(hostname);