test(plugins): align release fixtures with runtime contracts

This commit is contained in:
Vincent Koc 2026-07-01 18:34:53 -07:00
parent 080481fec9
commit cc76e06e7a
No known key found for this signature in database
4 changed files with 41 additions and 135 deletions

View file

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

View file

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

View file

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

View file

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