diff --git a/extensions/googlechat/src/monitor-ingress.test.ts b/extensions/googlechat/src/monitor-ingress.test.ts index 5e207879026..256edf66bd6 100644 --- a/extensions/googlechat/src/monitor-ingress.test.ts +++ b/extensions/googlechat/src/monitor-ingress.test.ts @@ -322,6 +322,12 @@ describe("Google Chat durable ingress", () => { await stopping; expect(stopped).toBe(true); expect(await queue.listClaims()).toHaveLength(0); + expect(await queue.listPending()).toEqual([ + expect.objectContaining({ + id: "spaces/AAA/messages/active", + lastError: expect.any(String), + }), + ]); }); }); diff --git a/extensions/irc/src/irc-ingress.test.ts b/extensions/irc/src/irc-ingress.test.ts index d691fdb5840..4e022c44c63 100644 --- a/extensions/irc/src/irc-ingress.test.ts +++ b/extensions/irc/src/irc-ingress.test.ts @@ -241,12 +241,12 @@ describe("IRC durable ingress", () => { const pausing = ingress.pause().then(() => { pauseSettled = true; }); - await connection.accept(CHANNEL_LINE, "bot"); + const secondAdmission = connection.accept(CHANNEL_LINE, "bot"); await Promise.resolve(); expect(pauseSettled).toBe(false); releaseDispatch.resolve(); - await pausing; + await Promise.all([pausing, secondAdmission]); expect(dispatch).toHaveBeenCalledOnce(); expect(await queue.listPending({ limit: "all" })).toEqual([ expect.objectContaining({ diff --git a/extensions/signal/src/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts b/extensions/signal/src/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts index aefae4b6f29..a83e3defbcf 100644 --- a/extensions/signal/src/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts +++ b/extensions/signal/src/monitor.tool-result.pairs-uuid-only-senders-uuid-allowlist-entry.test.ts @@ -7,6 +7,7 @@ import { installSignalToolResultTestHooks, setSignalToolResultTestConfig, toSignalToolResultTestError, + waitForSignalToolResultIngressIdle, } from "./monitor.tool-result.test-harness.js"; installSignalToolResultTestHooks(); @@ -63,6 +64,7 @@ describe("monitorSignalProvider tool results", () => { event: "receive", data: JSON.stringify(payload), }); + await waitForSignalToolResultIngressIdle(); abortController.abort(); }); @@ -264,6 +266,7 @@ describe("monitorSignalProvider tool results", () => { }, }), }); + await waitForSignalToolResultIngressIdle(); abortController.abort(); }); diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 0fbf0a5f346..ca4a960b92e 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -2509,7 +2509,10 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`; const { storePath } = storeFixture.makeTmpStorePath(); const now = Date.now(); const cfg = { - session: { store: storePath }, + session: { + store: storePath, + resetByType: { thread: { mode: "idle", idleMinutes: 60 } }, + }, channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, } as OpenClawConfig; const route = resolveAgentRoute({ @@ -2645,7 +2648,10 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`; const { storePath } = storeFixture.makeTmpStorePath(); const now = Date.now(); const cfg = { - session: { store: storePath }, + session: { + store: storePath, + resetByType: { thread: { mode: "idle", idleMinutes: 60 } }, + }, channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, } as OpenClawConfig; const route = resolveAgentRoute({ diff --git a/extensions/synology-chat/src/webhook-ingress.test.ts b/extensions/synology-chat/src/webhook-ingress.test.ts index fd296628578..c369db252c6 100644 --- a/extensions/synology-chat/src/webhook-ingress.test.ts +++ b/extensions/synology-chat/src/webhook-ingress.test.ts @@ -267,7 +267,10 @@ describe("Synology Chat durable ingress", () => { releaseDelivery(); await stopping; - expect(await queue.listClaims()).toHaveLength(1); + expect(await queue.listClaims()).toHaveLength(0); + expect(await queue.listPending()).toEqual([ + expect.objectContaining({ id: "post-active", lastError: expect.any(String) }), + ]); }); }); diff --git a/extensions/telegram/src/action-runtime.ts b/extensions/telegram/src/action-runtime.ts index 4cdf3f0115d..6d8f34ebee5 100644 --- a/extensions/telegram/src/action-runtime.ts +++ b/extensions/telegram/src/action-runtime.ts @@ -211,11 +211,15 @@ function readTelegramSendMediaUrls(params: Record) { function resolveTelegramButtonsFromParams( params: Record, presentation = normalizeMessagePresentation(params.presentation), + options?: { allowWebAppButtons?: boolean }, ) { - return resolveTelegramInlineButtons({ - presentation, - interactive: params.interactive, - }); + return resolveTelegramInlineButtons( + { + presentation, + interactive: params.interactive, + }, + options, + ); } function readTelegramSendContent(params: { @@ -459,7 +463,9 @@ export async function handleTelegramAction( const firstMediaUrl = mediaUrls[0]; const location = normalizeOutboundLocation(params.location); const presentation = normalizeMessagePresentation(params.presentation); - const buttons = resolveTelegramButtonsFromParams(params, presentation); + const buttons = resolveTelegramButtonsFromParams(params, presentation, { + allowWebAppButtons: resolveTelegramTargetChatType(to) === "direct", + }); const content = readTelegramSendContent({ args: params, mediaUrl: firstMediaUrl, @@ -698,7 +704,9 @@ export async function handleTelegramAction( readStringParam(params, "content", { allowEmpty: false }) ?? readStringParam(params, "message", { allowEmpty: false }); const caption = readStringParam(params, "caption", { allowEmpty: false }); - const buttons = resolveTelegramButtonsFromParams(params); + const buttons = resolveTelegramButtonsFromParams(params, undefined, { + allowWebAppButtons: resolveTelegramTargetChatType(chatId ?? "") === "direct", + }); if (content == null && caption == null && buttons === undefined) { throw new Error("content required."); } diff --git a/extensions/telegram/src/bot.test.ts b/extensions/telegram/src/bot.test.ts index a9c87c13170..a8923b7fa06 100644 --- a/extensions/telegram/src/bot.test.ts +++ b/extensions/telegram/src/bot.test.ts @@ -78,7 +78,7 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async () => { } const plan: import("openclaw/plugin-sdk/channel-inbound").ChannelInboundTurnPlan = resolved; - return { + const prepared: Awaited> = { ...plan, runDispatch: async () => await harness.dispatchReplyWithBufferedBlockDispatcher({ @@ -93,7 +93,14 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async () => { replyOptions: plan.replyOptions, replyResolver: plan.replyResolver, }), - } as typeof resolved; + // Prepared dispatch owns the outer durable-ingress lifecycle. If + // core suppresses dispatch, release that claim instead of orphaning it. + runDispatchLifecycle: { + turnAdoptionLifecycle: params.turnAdoptionLifecycle, + onDispatchSkipped: () => params.turnAdoptionLifecycle?.onAbandoned?.(), + }, + }; + return prepared; }, }, }); diff --git a/extensions/telegram/src/button-types.ts b/extensions/telegram/src/button-types.ts index 2e905fba00a..aeadb474994 100644 --- a/extensions/telegram/src/button-types.ts +++ b/extensions/telegram/src/button-types.ts @@ -121,13 +121,14 @@ function chunkInteractiveButtons( */ function buildTelegramInteractiveButtons( interactive?: LegacyInteractiveReply, + options?: { allowWebAppButtons?: boolean }, ): TelegramInlineButtons | undefined { const rows = reduceLegacyInteractiveReply( interactive, [] as TelegramInlineButton[][], (state, block) => { if (block.type === "buttons") { - chunkInteractiveButtons(block.buttons, state); + chunkInteractiveButtons(block.buttons, state, options); return state; } if (block.type === "select") { @@ -173,14 +174,17 @@ export function buildTelegramPresentationButtons( } /** Resolve Telegram inline buttons, preserving explicit and legacy button precedence. */ -export function resolveTelegramInlineButtons(params: { - buttons?: TelegramInlineButtons; - presentation?: unknown; - interactive?: unknown; -}): TelegramInlineButtons | undefined { +export function resolveTelegramInlineButtons( + params: { + buttons?: TelegramInlineButtons; + presentation?: unknown; + interactive?: unknown; + }, + options?: { allowWebAppButtons?: boolean }, +): TelegramInlineButtons | undefined { return ( params.buttons ?? - buildTelegramInteractiveButtons(normalizeLegacyInteractiveReply(params.interactive)) ?? - buildTelegramPresentationButtons(normalizeMessagePresentation(params.presentation)) + buildTelegramInteractiveButtons(normalizeLegacyInteractiveReply(params.interactive), options) ?? + buildTelegramPresentationButtons(normalizeMessagePresentation(params.presentation), options) ); } diff --git a/extensions/zoom-meetings/index.test.ts b/extensions/zoom-meetings/index.test.ts index 6ab39527a22..557d7b3d8cb 100644 --- a/extensions/zoom-meetings/index.test.ts +++ b/extensions/zoom-meetings/index.test.ts @@ -294,25 +294,6 @@ describe("Zoom meetings plugin surface", () => { }); }); - it("accepts timeoutMs on testListen and reports a bounded caption timeout", async () => { - const { invoke } = authorizationHarness(); - const response = await invoke("zoommeetings.testListen", { - mode: "transcribe", - timeoutMs: 1, - url: MEETING_URL, - }); - - expect(response).toMatchObject({ - ok: true, - payload: { - captioning: undefined, - createdSession: true, - listenTimedOut: true, - listenVerified: false, - }, - }); - }); - it.each([ ["zoommeetings.testSpeech", "transcribe", "test_speech requires mode: agent or bidi"], ["zoommeetings.testListen", "agent", "test_listen requires mode: transcribe"], diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index e08cb312132..8d466e53774 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -4624,12 +4624,15 @@ function sanitizeVitestCachePathSegment(value) { export function applyParallelVitestCachePaths(specs, params = {}) { const baseEnv = params.env ?? process.env; - if (baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) { - return specs; - } const cwd = params.cwd ?? process.cwd(); + const configuredCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() || undefined; + // CI publishes a persistent cache root, not a writer-safe leaf. Every + // concurrent Vitest process still needs its own live directory below it. + const cacheRoot = + configuredCacheRoot ?? path.join(cwd, "node_modules", ".experimental-vitest-cache"); return specs.map((spec, index) => { - if (spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) { + const specCachePath = spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim(); + if (specCachePath && specCachePath !== configuredCacheRoot) { return spec; } const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`); @@ -4637,12 +4640,7 @@ export function applyParallelVitestCachePaths(specs, params = {}) { ...spec, env: { ...spec.env, - [FS_MODULE_CACHE_PATH_ENV_KEY]: path.join( - cwd, - "node_modules", - ".experimental-vitest-cache", - cacheSegment, - ), + [FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(cacheRoot, cacheSegment), }, }; }); diff --git a/src/config/sessions/session-accessor.sqlite-active-events.test.ts b/src/config/sessions/session-accessor.sqlite-active-events.test.ts index 5407a6fbe69..cebf98cb5b0 100644 --- a/src/config/sessions/session-accessor.sqlite-active-events.test.ts +++ b/src/config/sessions/session-accessor.sqlite-active-events.test.ts @@ -627,5 +627,5 @@ describe("SQLite active transcript event projection", () => { await reconciliation; expect(order).toEqual(["event-loop-responsive", "live-write", "reconciled"]); expect(readSessionTranscriptMessageEventCount(scope)).toBe(100_001); - }, 30_000); + }, 60_000); }); diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index e9351973505..c0fabc18c44 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -578,22 +578,25 @@ describe("test-projects args", () => { ); }); - it("preserves explicit Vitest filesystem module cache paths", () => { - const specs = [ + it("splits an explicit Vitest filesystem module cache root", () => { + const [spec] = applyParallelVitestCachePaths( + [ + { + config: "test/vitest/vitest.gateway.config.ts", + env: {}, + }, + ], { - config: "test/vitest/vitest.gateway.config.ts", - env: {}, - }, - ]; - - expect( - applyParallelVitestCachePaths(specs, { cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache", }, - }), - ).toBe(specs); + }, + ); + + expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe( + "/tmp/cache/0-test-vitest-vitest.gateway.config.ts", + ); }); it("routes cli targets to the cli config", () => { diff --git a/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.test.ts b/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.test.ts index 7beeae3d941..d1d8d8eea4e 100644 --- a/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.test.ts +++ b/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.test.ts @@ -17,7 +17,7 @@ describe("heartbeat active-hours runtime evidence", () => { const evidence = await runHeartbeatActiveHoursRuntime({ artifactBase, repoRoot: process.cwd(), - timeoutMs: 2_000, + timeoutMs: 5_000, }); expect(evidence.entries[0]?.result.status).toBe("pass"); diff --git a/test/scripts/bench-sqlite-reliability.test.ts b/test/scripts/bench-sqlite-reliability.test.ts index d886c5ac928..bd7db5455db 100644 --- a/test/scripts/bench-sqlite-reliability.test.ts +++ b/test/scripts/bench-sqlite-reliability.test.ts @@ -23,7 +23,7 @@ function runProof(args: string[]) { { cwd: process.cwd(), encoding: "utf8", - timeout: 120_000, + timeout: 240_000, }, ); } diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 8e8381aa729..879257359ef 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -4922,13 +4922,42 @@ describe("scripts/test-projects parallel cache paths", () => { ]); }); - it("keeps an explicit global cache path", () => { - const [spec] = applyParallelVitestCachePaths( - [{ config: "test/vitest/vitest.gateway.config.ts", env: {}, pnpmArgs: [] }], + it("splits an explicit global cache root per parallel shard", () => { + const specs = applyParallelVitestCachePaths( + [ + { + config: "test/vitest/vitest.gateway.config.ts", + env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" }, + pnpmArgs: [], + }, + { + config: "test/vitest/vitest.extension-telegram.config.ts", + env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" }, + pnpmArgs: [], + }, + ], { cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" } }, ); - expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined(); + expect(specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([ + path.join("/tmp/cache", "0-test-vitest-vitest.gateway.config.ts"), + path.join("/tmp/cache", "1-test-vitest-vitest.extension-telegram.config.ts"), + ]); + }); + + it("keeps an already isolated cache path", () => { + const [spec] = applyParallelVitestCachePaths( + [ + { + config: "test/vitest/vitest.gateway.config.ts", + env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache/gateway" }, + pnpmArgs: [], + }, + ], + { cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" } }, + ); + + expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe("/tmp/cache/gateway"); }); }); diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index e6de3c96d3d..fa3e2fb3b61 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -216,8 +216,8 @@ describe("projects vitest config", () => { const config = createUiVitestConfig(); const testConfig = requireTestConfig(config); expect(testConfig.environment).toBe("jsdom"); - expect(testConfig.isolate).toBe(false); - expect(normalizeConfigPath(testConfig.runner)).toBe("test/non-isolated-runner.ts"); + expect(testConfig.isolate).toBe(true); + expect(testConfig.runner).toBeUndefined(); const setupFiles = normalizeConfigPaths(testConfig.setupFiles); expect(setupFiles).not.toContain("test/setup-openclaw-runtime.ts"); expect(setupFiles).toContain("ui/src/test-helpers/lit-warnings.setup.ts"); diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index 7fa3972a37d..0da9f903ae1 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -607,7 +607,8 @@ describe("scoped vitest configs", () => { expectForkedNonIsolatedRunner(defaultCommandsConfig); - expectThreadedNonIsolatedRunner(defaultUiConfig); + expect(requireTestConfig(defaultUiConfig).isolate).toBe(true); + expect(requireTestConfig(defaultUiConfig).runner).toBeUndefined(); expectThreadedIsolatedRunner(defaultExtensionMemoryConfig); expectThreadedIsolatedRunner(defaultExtensionProvidersConfig); expectForkedIsolatedRunner(defaultInfraConfig); diff --git a/test/vitest/vitest.ui.config.ts b/test/vitest/vitest.ui.config.ts index dd53d94ee5a..c5e7fa31bd2 100644 --- a/test/vitest/vitest.ui.config.ts +++ b/test/vitest/vitest.ui.config.ts @@ -15,9 +15,10 @@ export function createUiVitestConfig( exclude, excludeUnitFastTests: false, includeOpenClawRuntimeSetup: false, - isolate: false, + isolate: true, name: options?.name ?? "ui", setupFiles: ["ui/src/test-helpers/lit-warnings.setup.ts"], + useNonIsolatedRunner: false, }); }