perf: slim slack media test imports

This commit is contained in:
Peter Steinberger
2026-04-23 23:47:37 +01:00
parent 789e71cdb8
commit 0999fec19b
3 changed files with 109 additions and 48 deletions

View File

@@ -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";

View File

@@ -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<Response>;
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 vi.fn<FetchMock>>): 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<typeof vi.fn<FetchMock>>;
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("<!DOCTYPE html><html><body>login</body></html>", {
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"),
);

View File

@@ -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);