diff --git a/extensions/qa-channel/setup-entry.ts b/extensions/qa-channel/setup-entry.ts index 3c27bd24cd3..23f42cbfac9 100644 --- a/extensions/qa-channel/setup-entry.ts +++ b/extensions/qa-channel/setup-entry.ts @@ -3,8 +3,8 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./channel-plugin-api.js", - exportName: "qaChannelPlugin", + specifier: "./setup-plugin-api.js", + exportName: "qaChannelSetupPlugin", }, runtime: { specifier: "./api.js", diff --git a/extensions/qa-channel/setup-plugin-api.ts b/extensions/qa-channel/setup-plugin-api.ts new file mode 100644 index 00000000000..f6a17f83dd3 --- /dev/null +++ b/extensions/qa-channel/setup-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled setup entry imports narrow so setup loads do not pull the +// broader QA channel runtime and gateway surface. +export { qaChannelSetupPlugin } from "./src/channel.setup.js"; diff --git a/extensions/qa-channel/src/channel.setup.ts b/extensions/qa-channel/src/channel.setup.ts new file mode 100644 index 00000000000..da636408c03 --- /dev/null +++ b/extensions/qa-channel/src/channel.setup.ts @@ -0,0 +1,43 @@ +import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common"; +import { + listQaChannelAccountIds, + resolveDefaultQaChannelAccountId, + resolveQaChannelAccount, + type ResolvedQaChannelAccount, +} from "./accounts.js"; +import { qaChannelPluginConfigSchema } from "./config-schema.js"; +import type { ChannelPlugin } from "./runtime-api.js"; +import { applyQaSetup } from "./setup.js"; +import type { CoreConfig } from "./types.js"; + +const CHANNEL_ID = "qa-channel" as const; +const meta = { ...getChatChannelMeta(CHANNEL_ID) }; + +export const qaChannelSetupPlugin: ChannelPlugin = { + id: CHANNEL_ID, + meta, + capabilities: { + chatTypes: ["direct", "group"], + }, + reload: { configPrefixes: ["channels.qa-channel"] }, + configSchema: qaChannelPluginConfigSchema, + setup: { + applyAccountConfig: ({ cfg, accountId, input }) => + applyQaSetup({ + cfg, + accountId, + input: input as Record, + }), + }, + config: { + listAccountIds: (cfg) => listQaChannelAccountIds(cfg as CoreConfig), + resolveAccount: (cfg, accountId) => + resolveQaChannelAccount({ cfg: cfg as CoreConfig, accountId }), + defaultAccountId: (cfg) => resolveDefaultQaChannelAccountId(cfg as CoreConfig), + isConfigured: (account) => account.configured, + resolveAllowFrom: ({ cfg, accountId }) => + resolveQaChannelAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom, + resolveDefaultTo: ({ cfg, accountId }) => + resolveQaChannelAccount({ cfg: cfg as CoreConfig, accountId }).config.defaultTo, + }, +}; diff --git a/extensions/qa-lab/src/lab-server.ts b/extensions/qa-lab/src/lab-server.ts index cca29c3edc4..1ad9a86d344 100644 --- a/extensions/qa-lab/src/lab-server.ts +++ b/extensions/qa-lab/src/lab-server.ts @@ -130,30 +130,33 @@ async function startQaGatewayLoop(params: { state: QaBusState; baseUrl: string } const cfg = createQaLabConfig(params.baseUrl); const account = qaChannelPlugin.config.resolveAccount(cfg, "default"); const abort = new AbortController(); - const task = qaChannelPlugin.gateway?.startAccount?.({ - accountId: account.accountId, - account, - cfg, - runtime: { - log: () => undefined, - error: () => undefined, - exit: () => undefined, - }, - abortSignal: abort.signal, - log: { - info: () => undefined, - warn: () => undefined, - error: () => undefined, - debug: () => undefined, - }, - getStatus: () => ({ - accountId: account.accountId, - configured: true, - enabled: true, - running: true, - }), - setStatus: () => undefined, - }); + const task = Promise.resolve().then( + async () => + await qaChannelPlugin.gateway?.startAccount?.({ + accountId: account.accountId, + account, + cfg, + runtime: { + log: () => undefined, + error: () => undefined, + exit: () => undefined, + }, + abortSignal: abort.signal, + log: { + info: () => undefined, + warn: () => undefined, + error: () => undefined, + debug: () => undefined, + }, + getStatus: () => ({ + accountId: account.accountId, + configured: true, + enabled: true, + running: true, + }), + setStatus: () => undefined, + }), + ); return { cfg, async stop() { diff --git a/extensions/qa-lab/src/runtime-api.ts b/extensions/qa-lab/src/runtime-api.ts index 07e7da977de..de44ddf1bdf 100644 --- a/extensions/qa-lab/src/runtime-api.ts +++ b/extensions/qa-lab/src/runtime-api.ts @@ -20,7 +20,7 @@ export { searchQaBusMessages, sendQaBusMessage, setQaChannelRuntime, -} from "openclaw/plugin-sdk/qa-channel"; +} from "@openclaw/qa-channel/api.js"; export type { QaBusAttachment, QaBusConversation, diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index a81129dd0fe..afd9305b26c 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -1135,12 +1135,8 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag await sleep(statusReactionTiming.errorHoldMs); if (anyReplyDelivered) { await statusReactions.clear(); - return; } - await statusReactions.restoreInitial(); })(); - } else { - void statusReactions.restoreInitial(); } } else if (anyReplyDelivered) { await statusReactions.setDone(); diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 6e16a7655b3..deeb7064293 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -404,7 +404,7 @@ const IMPORT_SPECIFIER_PATTERN = const BROAD_CHANGED_ENV_KEY = "OPENCLAW_TEST_CHANGED_BROAD"; const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS"; const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY"; -export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = "180000"; +export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = "300000"; const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map( Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]), ); diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index ef579460ea8..fadb3086e56 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -564,6 +564,8 @@ describe("buildStatusReply subagent summary", () => { { OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir, OPENCLAW_STATE_DIR: stateDir, + ANTHROPIC_API_KEY: undefined, + ANTHROPIC_OAUTH_TOKEN: undefined, WORKSPACE_STATUS_CREDENTIALS: credentialPath, }, async () => { diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index 1beafd7a896..c2952d56f1a 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -24,6 +24,7 @@ const PRIVATE_BUNDLED_SDK_SURFACE_PATTERN = const GENERIC_CORE_HELPER_FILES = ["src/polls.ts", "src/poll-params.ts"] as const; const GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN = /\b(?:bluebubbles|discord|feishu|googlechat|matrix|mattermost|msteams|slack|telegram|whatsapp|zalo|zalouser)\b/gi; +const PACKAGE_CONTRACT_SCAN_TIMEOUT_MS = 240_000; const DEPRECATED_EXTENSION_SDK_SPECIFIERS = new Set([ "openclaw/plugin-sdk", "openclaw/plugin-sdk/channel-config-schema-legacy", @@ -655,9 +656,13 @@ describe("plugin-sdk package contract guardrails", () => { expect(collectDeprecatedPackageTestingBridgeDrift()).toEqual([]); }); - it("keeps extension test-api exports consumed", () => { - expect(collectUnusedExtensionTestApiExports()).toEqual([]); - }); + it( + "keeps extension test-api exports consumed", + () => { + expect(collectUnusedExtensionTestApiExports()).toEqual([]); + }, + PACKAGE_CONTRACT_SCAN_TIMEOUT_MS, + ); it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => { expect(collectCrossOwnerReservedSdkImports()).toEqual([]); diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index 9bfa2babfc5..a7423567b7a 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -54,6 +54,7 @@ function loadCoverageRegistryEntries(): SecretRegistryEntry[] { const COVERAGE_REGISTRY_ENTRIES = loadCoverageRegistryEntries(); const DEBUG_COVERAGE_BATCHES = process.env.OPENCLAW_DEBUG_RUNTIME_COVERAGE === "1"; +const RUNTIME_COVERAGE_TEST_TIMEOUT_MS = 240_000; const COVERAGE_LOADABLE_PLUGIN_ORIGINS = buildCoverageLoadablePluginOrigins(COVERAGE_REGISTRY_ENTRIES); const PLUGIN_OWNED_OPENCLAW_COVERAGE_EXCLUSIONS = new Set([ @@ -564,19 +565,27 @@ describe("secrets runtime target coverage", () => { ({ resolveSecretRefValues } = resolver); }); - it("handles every core and channel openclaw.json registry target when configured as active", async () => { - await expectOpenClawCoverageEntriesResolved( - "openclaw.json core", - collectOpenClawCoverageEntries({ includePluginEntries: false }), - ); - }); + it( + "handles every core and channel openclaw.json registry target when configured as active", + async () => { + await expectOpenClawCoverageEntriesResolved( + "openclaw.json core", + collectOpenClawCoverageEntries({ includePluginEntries: false }), + ); + }, + RUNTIME_COVERAGE_TEST_TIMEOUT_MS, + ); - it("handles every plugin openclaw.json registry target when configured as active", async () => { - await expectOpenClawCoverageEntriesResolved( - "openclaw.json plugins", - collectOpenClawCoverageEntries({ includePluginEntries: true }), - ); - }); + it( + "handles every plugin openclaw.json registry target when configured as active", + async () => { + await expectOpenClawCoverageEntriesResolved( + "openclaw.json plugins", + collectOpenClawCoverageEntries({ includePluginEntries: true }), + ); + }, + RUNTIME_COVERAGE_TEST_TIMEOUT_MS, + ); it("handles every auth-profiles registry target", async () => { const entries = COVERAGE_REGISTRY_ENTRIES.filter(