mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
test(plugins): align release fixtures with runtime contracts
This commit is contained in:
parent
080481fec9
commit
cc76e06e7a
4 changed files with 41 additions and 135 deletions
|
|
@ -100,13 +100,12 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
return run;
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return Response.json(body, { status });
|
||||
}
|
||||
|
||||
it("succeeds on first attempt without retries", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-123" }),
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-123" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
const onRetry = vi.fn();
|
||||
|
|
@ -124,19 +123,8 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
|
||||
it("retries on 429 rate limit error and succeeds", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Too many requests" }),
|
||||
text: async () => "Too many requests",
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-456" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ message: "Too many requests" }, 429))
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-456" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
const onRetry = vi.fn();
|
||||
|
|
@ -157,21 +145,14 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
expect(retryCall?.[1]).toBeGreaterThanOrEqual(10);
|
||||
expect(retryCall?.[1]).toBeLessThanOrEqual(20);
|
||||
expect(retryCall?.[2]).toBeInstanceOf(Error);
|
||||
expect((retryCall?.[2] as Error | undefined)?.message).toBe(
|
||||
"Mattermost API 429 undefined: Too many requests",
|
||||
);
|
||||
expect((retryCall?.[2] as Error | undefined)?.message).toContain("Too many requests");
|
||||
});
|
||||
|
||||
it("retries on port 443 connection errors (not misclassified as 4xx)", async () => {
|
||||
// This tests that port numbers like :443 don't trigger false 4xx classification
|
||||
mockFetch
|
||||
.mockRejectedValueOnce(new Error("connect ECONNRESET 104.18.32.10:443"))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-port" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-port" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -190,13 +171,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
it("does not retry on 400 even if error message contains '429' text", async () => {
|
||||
// This tests that "429" in error detail doesn't trigger false rate-limit retry
|
||||
// e.g., "Invalid user ID: 4294967295" should NOT be retried
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Invalid user ID: 4294967295" }),
|
||||
text: async () => "Invalid user ID: 4294967295",
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ message: "Invalid user ID: 4294967295" }, 400));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -214,26 +189,9 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
|
||||
it("retries on 5xx server errors", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 503,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Service unavailable" }),
|
||||
text: async () => "Service unavailable",
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 502,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Bad gateway" }),
|
||||
text: async () => "Bad gateway",
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-789" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ message: "Service unavailable" }, 503))
|
||||
.mockResolvedValueOnce(jsonResponse({ message: "Bad gateway" }, 502))
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-789" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -252,12 +210,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
mockFetch
|
||||
.mockRejectedValueOnce(new Error("Network error: connection refused"))
|
||||
.mockRejectedValueOnce(new Error("ECONNRESET"))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-abc" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-abc" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -280,12 +233,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
code: "ECONNREFUSED",
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-fetch-failed" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-fetch-failed" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -301,13 +249,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
});
|
||||
|
||||
it("does not retry on 4xx client errors (except 429)", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Bad request" }),
|
||||
text: async () => "Bad request",
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ message: "Bad request" }, 400));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -323,13 +265,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
});
|
||||
|
||||
it("does not retry on 404 not found", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "User not found" }),
|
||||
text: async () => "User not found",
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ message: "User not found" }, 404));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -345,13 +281,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
});
|
||||
|
||||
it("throws after exhausting all retries", async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 503,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Service unavailable" }),
|
||||
text: async () => "Service unavailable",
|
||||
} as Response);
|
||||
mockFetch.mockImplementation(async () => jsonResponse({ message: "Service unavailable" }, 503));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -414,12 +344,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
.spyOn(globalThis, "setTimeout")
|
||||
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
|
||||
vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-capped" }),
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-capped" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -436,12 +361,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
mockFetch
|
||||
.mockRejectedValueOnce(new Error("Mattermost API 503 Service Unavailable"))
|
||||
.mockRejectedValueOnce(new Error("Mattermost API 503 Service Unavailable"))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-delay" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-delay" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -472,12 +392,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
.mockRejectedValueOnce(new Error("Mattermost API 503"))
|
||||
.mockRejectedValueOnce(new Error("Mattermost API 503"))
|
||||
.mockRejectedValueOnce(new Error("Mattermost API 503"))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-max" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-max" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -502,13 +417,9 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
it("does not retry on 4xx errors even if message contains retryable keywords", async () => {
|
||||
// This tests the fix for false positives where a 400 error with "timeout" in the message
|
||||
// would incorrectly be retried
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Request timeout: connection timed out" }),
|
||||
text: async () => "Request timeout: connection timed out",
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(
|
||||
jsonResponse({ message: "Request timeout: connection timed out" }, 400),
|
||||
);
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -525,13 +436,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
});
|
||||
|
||||
it("does not retry on 403 Forbidden even with 'abort' in message", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 403,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ message: "Request aborted: forbidden" }),
|
||||
text: async () => "Request aborted: forbidden",
|
||||
} as Response);
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ message: "Request aborted: forbidden" }, 403));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
@ -550,12 +455,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
let capturedSignal: AbortSignal | undefined;
|
||||
mockFetch.mockImplementationOnce((url, init) => {
|
||||
capturedSignal = init?.signal ?? undefined;
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-signal" }),
|
||||
} as Response);
|
||||
return Promise.resolve(jsonResponse({ id: "dm-channel-signal" }, 201));
|
||||
});
|
||||
|
||||
const client = createMockClient();
|
||||
|
|
@ -573,12 +473,7 @@ describe("createMattermostDirectChannelWithRetry", () => {
|
|||
// This tests the fix for the ordering bug: 503 with "upstream 404" should be retried
|
||||
mockFetch
|
||||
.mockRejectedValueOnce(new Error("Mattermost API 503: upstream returned 404 Not Found"))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
json: async () => ({ id: "dm-channel-5xx-with-404" }),
|
||||
} as Response);
|
||||
.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-5xx-with-404" }, 201));
|
||||
|
||||
const client = createMockClient();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import {
|
|||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
import { clearNvidiaFeaturedModelCacheForTests } from "./provider-catalog.js";
|
||||
import {
|
||||
clearNvidiaFeaturedModelCacheForTests,
|
||||
NVIDIA_FEATURED_MODELS_URL,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
const ssrfRuntimeMocks = vi.hoisted(() => ({
|
||||
fetchWithSsrFGuard: vi.fn(),
|
||||
|
|
@ -44,6 +47,7 @@ afterEach(() => {
|
|||
function mockFeaturedCatalogResponse(payload: unknown, status = 200) {
|
||||
ssrfRuntimeMocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
||||
response: Response.json(payload, { status }),
|
||||
finalUrl: NVIDIA_FEATURED_MODELS_URL,
|
||||
release: vi.fn(),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function mockFeaturedCatalogResponse(payload: unknown, status = 200) {
|
|||
const release = vi.fn();
|
||||
ssrfRuntimeMocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
||||
response: Response.json(payload, { status }),
|
||||
finalUrl: NVIDIA_FEATURED_MODELS_URL,
|
||||
release,
|
||||
});
|
||||
return release;
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@ describe("createTelegramBot command menu", () => {
|
|||
|
||||
const registered = registeredCommands();
|
||||
const skillCommands = resolveSkillCommands(config);
|
||||
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
||||
const native = listNativeCommandSpecsForConfig(config, {
|
||||
skillCommands,
|
||||
provider: "telegram",
|
||||
}).map((command) => ({
|
||||
command: normalizeTelegramCommandName(command.name),
|
||||
description: command.description,
|
||||
}));
|
||||
|
|
@ -183,7 +186,10 @@ describe("createTelegramBot command menu", () => {
|
|||
|
||||
const registered = registeredCommands();
|
||||
const skillCommands = resolveSkillCommands(config);
|
||||
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
||||
const native = listNativeCommandSpecsForConfig(config, {
|
||||
skillCommands,
|
||||
provider: "telegram",
|
||||
}).map((command) => ({
|
||||
command: normalizeTelegramCommandName(command.name),
|
||||
description: command.description,
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue