Files
openclaw/extensions/discord/src/pluralkit.test.ts
2026-07-29 01:06:53 -04:00

209 lines
6.5 KiB
TypeScript

// Discord tests cover pluralkit plugin behavior.
import { describe, expect, it, vi } from "vitest";
import { fetchPluralKitMessageInfo } from "./pluralkit.js";
type MockResponse = {
status: number;
ok: boolean;
text: () => Promise<string>;
json: () => Promise<unknown>;
body: null;
arrayBuffer: () => Promise<Buffer>;
};
const buildResponse = (params: { status: number; body?: unknown }): MockResponse => {
const body = params.body;
const textPayload = typeof body === "string" ? body : body == null ? "" : JSON.stringify(body);
return {
status: params.status,
ok: params.status >= 200 && params.status < 300,
text: async () => textPayload,
json: async () => body ?? {},
body: null,
arrayBuffer: async () => Buffer.from(textPayload),
};
};
function cancelTrackedResponse(
text: string,
init: ResponseInit,
): {
response: Response;
wasCanceled: () => boolean;
} {
let canceled = false;
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(text));
},
cancel() {
canceled = true;
},
});
return {
response: new Response(stream, init),
wasCanceled: () => canceled,
};
}
describe("fetchPluralKitMessageInfo", () => {
it("returns null when disabled", async () => {
const fetcher = vi.fn();
const result = await fetchPluralKitMessageInfo({
messageId: "123",
config: { enabled: false },
fetcher: fetcher as unknown as typeof fetch,
});
expect(result).toBeNull();
expect(fetcher).not.toHaveBeenCalled();
});
it("returns null on 404", async () => {
const tracked = cancelTrackedResponse("missing", { status: 404 });
const fetcher = vi.fn(async () => tracked.response);
const result = await fetchPluralKitMessageInfo({
messageId: "missing",
config: { enabled: true },
fetcher: fetcher as unknown as typeof fetch,
});
expect(result).toBeNull();
expect(tracked.wasCanceled()).toBe(true);
});
it("returns payload and sends token when configured", async () => {
let receivedHeaders: Record<string, string> | undefined;
const fetcher = vi.fn(async (_url: string, init?: RequestInit) => {
receivedHeaders = init?.headers as Record<string, string> | undefined;
return buildResponse({
status: 200,
body: {
id: "123",
member: { id: "mem_1", name: "Alex" },
system: { id: "sys_1", name: "System" },
},
});
});
const result = await fetchPluralKitMessageInfo({
messageId: "123",
config: { enabled: true, token: "pk_test" },
fetcher: fetcher as unknown as typeof fetch,
});
expect(result?.member?.id).toBe("mem_1");
expect(receivedHeaders?.Authorization).toBe("pk_test");
});
it.each([
["null", "null"],
["array", "[]"],
])(
"rejects a %s PluralKit message envelope with a stable provider error",
async (_kind, body) => {
const fetcher = vi.fn(async () => buildResponse({ status: 200, body }));
await expect(
fetchPluralKitMessageInfo({
messageId: "123",
config: { enabled: true },
fetcher: fetcher as unknown as typeof fetch,
}),
).rejects.toThrow("PluralKit message: malformed JSON response");
},
);
it("aborts PluralKit response body reads that exceed the lookup timeout", async () => {
vi.useFakeTimers();
try {
let observedSignal: AbortSignal | undefined;
const fetcher = vi.fn<typeof fetch>(async (_url, init) => {
observedSignal = init?.signal ?? undefined;
const body = new ReadableStream<Uint8Array>({
start(controller) {
observedSignal?.addEventListener(
"abort",
() => controller.error(new DOMException("PluralKit lookup timed out", "AbortError")),
{ once: true },
);
},
});
return new Response(body, {
status: 200,
headers: { "content-type": "application/json" },
});
});
const lookupPromise = fetchPluralKitMessageInfo({
messageId: "slow-body",
config: { enabled: true },
fetcher,
});
await vi.advanceTimersByTimeAsync(0);
expect(fetcher).toHaveBeenCalledOnce();
expect(observedSignal?.aborted).toBe(false);
const lookupRejection = expect(lookupPromise).rejects.toThrow(/timed out|abort/i);
await vi.advanceTimersByTimeAsync(10_000);
await lookupRejection;
expect(observedSignal?.aborted).toBe(true);
} finally {
vi.useRealTimers();
}
});
it("relays parent cancellation to the PluralKit request", async () => {
const parent = new AbortController();
let observedSignal: AbortSignal | undefined;
const fetcher = vi.fn<typeof fetch>(async (_url, init) => {
observedSignal = init?.signal ?? undefined;
return await new Promise<Response>((_resolve, reject) => {
observedSignal?.addEventListener(
"abort",
() => {
const reason = observedSignal?.reason;
reject(reason instanceof Error ? reason : new Error("PluralKit request aborted"));
},
{ once: true },
);
});
});
const lookupPromise = fetchPluralKitMessageInfo({
messageId: "cancelled",
config: { enabled: true },
fetcher,
signal: parent.signal,
});
parent.abort(new Error("preflight stopped"));
await expect(lookupPromise).rejects.toThrow("preflight stopped");
expect(observedSignal?.aborted).toBe(true);
});
it("bounds PluralKit API error bodies without using response.text()", async () => {
const tracked = cancelTrackedResponse(`${"plural failure ".repeat(1024)}tail`, {
status: 500,
headers: { "content-type": "text/plain" },
});
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
const fetcher = vi.fn(async () => tracked.response);
let caught: Error | undefined;
try {
await fetchPluralKitMessageInfo({
messageId: "boom",
config: { enabled: true },
fetcher: fetcher as unknown as typeof fetch,
});
} catch (error) {
caught = error as Error;
}
expect(caught?.message).toContain("PluralKit API failed (500): plural failure");
expect(caught?.message).not.toContain("tail");
expect(caught?.message.length).toBeLessThan(8_400);
expect(tracked.wasCanceled()).toBe(true);
expect(textSpy).not.toHaveBeenCalled();
});
});