mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:30:43 +00:00
perf: slim slack media test imports
This commit is contained in:
4
extensions/slack/src/monitor/media.runtime.ts
Normal file
4
extensions/slack/src/monitor/media.runtime.ts
Normal 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";
|
||||
@@ -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"),
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user