diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts index e68300d2580..ada683fe123 100644 --- a/extensions/discord/src/monitor/provider.test.ts +++ b/extensions/discord/src/monitor/provider.test.ts @@ -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); diff --git a/extensions/discord/src/send.creates-thread.test.ts b/extensions/discord/src/send.creates-thread.test.ts index 4c24fb3409a..9386ee91749 100644 --- a/extensions/discord/src/send.creates-thread.test.ts +++ b/extensions/discord/src/send.creates-thread.test.ts @@ -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", () => { diff --git a/extensions/discord/src/voice-message.ts b/extensions/discord/src/voice-message.ts index 6da3f7349d2..8d485935005 100644 --- a/extensions/discord/src/voice-message.ts +++ b/extensions/discord/src/voice-message.ts @@ -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; diff --git a/extensions/feishu/runtime-api.ts b/extensions/feishu/runtime-api.ts index 97211ca75b2..fd73a1fcd62 100644 --- a/extensions/feishu/runtime-api.ts +++ b/extensions/feishu/runtime-api.ts @@ -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, diff --git a/extensions/irc/src/probe.test.ts b/extensions/irc/src/probe.test.ts index 05b8eda62e1..4a370b222b7 100644 --- a/extensions/irc/src/probe.test.ts +++ b/extensions/irc/src/probe.test.ts @@ -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, diff --git a/extensions/irc/src/setup.test.ts b/extensions/irc/src/setup.test.ts index cd73e3408aa..2ce877e6d44 100644 --- a/extensions/irc/src/setup.test.ts +++ b/extensions/irc/src/setup.test.ts @@ -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(), }); diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index 1e2c7d3836e..b5e08506ce2 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -180,6 +180,7 @@ function collectRuntimeApiPreExports(runtimeApiPath: string): string[] { true, ); const preExports = new Set(); + 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(); } diff --git a/extensions/matrix/src/matrix/monitor/index.test.ts b/extensions/matrix/src/matrix/monitor/index.test.ts index 6096df30fad..237bcf699b3 100644 --- a/extensions/matrix/src/matrix/monitor/index.test.ts +++ b/extensions/matrix/src/matrix/monitor/index.test.ts @@ -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); diff --git a/extensions/matrix/src/runtime-api.ts b/extensions/matrix/src/runtime-api.ts index 92c502f0c45..08d3477374b 100644 --- a/extensions/matrix/src/runtime-api.ts +++ b/extensions/matrix/src/runtime-api.ts @@ -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); + }, + }; +} diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts index 67911a6af2a..6c26ae220bb 100644 --- a/src/plugin-sdk/matrix.ts +++ b/src/plugin-sdk/matrix.ts @@ -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,