fix(ci): repair extension and discord test gates

This commit is contained in:
Peter Steinberger
2026-03-27 19:26:25 +00:00
parent febcb01128
commit 605c9306ab
10 changed files with 159 additions and 16 deletions

View File

@@ -623,6 +623,9 @@ describe("monitorDiscordProvider", () => {
it("continues startup when Discord daily slash-command create quota is exhausted", async () => {
const { RateLimitError } = await import("@buape/carbon");
const runtime = baseRuntime();
const request = new Request("https://discord.com/api/v10/applications/commands", {
method: "PUT",
});
const rateLimitError = new RateLimitError(
new Response(null, {
status: 429,
@@ -636,6 +639,7 @@ describe("monitorDiscordProvider", () => {
retry_after: 193.632,
global: false,
},
request,
);
rateLimitError.discordCode = 30034;
clientHandleDeployRequestMock.mockRejectedValueOnce(rateLimitError);

View File

@@ -413,6 +413,9 @@ describe("sendPollDiscord", () => {
});
function createMockRateLimitError(retryAfter = 0.001): RateLimitError {
const request = new Request("https://discord.com/api/v10/channels/789/messages", {
method: "POST",
});
const response = new Response(null, {
status: 429,
headers: {
@@ -420,11 +423,15 @@ function createMockRateLimitError(retryAfter = 0.001): RateLimitError {
"X-RateLimit-Bucket": "test-bucket",
},
});
return new RateLimitError(response, {
message: "You are being rate limited.",
retry_after: retryAfter,
global: false,
});
return new RateLimitError(
response,
{
message: "You are being rate limited.",
retry_after: retryAfter,
global: false,
},
request,
);
}
describe("retry rate limits", () => {

View File

@@ -283,11 +283,15 @@ export async function sendDiscordVoiceMessage(
retry_after?: number;
global?: boolean;
};
throw new RateLimitError(res, {
message: retryData.message ?? "You are being rate limited.",
retry_after: retryData.retry_after ?? 1,
global: retryData.global ?? false,
});
throw new RateLimitError(
res,
{
message: retryData.message ?? "You are being rate limited.",
retry_after: retryData.retry_after ?? 1,
global: retryData.global ?? false,
},
uploadUrlRequest,
);
}
const errorBody = (await res.json().catch(() => null)) as {
code?: number;

View File

@@ -1,6 +1,24 @@
// Private runtime barrel for the bundled Feishu extension.
// Keep this barrel thin and aligned with the local extension surface.
export type {
ChannelMessageActionName,
ChannelMeta,
ChannelOutboundAdapter,
OpenClawConfig as ClawdbotConfig,
OpenClawConfig,
OpenClawPluginApi,
PluginRuntime,
RuntimeEnv,
} from "openclaw/plugin-sdk/feishu";
export {
DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE,
buildChannelConfigSchema,
buildProbeChannelStatusSummary,
createActionGate,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/feishu";
export * from "openclaw/plugin-sdk/feishu";
export {
isRequestBodyLimitError,

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const resolveIrcAccountMock = vi.hoisted(() => vi.fn());
const buildIrcConnectOptionsMock = vi.hoisted(() => vi.fn());
@@ -16,9 +16,21 @@ vi.mock("./client.js", () => ({
connectIrcClient: connectIrcClientMock,
}));
import { probeIrc } from "./probe.js";
let probeIrc: typeof import("./probe.js").probeIrc;
describe("probeIrc", () => {
beforeEach(async () => {
vi.resetModules();
resolveIrcAccountMock.mockReset();
buildIrcConnectOptionsMock.mockReset();
connectIrcClientMock.mockReset();
({ probeIrc } = await import("./probe.js"));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns a configuration error when the IRC account is incomplete", async () => {
resolveIrcAccountMock.mockReturnValue({
configured: false,

View File

@@ -13,6 +13,7 @@ import {
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
import type { ResolvedIrcAccount } from "./accounts.js";
import { ircPlugin } from "./channel.js";
import { setIrcRuntime } from "./runtime.js";
import {
ircSetupAdapter,
parsePort,
@@ -56,6 +57,26 @@ function buildAccount(): ResolvedIrcAccount {
};
}
function installIrcRuntime() {
setIrcRuntime({
logging: {
shouldLogVerbose: vi.fn(() => false),
getChildLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
},
channel: {
activity: {
record: vi.fn(),
get: vi.fn(),
},
},
} as never);
}
describe("irc setup", () => {
afterEach(() => {
vi.clearAllMocks();
@@ -329,10 +350,16 @@ describe("irc setup", () => {
it("keeps startAccount pending until abort, then stops the monitor", async () => {
const stop = vi.fn();
vi.resetModules();
vi.doMock("../../../src/generated/bundled-channel-entries.generated.ts", () => ({
GENERATED_BUNDLED_CHANNEL_ENTRIES: [],
}));
hoisted.monitorIrcProvider.mockResolvedValue({ stop });
installIrcRuntime();
const { ircPlugin: runtimeMockedPlugin } = await import("./channel.js");
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: ircPlugin.gateway!.startAccount!,
startAccount: runtimeMockedPlugin.gateway!.startAccount!,
account: buildAccount(),
});

View File

@@ -180,6 +180,7 @@ function collectRuntimeApiPreExports(runtimeApiPath: string): string[] {
true,
);
const preExports = new Set<string>();
let pluginSdkLineRuntimeSeen = false;
for (const statement of runtimeApiFile.statements) {
if (!ts.isExportDeclaration(statement)) {
@@ -193,6 +194,7 @@ function collectRuntimeApiPreExports(runtimeApiPath: string): string[] {
continue;
}
if (moduleSpecifier === "openclaw/plugin-sdk/line-runtime") {
pluginSdkLineRuntimeSeen = true;
break;
}
const normalized = normalizeModuleSpecifier(moduleSpecifier);
@@ -206,6 +208,10 @@ function collectRuntimeApiPreExports(runtimeApiPath: string): string[] {
}
}
if (!pluginSdkLineRuntimeSeen) {
return [];
}
return Array.from(preExports).toSorted();
}

View File

@@ -111,6 +111,10 @@ vi.mock("../../resolve-targets.js", () => ({
resolveMatrixTargets: vi.fn(async () => []),
}));
vi.mock("../../../../../src/generated/bundled-channel-entries.generated.ts", () => ({
GENERATED_BUNDLED_CHANNEL_ENTRIES: [],
}));
vi.mock("../../runtime.js", () => ({
getMatrixRuntime: () => ({
config: {
@@ -460,7 +464,19 @@ describe("matrix plugin registration", () => {
loadRuntimeApiExportTypesViaJiti({
modulePath: runtimeApiPath,
exportNames: [],
realPluginSdkSpecifiers: [],
realPluginSdkSpecifiers: [
"openclaw/plugin-sdk/account-helpers",
"openclaw/plugin-sdk/allow-from",
"openclaw/plugin-sdk/channel-config-helpers",
"openclaw/plugin-sdk/channel-policy",
"openclaw/plugin-sdk/core",
"openclaw/plugin-sdk/directory-runtime",
"openclaw/plugin-sdk/extension-shared",
"openclaw/plugin-sdk/irc",
"openclaw/plugin-sdk/signal",
"openclaw/plugin-sdk/status-helpers",
"openclaw/plugin-sdk/text-runtime",
],
}),
).toEqual({});
}, 240_000);

View File

@@ -1,14 +1,30 @@
export {
DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE,
buildChannelConfigSchema,
buildProbeChannelStatusSummary,
collectStatusIssuesFromLastError,
createActionGate,
formatZonedTimestamp,
getChatChannelMeta,
jsonResult,
normalizeAccountId,
normalizeOptionalAccountId,
readNumberParam,
readReactionParams,
readStringArrayParam,
readStringParam,
} from "openclaw/plugin-sdk/matrix";
export * from "openclaw/plugin-sdk/matrix";
export {
assertHttpUrlTargetsPrivateNetwork,
buildTimeoutAbortSignal,
closeDispatcher,
createPinnedDispatcher,
resolvePinnedHostnameWithPolicy,
ssrfPolicyFromAllowPrivateNetwork,
type LookupFn,
type SsrFPolicy,
} from "openclaw/plugin-sdk/infra-runtime";
} from "openclaw/plugin-sdk/ssrf-runtime";
export {
dispatchReplyFromConfigWithSettledDispatcher,
ensureConfiguredAcpBindingReady,
@@ -17,3 +33,35 @@ export {
} from "openclaw/plugin-sdk/matrix-runtime-heavy";
// resolveMatrixAccountStringValues already comes from plugin-sdk/matrix.
// Re-exporting auth-precedence here makes Jiti try to define the same export twice.
export function buildTimeoutAbortSignal(params: { timeoutMs?: number; signal?: AbortSignal }): {
signal?: AbortSignal;
cleanup: () => void;
} {
const { timeoutMs, signal } = params;
if (!timeoutMs && !signal) {
return { signal: undefined, cleanup: () => {} };
}
if (!timeoutMs) {
return { signal, cleanup: () => {} };
}
const controller = new AbortController();
const timeoutId = setTimeout(controller.abort.bind(controller), timeoutMs);
const onAbort = () => controller.abort();
if (signal) {
if (signal.aborted) {
controller.abort();
} else {
signal.addEventListener("abort", onAbort, { once: true });
}
}
return {
signal: controller.signal,
cleanup: () => {
clearTimeout(timeoutId);
signal?.removeEventListener("abort", onAbort);
},
};
}

View File

@@ -37,6 +37,7 @@ export {
buildChannelKeyCandidates,
resolveChannelEntryMatch,
} from "../channels/plugins/channel-config.js";
export { getChatChannelMeta } from "./channel-plugin-common.js";
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
export {
deleteAccountFromConfigSection,