mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix: clear changed gate regressions
This commit is contained in:
@@ -1,6 +1,27 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { discoverKilocodeModels, KILOCODE_MODELS_URL } from "./provider-models.js";
|
||||
|
||||
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
|
||||
fetchWithSsrFGuardMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
|
||||
}));
|
||||
|
||||
type MockKilocodeFetchResponse = {
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
json?: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
type MockKilocodeFetch = ((
|
||||
url: string,
|
||||
init?: RequestInit,
|
||||
) => Promise<MockKilocodeFetchResponse>) & {
|
||||
mock: { calls: unknown[][] };
|
||||
};
|
||||
|
||||
function makeGatewayModel(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
@@ -51,20 +72,24 @@ function makeAutoModel(overrides: Record<string, unknown> = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
async function withFetchPathTest(
|
||||
mockFetch: ReturnType<typeof vi.fn>,
|
||||
runAssertions: () => Promise<void>,
|
||||
) {
|
||||
async function withFetchPathTest(mockFetch: MockKilocodeFetch, runAssertions: () => Promise<void>) {
|
||||
const origNodeEnv = process.env.NODE_ENV;
|
||||
const origVitest = process.env.VITEST;
|
||||
const release = vi.fn(async () => {});
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.VITEST;
|
||||
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
fetchWithSsrFGuardMock.mockImplementation(
|
||||
async (params: { url: string; init?: RequestInit }) => ({
|
||||
response: await mockFetch(params.url, params.init),
|
||||
release,
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await runAssertions();
|
||||
} finally {
|
||||
fetchWithSsrFGuardMock.mockReset();
|
||||
if (origNodeEnv === undefined) {
|
||||
delete process.env.NODE_ENV;
|
||||
} else {
|
||||
@@ -75,7 +100,6 @@ async function withFetchPathTest(
|
||||
} else {
|
||||
process.env.VITEST = origVitest;
|
||||
}
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +135,14 @@ describe("discoverKilocodeModels (fetch path)", () => {
|
||||
await withFetchPathTest(mockFetch, async () => {
|
||||
const models = await discoverKilocodeModels();
|
||||
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: KILOCODE_MODELS_URL,
|
||||
init: expect.objectContaining({
|
||||
headers: { Accept: "application/json" },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
KILOCODE_MODELS_URL,
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { TwilioApiError, twilioApiRequest } from "./api.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const apiMocks = vi.hoisted(() => ({
|
||||
fetchWithSsrFGuard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
fetchWithSsrFGuard: apiMocks.fetchWithSsrFGuard,
|
||||
}));
|
||||
|
||||
describe("twilioApiRequest", () => {
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
apiMocks.fetchWithSsrFGuard.mockReset();
|
||||
});
|
||||
|
||||
it("posts form bodies with basic auth and parses json", async () => {
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
return new Response(JSON.stringify({ sid: "CA123" }), { status: 200 });
|
||||
}) as unknown as typeof fetch;
|
||||
const release = vi.fn(async () => {});
|
||||
apiMocks.fetchWithSsrFGuard.mockResolvedValue({
|
||||
response: new Response(JSON.stringify({ sid: "CA123" }), { status: 200 }),
|
||||
release,
|
||||
});
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
@@ -26,8 +34,10 @@ describe("twilioApiRequest", () => {
|
||||
}),
|
||||
).resolves.toEqual({ sid: "CA123" });
|
||||
|
||||
const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0] ?? [];
|
||||
const [{ url, init, auditContext, policy }] = apiMocks.fetchWithSsrFGuard.mock.calls[0] ?? [];
|
||||
expect(url).toBe("https://api.twilio.com/Calls.json");
|
||||
expect(auditContext).toBe("voice-call.twilio.api");
|
||||
expect(policy).toEqual({ allowedHostnames: ["api.twilio.com"] });
|
||||
expect(init).toEqual(
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
@@ -44,6 +54,7 @@ describe("twilioApiRequest", () => {
|
||||
expect(requestBody.toString()).toBe(
|
||||
"To=%2B14155550123&StatusCallbackEvent=initiated&StatusCallbackEvent=completed",
|
||||
);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes through URLSearchParams, allows 404s, and returns undefined for empty bodies", async () => {
|
||||
@@ -51,7 +62,11 @@ describe("twilioApiRequest", () => {
|
||||
new Response(null, { status: 204 }),
|
||||
new Response("missing", { status: 404 }),
|
||||
];
|
||||
globalThis.fetch = vi.fn(async () => responses.shift()!) as unknown as typeof fetch;
|
||||
const release = vi.fn(async () => {});
|
||||
apiMocks.fetchWithSsrFGuard.mockImplementation(async () => ({
|
||||
response: responses.shift()!,
|
||||
release,
|
||||
}));
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
@@ -73,12 +88,15 @@ describe("twilioApiRequest", () => {
|
||||
allowNotFound: true,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(release).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("throws twilio api errors for non-ok responses", async () => {
|
||||
globalThis.fetch = vi.fn(
|
||||
async () => new Response("bad request", { status: 400 }),
|
||||
) as unknown as typeof fetch;
|
||||
const release = vi.fn(async () => {});
|
||||
apiMocks.fetchWithSsrFGuard.mockResolvedValue({
|
||||
response: new Response("bad request", { status: 400 }),
|
||||
release,
|
||||
});
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
@@ -89,19 +107,21 @@ describe("twilioApiRequest", () => {
|
||||
body: {},
|
||||
}),
|
||||
).rejects.toThrow("Twilio API error: 400 bad request");
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("exposes structured Twilio error codes from json error bodies", async () => {
|
||||
globalThis.fetch = vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
code: 21220,
|
||||
message: "Call is not in-progress. Cannot redirect.",
|
||||
}),
|
||||
{ status: 400 },
|
||||
),
|
||||
) as unknown as typeof fetch;
|
||||
const release = vi.fn(async () => {});
|
||||
apiMocks.fetchWithSsrFGuard.mockResolvedValue({
|
||||
response: new Response(
|
||||
JSON.stringify({
|
||||
code: 21220,
|
||||
message: "Call is not in-progress. Cannot redirect.",
|
||||
}),
|
||||
{ status: 400 },
|
||||
),
|
||||
release,
|
||||
});
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
@@ -117,5 +137,6 @@ describe("twilioApiRequest", () => {
|
||||
twilioCode: 21220,
|
||||
message: "Twilio API error: 400 Call is not in-progress. Cannot redirect.",
|
||||
} satisfies Partial<TwilioApiError>);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,8 +71,9 @@ export async function twilioApiRequest<T = unknown>(params: {
|
||||
},
|
||||
body: bodyParams,
|
||||
},
|
||||
policy: { allowedHostnames: ["api.twilio.com"] },
|
||||
timeoutMs: TWILIO_API_TIMEOUT_MS,
|
||||
auditContext: "voice-call.twilio_api",
|
||||
auditContext: "voice-call.twilio.api",
|
||||
});
|
||||
try {
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user