




















11// Mattermost tests cover client plugin behavior.
22import { describe, expect, it, vi } from "vitest";
3+4+const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
5+6+vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {
7+const actual = await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>();
8+return {
9+ ...actual,
10+fetchWithSsrFGuard: (...args: unknown[]) => fetchWithSsrFGuardMock(...args),
11+};
12+});
13+314import {
415createMattermostClient,
516createMattermostPost,
@@ -49,6 +60,55 @@ function parseRequestJson(init: RequestInit | undefined): Record<string, unknown
4960return parsed as Record<string, unknown>;
5061}
516263+function streamingMattermostResponse(body: unknown): {
64+response: Response;
65+arrayBuffer: ReturnType<typeof vi.fn>;
66+} {
67+const encoded = new TextEncoder().encode(JSON.stringify(body));
68+const stream = new ReadableStream<Uint8Array>({
69+start(controller) {
70+controller.enqueue(encoded);
71+controller.close();
72+},
73+});
74+const arrayBuffer = vi.fn(async () => {
75+throw new Error("guarded Mattermost responses must stay streaming");
76+});
77+return {
78+response: {
79+ok: true,
80+status: 200,
81+statusText: "OK",
82+headers: new Headers({ "content-type": "application/json" }),
83+body: stream,
84+ arrayBuffer,
85+} as unknown as Response,
86+ arrayBuffer,
87+};
88+}
89+90+function cancelTrackedResponse(
91+text: string,
92+init: ResponseInit,
93+): {
94+response: Response;
95+wasCanceled: () => boolean;
96+} {
97+let canceled = false;
98+const stream = new ReadableStream<Uint8Array>({
99+start(controller) {
100+controller.enqueue(new TextEncoder().encode(text));
101+},
102+cancel() {
103+canceled = true;
104+},
105+});
106+return {
107+response: new Response(stream, init),
108+wasCanceled: () => canceled,
109+};
110+}
111+52112function createTestClient(response?: { status?: number; body?: unknown; contentType?: string }) {
53113const { mockFetch, calls } = createMockFetch(response);
54114const client = createMattermostClient({
@@ -98,6 +158,72 @@ describe("normalizeMattermostBaseUrl", () => {
98158// ── createMattermostClient ───────────────────────────────────────────
99159100160describe("createMattermostClient", () => {
161+it("keeps guarded Mattermost responses streaming until callers consume them", async () => {
162+const release = vi.fn(async () => {});
163+const { response, arrayBuffer } = streamingMattermostResponse({ id: "u1" });
164+fetchWithSsrFGuardMock.mockResolvedValueOnce({ response, release });
165+const client = createMattermostClient({
166+baseUrl: "https://chat.example.com",
167+botToken: "test-token",
168+});
169+170+await expect(client.request("/users/me")).resolves.toEqual({ id: "u1" });
171+172+expect(arrayBuffer).not.toHaveBeenCalled();
173+expect(release).toHaveBeenCalledTimes(1);
174+});
175+176+it("bounds and cancels guarded Mattermost error bodies", async () => {
177+const release = vi.fn(async () => {});
178+const tracked = cancelTrackedResponse(`${"upstream unavailable ".repeat(512)}tail`, {
179+status: 503,
180+statusText: "Service Unavailable",
181+headers: { "content-type": "text/plain" },
182+});
183+fetchWithSsrFGuardMock.mockResolvedValueOnce({ response: tracked.response, release });
184+const client = createMattermostClient({
185+baseUrl: "https://chat.example.com",
186+botToken: "test-token",
187+});
188+189+let caught: Error | undefined;
190+try {
191+await client.request("/users/me");
192+} catch (error) {
193+caught = error as Error;
194+}
195+196+expect(caught?.message).toContain("Mattermost API 503 Service Unavailable");
197+expect(caught?.message).toContain("upstream unavailable");
198+expect(caught?.message).not.toContain("tail");
199+expect(caught?.message.length).toBeLessThan(8_300);
200+expect(tracked.wasCanceled()).toBe(true);
201+expect(release).toHaveBeenCalledTimes(1);
202+});
203+204+it("releases guarded Mattermost responses when upstream body reads fail", async () => {
205+const release = vi.fn(async () => {});
206+const stream = new ReadableStream<Uint8Array>({
207+pull() {
208+throw new Error("upstream body failed");
209+},
210+});
211+fetchWithSsrFGuardMock.mockResolvedValueOnce({
212+response: new Response(stream, {
213+status: 200,
214+headers: { "content-type": "application/json" },
215+}),
216+ release,
217+});
218+const client = createMattermostClient({
219+baseUrl: "https://chat.example.com",
220+botToken: "test-token",
221+});
222+223+await expect(client.request("/users/me")).rejects.toThrow("upstream body failed");
224+expect(release).toHaveBeenCalledTimes(1);
225+});
226+101227it("creates a client with normalized baseUrl", () => {
102228const { mockFetch } = createMockFetch();
103229const client = createMattermostClient({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。