fix: clear changed gate regressions

This commit is contained in:
Peter Steinberger
2026-05-01 08:33:36 +01:00
parent a379ac0562
commit 9d21df251e
3 changed files with 81 additions and 27 deletions

View File

@@ -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({

View File

@@ -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);
});
});

View File

@@ -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) {