diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d892d3f30df..eaee7ea9412 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -312,11 +312,8 @@ jobs: - name: Strict TS build smoke run: pnpm build:strict-smoke - - name: Enforce safe external URL opening policy - run: pnpm lint:ui:no-raw-window-open - - plugin-extension-boundary: - name: "plugin-extension-boundary" + check-additional: + name: "check-additional" needs: [docs-scope, changed-scope] if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 @@ -333,68 +330,71 @@ jobs: use-sticky-disk: "false" - name: Run plugin extension boundary guard + id: plugin_extension_boundary + continue-on-error: true run: pnpm run lint:plugins:no-extension-imports - web-search-provider-boundary: - name: "web-search-provider-boundary" - needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' - runs-on: blacksmith-16vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - use-sticky-disk: "false" - - name: Run web search provider boundary guard + id: web_search_provider_boundary + continue-on-error: true run: pnpm run lint:web-search-provider-boundaries - extension-src-outside-plugin-sdk-boundary: - name: "extension-src-outside-plugin-sdk-boundary" - needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' - runs-on: blacksmith-16vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - use-sticky-disk: "false" - - name: Run extension src boundary guard + id: extension_src_outside_plugin_sdk_boundary + continue-on-error: true run: pnpm run lint:extensions:no-src-outside-plugin-sdk - extension-plugin-sdk-internal-boundary: - name: "extension-plugin-sdk-internal-boundary" - needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' - runs-on: blacksmith-16vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - use-sticky-disk: "false" - - name: Run extension plugin-sdk-internal guard + id: extension_plugin_sdk_internal_boundary + continue-on-error: true run: pnpm run lint:extensions:no-plugin-sdk-internal + - name: Enforce safe external URL opening policy + id: no_raw_window_open + continue-on-error: true + run: pnpm lint:ui:no-raw-window-open + + - name: Run gateway watch regression harness + id: gateway_watch_regression + continue-on-error: true + run: pnpm test:gateway:watch-regression + + - name: Upload gateway watch regression artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: gateway-watch-regression + path: .local/gateway-watch-regression/ + retention-days: 7 + + - name: Fail if any additional check failed + if: always() + env: + PLUGIN_EXTENSION_BOUNDARY_OUTCOME: ${{ steps.plugin_extension_boundary.outcome }} + WEB_SEARCH_PROVIDER_BOUNDARY_OUTCOME: ${{ steps.web_search_provider_boundary.outcome }} + EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME: ${{ steps.extension_src_outside_plugin_sdk_boundary.outcome }} + EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME: ${{ steps.extension_plugin_sdk_internal_boundary.outcome }} + NO_RAW_WINDOW_OPEN_OUTCOME: ${{ steps.no_raw_window_open.outcome }} + GATEWAY_WATCH_REGRESSION_OUTCOME: ${{ steps.gateway_watch_regression.outcome }} + run: | + failures=0 + for result in \ + "plugin-extension-boundary|$PLUGIN_EXTENSION_BOUNDARY_OUTCOME" \ + "web-search-provider-boundary|$WEB_SEARCH_PROVIDER_BOUNDARY_OUTCOME" \ + "extension-src-outside-plugin-sdk-boundary|$EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME" \ + "extension-plugin-sdk-internal-boundary|$EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME" \ + "lint:ui:no-raw-window-open|$NO_RAW_WINDOW_OPEN_OUTCOME" \ + "gateway-watch-regression|$GATEWAY_WATCH_REGRESSION_OUTCOME"; do + name="${result%%|*}" + outcome="${result#*|}" + if [ "$outcome" != "success" ]; then + echo "::error title=${name} failed::${name} outcome: ${outcome}" + failures=1 + fi + done + + exit "$failures" + build-smoke: name: "build-smoke" needs: [docs-scope, changed-scope] @@ -427,34 +427,6 @@ jobs: - name: Check CLI startup memory run: pnpm test:startup:memory - gateway-watch-regression: - name: "gateway-watch-regression" - needs: [docs-scope, changed-scope] - if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true' - runs-on: blacksmith-16vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - use-sticky-disk: "false" - - - name: Run gateway watch regression harness - run: pnpm test:gateway:watch-regression - - - name: Upload gateway watch regression artifacts - if: always() - uses: actions/upload-artifact@v7 - with: - name: gateway-watch-regression - path: .local/gateway-watch-regression/ - retention-days: 7 - # Validate docs (format, lint, broken links) only when docs files changed. check-docs: needs: [docs-scope] diff --git a/CHANGELOG.md b/CHANGELOG.md index 553fab9d3a8..697bdd2e29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai - Plugins/Matrix: add `allowBots` room policy so configured Matrix bot accounts can talk to each other, with optional mention-only gating. Thanks @gumadeiras. - Plugins/Matrix: add per-account `allowPrivateNetwork` opt-in for private/internal homeservers, while keeping public cleartext homeservers blocked. Thanks @gumadeiras. - Web tools/Tavily: add Tavily as a bundled web-search provider with dedicated `tavily_search` and `tavily_extract` tools, using canonical plugin-owned config under `plugins.entries.tavily.config.webSearch.*`. (#49200) thanks @lakshyaag-tavily. +- Docs/plugins: add the community QQbot plugin listing to the docs catalog. (#29898) Thanks @sliverp. ### Fixes diff --git a/docs/plugins/community.md b/docs/plugins/community.md index 94c6ddbe00d..ebd660ccdbd 100644 --- a/docs/plugins/community.md +++ b/docs/plugins/community.md @@ -45,6 +45,11 @@ Use this format when adding entries: ## Listed plugins +- **QQbot** — Connect OpenClaw to QQ via the QQ Bot API. Supports private chats, group mentions, channel messages, and rich media including voice, images, videos, and files. + npm: `@sliverp/qqbot` + repo: `https://github.com/sliverp/qqbot` + install: `openclaw plugins install @sliverp/qqbot` + - **WeChat** — Connect OpenClaw to WeChat personal accounts via WeChatPadPro (iPad protocol). Supports text, image, and file exchange with keyword-triggered conversations. npm: `@icesword760/openclaw-wechat` repo: `https://github.com/icesword0760/openclaw-wechat` diff --git a/extensions/signal/src/monitor.tool-result.test-harness.ts b/extensions/signal/src/monitor.tool-result.test-harness.ts index ad81a4d6da2..7f1c8b7d7cf 100644 --- a/extensions/signal/src/monitor.tool-result.test-harness.ts +++ b/extensions/signal/src/monitor.tool-result.test-harness.ts @@ -66,8 +66,12 @@ export function createMockSignalDaemonHandle( }; } -vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { - const actual = await importOriginal(); +// Use importActual so shared-worker mocks from earlier test files do not leak +// into this harness's partial overrides. +vi.mock("openclaw/plugin-sdk/config-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/config-runtime", + ); return { ...actual, loadConfig: () => config, @@ -78,8 +82,10 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/reply-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/reply-runtime", + ); return { ...actual, getReplyFromConfig: (...args: unknown[]) => replyMock(...args), @@ -104,8 +110,8 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { }; }); -vi.mock("./send.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./send.js", async () => { + const actual = await vi.importActual("./send.js"); return { ...actual, sendMessageSignal: (...args: unknown[]) => sendMock(...args), @@ -114,8 +120,10 @@ vi.mock("./send.js", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/conversation-runtime", + ); return { ...actual, readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), @@ -123,8 +131,10 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/security-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/security-runtime", + ); return { ...actual, readStoreAllowFromForDmPolicy: (...args: unknown[]) => readAllowFromStoreMock(...args), @@ -137,16 +147,18 @@ vi.mock("./client.js", () => ({ signalRpcRequest: (...args: unknown[]) => signalRpcRequestMock(...args), })); -vi.mock("./daemon.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./daemon.js", async () => { + const actual = await vi.importActual("./daemon.js"); return { ...actual, spawnSignalDaemon: (...args: unknown[]) => spawnSignalDaemonMock(...args), }; }); -vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/infra-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/infra-runtime", + ); return { ...actual, waitForTransportReady: (...args: unknown[]) => waitForTransportReadyMock(...args), diff --git a/src/plugins/loader.git-path-regression.test.ts b/src/plugins/loader.git-path-regression.test.ts index fde7d6554bc..23ab4f4243d 100644 --- a/src/plugins/loader.git-path-regression.test.ts +++ b/src/plugins/loader.git-path-regression.test.ts @@ -77,10 +77,9 @@ export const copiedRuntimeMarker = { ...__testing.buildPluginLoaderJitiOptions({}), tryNative: false, }); - // Jiti's pre-alias failure text varies across Node versions and platforms. - // The contract is simply that the source import rejects until the scoped - // plugin-sdk alias is applied. - await expect(withoutAlias.import(copiedChannelRuntime)).rejects.toThrow(); + // The production loader uses sync Jiti evaluation, so this regression test + // should exercise the same seam instead of Jiti's async import helper. + expect(() => withoutAlias(copiedChannelRuntime)).toThrow(); const withAlias = createJiti(jitiBaseUrl, { ...__testing.buildPluginLoaderJitiOptions({ @@ -88,7 +87,7 @@ export const copiedRuntimeMarker = { }), tryNative: false, }); - await expect(withAlias.import(copiedChannelRuntime)).resolves.toMatchObject({ + expect(withAlias(copiedChannelRuntime)).toMatchObject({ copiedRuntimeMarker: { PAIRING_APPROVED_MESSAGE: "paired", resolveOutboundSendDep: expect.any(Function), diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 4f6132a3bd5..a4bf12fad15 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -3595,10 +3595,9 @@ export const syntheticRuntimeMarker = { ...__testing.buildPluginLoaderJitiOptions({}), tryNative: false, }); - // Jiti's pre-alias failure text varies across Node versions and platforms. - // This boundary only needs to prove the source import rejects until the - // plugin-sdk alias is present. - await expect(withoutAlias.import(copiedChannelRuntime)).rejects.toThrow(); + // The production loader uses sync Jiti evaluation, so this boundary should + // follow the same path instead of the async import helper. + expect(() => withoutAlias(copiedChannelRuntime)).toThrow(); const withAlias = createJiti(jitiBaseUrl, { ...__testing.buildPluginLoaderJitiOptions({ @@ -3606,7 +3605,7 @@ export const syntheticRuntimeMarker = { }), tryNative: false, }); - await expect(withAlias.import(copiedChannelRuntime)).resolves.toMatchObject({ + expect(withAlias(copiedChannelRuntime)).toMatchObject({ syntheticRuntimeMarker: { resolveOutboundSendDep: expect.any(Function), }, diff --git a/test/fixtures/test-parallel.behavior.json b/test/fixtures/test-parallel.behavior.json index 2de992a45d5..954b5f87557 100644 --- a/test/fixtures/test-parallel.behavior.json +++ b/test/fixtures/test-parallel.behavior.json @@ -183,6 +183,26 @@ "file": "src/infra/heartbeat-runner.returns-default-unset.test.ts", "reason": "Heartbeat default-unset coverage retained a large shared unit-fast heap spike on Linux Node 22 CI." }, + { + "file": "src/infra/heartbeat-runner.ghost-reminder.test.ts", + "reason": "Mocks jiti at file scope, so it is safer outside shared Vitest workers." + }, + { + "file": "src/infra/heartbeat-runner.transcript-prune.test.ts", + "reason": "Mocks jiti at file scope, so it is safer outside shared Vitest workers." + }, + { + "file": "src/infra/heartbeat-runner.sender-prefers-delivery-target.test.ts", + "reason": "Mocks jiti at file scope, so it is safer outside shared Vitest workers." + }, + { + "file": "src/infra/heartbeat-runner.model-override.test.ts", + "reason": "Mocks jiti at file scope, so it is safer outside shared Vitest workers." + }, + { + "file": "src/plugins/loader.git-path-regression.test.ts", + "reason": "Constructs a real Jiti boundary and is safer outside shared workers that may have mocked jiti earlier." + }, { "file": "src/infra/outbound/outbound-session.test.ts", "reason": "Outbound session coverage retained a large shared unit-fast heap spike on Linux Node 22 CI."