






















1+// Qqbot tests cover channel-api tool behavior.
2+import { afterEach, 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: fetchWithSsrFGuardMock,
11+};
12+});
13+14+import { executeChannelApi } from "./channel-api.js";
15+16+function cancelTrackedResponse(
17+text: string,
18+init: ResponseInit,
19+): {
20+response: Response;
21+wasCanceled: () => boolean;
22+} {
23+let canceled = false;
24+const stream = new ReadableStream<Uint8Array>({
25+start(controller) {
26+controller.enqueue(new TextEncoder().encode(text));
27+},
28+cancel() {
29+canceled = true;
30+},
31+});
32+return {
33+response: new Response(stream, init),
34+wasCanceled: () => canceled,
35+};
36+}
37+38+describe("executeChannelApi", () => {
39+afterEach(() => {
40+vi.restoreAllMocks();
41+fetchWithSsrFGuardMock.mockReset();
42+});
43+44+it("uses guarded QQ API fetches and releases successful responses", async () => {
45+const release = vi.fn(async () => {});
46+fetchWithSsrFGuardMock.mockResolvedValueOnce({
47+response: new Response(JSON.stringify({ id: "guild-1" }), { status: 200 }),
48+ release,
49+});
50+51+const result = await executeChannelApi(
52+{ method: "GET", path: "/users/@me/guilds", query: { limit: "1" } },
53+{ accessToken: "token-1" },
54+);
55+56+expect(result.details).toEqual({
57+success: true,
58+status: 200,
59+path: "/users/@me/guilds",
60+data: { id: "guild-1" },
61+});
62+expect(release).toHaveBeenCalledTimes(1);
63+expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith({
64+url: "https://api.sgroup.qq.com/users/@me/guilds?limit=1",
65+init: {
66+method: "GET",
67+headers: {
68+Authorization: "QQBot token-1",
69+"Content-Type": "application/json",
70+},
71+signal: expect.any(AbortSignal),
72+},
73+auditContext: "qqbot-channel-api",
74+policy: {
75+hostnameAllowlist: ["api.sgroup.qq.com"],
76+allowRfc2544BenchmarkRange: true,
77+},
78+});
79+});
80+81+it("bounds error bodies without using response.text()", async () => {
82+const release = vi.fn(async () => {});
83+const tracked = cancelTrackedResponse(`${"channel api unavailable ".repeat(1024)}tail`, {
84+status: 503,
85+statusText: "Service Unavailable",
86+headers: { "content-type": "text/plain" },
87+});
88+const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
89+fetchWithSsrFGuardMock.mockResolvedValueOnce({
90+response: tracked.response,
91+ release,
92+});
93+94+const result = await executeChannelApi(
95+{ method: "GET", path: "/guilds/123/channels" },
96+{ accessToken: "token-1" },
97+);
98+99+expect(result.details).toMatchObject({
100+error: "503 Service Unavailable",
101+status: 503,
102+path: "/guilds/123/channels",
103+});
104+expect(JSON.stringify(result.details)).toContain("channel api unavailable");
105+expect(JSON.stringify(result.details)).not.toContain("tail");
106+expect(tracked.wasCanceled()).toBe(true);
107+expect(textSpy).not.toHaveBeenCalled();
108+expect(release).toHaveBeenCalledTimes(1);
109+});
110+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。