From 98e3f729bcb8f8ff76a38f03b327fb1c5c5eebbd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 01:29:33 -0700 Subject: [PATCH] refactor: remove dead plugin loader exports (#105937) * refactor(plugins): trim activation and contract exports * test(plugins): restore fixture cleanup * refactor(plugins): trim install and loader exports * test(plugins): fully reset loader caches * refactor(plugins): trim metadata and catalog exports * test(plugins): preserve catalog trust coverage * refactor(plugins): trim provider and plugin exports * refactor(plugins): trim runtime and tool exports * test(plugins): update dead-export consumers * test(plugins): remove empty dead-export suites * refactor(plugins): align exports with split registry * refactor(plugins): trim drifted loader exports * style(plugins): format test fixtures * refactor(scripts): use supported plugin APIs * refactor(plugins): finish dead export cleanup * chore(deadcode): refresh export baseline * test(cli): mock production memory state * chore(deadcode): sync latest export baseline * fix(tests): keep plugin fixtures inside core * chore(deadcode): refresh rebased export baseline * chore(deadcode): sync current ratchets * fix(plugins): retain reserved slot invariant * fix(plugins): preserve dead-export invariants * test(plugins): use neutral catalog query fixture * test(plugins): satisfy catalog lint * test(plugins): preserve integrity drift coverage * fix(ci): register skill experience live proof --- scripts/deadcode-exports.baseline.mjs | 536 ----- scripts/lib/bundled-extension-manifest.ts | 19 +- scripts/load-channel-config-surface.ts | 17 +- scripts/plugin-boundary-report.ts | 8 +- ...s.before-tool-call.integration.e2e.test.ts | 2 +- ...tools.create-openclaw-coding-tools.test.ts | 2 +- src/agents/cli-runner.bundle-mcp.e2e.test.ts | 281 --- src/agents/cli-runner/prepare.test.ts | 5 +- .../attempt.model-diagnostic-events.test.ts | 2 +- ...mpt.spawn-workspace.context-engine.test.ts | 2 +- .../system-prompt.test.ts | 5 +- ...agent-subscribe.handlers.lifecycle.test.ts | 2 +- src/agents/harness/native-hook-relay.test.ts | 2 +- .../prompt-compaction-hook-helpers.test.ts | 2 +- src/agents/model-fallback.test.ts | 114 - src/agents/model-selection-cli.test.ts | 75 +- .../openclaw-tools.media-factory-plan.test.ts | 8 +- .../runtime-plugins.registry-reuse.test.ts | 92 - src/agents/system-prompt.memory.test.ts | 5 +- .../temp-plugin-extension-fixtures.ts | 2 +- ...irective.directive-behavior.e2e-harness.ts | 4 +- ...nner-memory.preflight-stale-tokens.test.ts | 2 +- .../reply/agent-runner-memory.test.ts | 2 +- .../agent-runner.misc.runreplyagent.test.ts | 2 +- .../reply/commands-diagnostics.test.ts | 2 +- ...ispatch-from-config.reply-dispatch.test.ts | 2 +- ...ispatch-from-config.shared.test-harness.ts | 7 +- .../reply/dispatch-from-config.test.ts | 2 +- .../test-helpers/runtime-artifacts.ts | 30 +- src/cli/plugins-cli-test-helpers.ts | 35 +- src/cli/plugins-cli.list.test.ts | 2 +- src/cli/plugins-list-format.test.ts | 2 +- src/cli/plugins-location-bridges.test.ts | 2 +- src/commands/agent.test.ts | 2 +- src/commands/auth-choice-options.test.ts | 5 +- .../auth-choice.apply.plugin-provider.test.ts | 128 - src/commands/auth-choice.test.ts | 92 +- src/commands/config-validation.test.ts | 2 +- src/commands/doctor-workspace-status.test.ts | 2 +- src/commands/health.snapshot.test.ts | 2 +- src/commands/status.test.ts | 2 +- src/config/config.pruning-defaults.test.ts | 26 +- src/config/io.best-effort.test.ts | 15 +- .../plugin-auto-enable.prefer-over.test.ts | 122 - src/config/plugin-auto-enable.test-helpers.ts | 4 +- src/config/test-helpers.ts | 4 +- src/context-engine/context-engine.test.ts | 5 +- .../isolated-agent.model-overrides.test.ts | 2 +- src/gateway/gateway-acp-bind.live.test.ts | 2 +- .../gateway-acp-spawn-defaults.live.test.ts | 2 +- src/gateway/gateway-codex-bind.live.test.ts | 2 +- .../gateway-models.profiles.live.test.ts | 6 +- src/gateway/gateway.test.ts | 2 +- .../server-network-runtime.e2e.test.ts | 2 +- ...otstrap.browser-plugin.integration.test.ts | 2 +- ...server-plugins.subagent-ended-hook.test.ts | 2 +- src/gateway/server-plugins.test.ts | 7 +- .../server.sessions.list-changed.test.ts | 2 +- src/gateway/test-helpers.server.ts | 2 +- src/infra/outbound/deliver.test.ts | 2 +- src/mcp/plugin-tools-serve.test.ts | 2 +- .../runner.vision-skip.test.ts | 4 +- src/pairing/pairing-challenge.test.ts | 2 +- src/plugin-sdk/channel-pairing.test.ts | 2 +- src/plugin-sdk/facade-runtime.test.ts | 3 - src/plugin-sdk/memory-host-core.test.ts | 2 +- src/plugins/activation-context.ts | 8 +- src/plugins/activation-planner.ts | 10 +- src/plugins/active-runtime-registry.test.ts | 36 +- .../agent-tool-result-middleware-loader.ts | 5 - src/plugins/agent-tool-result-middleware.ts | 5 - src/plugins/api-builder.ts | 2 +- src/plugins/api-lifecycle.ts | 4 +- src/plugins/bundle-commands.ts | 2 +- src/plugins/bundle-config-shared.ts | 2 +- src/plugins/bundle-lsp.ts | 4 +- src/plugins/bundle-manifest.test.ts | 6 +- src/plugins/bundle-manifest.ts | 4 +- src/plugins/bundle-mcp.ts | 4 +- .../bundled-capability-metadata.test.ts | 94 - .../bundled-capability-runtime.test.ts | 22 - src/plugins/bundled-capability-runtime.ts | 2 +- src/plugins/bundled-channel-runtime.test.ts | 10 - src/plugins/bundled-channel-runtime.ts | 16 - src/plugins/bundled-dir.ts | 16 +- src/plugins/bundled-load-path-aliases.ts | 8 +- src/plugins/bundled-plugin-metadata.test.ts | 3 +- src/plugins/bundled-plugin-metadata.ts | 2 +- .../bundled-plugin-startup-metadata.ts | 2 +- src/plugins/bundled-sources.ts | 4 +- src/plugins/clawhub.test.ts | 8 +- src/plugins/clawhub.ts | 4 +- src/plugins/cli-backend.types.ts | 2 +- src/plugins/cli-backends.runtime.ts | 2 +- src/plugins/cli-gateway-nodes-runtime.test.ts | 17 +- src/plugins/cli-gateway-nodes-runtime.ts | 2 +- .../cli.browser-plugin.integration.test.ts | 2 +- src/plugins/command-registration.ts | 4 +- src/plugins/command-specs.ts | 2 +- src/plugins/commands.test.ts | 2 +- src/plugins/compaction-provider.ts | 4 +- src/plugins/compat/registry.test.ts | 261 +- src/plugins/compat/registry.ts | 14 +- src/plugins/compat/types.ts | 4 +- src/plugins/config-activation-shared.ts | 6 +- src/plugins/config-contract-matches.ts | 2 +- src/plugins/config-contracts.ts | 7 +- src/plugins/config-policy.test.ts | 22 +- src/plugins/config-policy.ts | 10 +- .../contracts/host-hooks.contract.test.ts | 20 +- .../inventory/bundled-capability-metadata.ts | 41 +- src/plugins/contracts/loader.contract.test.ts | 80 - ...memory-embedding-provider.contract.test.ts | 2 +- .../contracts/registry.contract.test.ts | 13 +- src/plugins/contracts/registry.retry.test.ts | 38 - src/plugins/contracts/registry.ts | 46 - .../run-context-lifecycle.contract.test.ts | 6 +- .../scheduled-turns.contract.test.ts | 25 +- .../session-actions.contract.test.ts | 2 +- .../session-attachments.contract.test.ts | 130 +- .../session-entry-projection.contract.test.ts | 2 +- .../conversation-binding.test-fixtures.ts | 26 + src/plugins/conversation-binding.test.ts | 11 +- src/plugins/conversation-binding.ts | 12 - .../current-plugin-metadata-snapshot.ts | 2 +- src/plugins/default-enablement.ts | 2 +- src/plugins/dependency-denylist.test.ts | 13 - src/plugins/dependency-denylist.ts | 6 +- .../doctor-contract-registry-loader-state.ts | 10 + ...octor-contract-registry.load-paths.test.ts | 2 +- .../doctor-contract-registry.test-fixtures.ts | 14 + src/plugins/doctor-contract-registry.test.ts | 10 +- src/plugins/doctor-contract-registry.ts | 31 +- src/plugins/embedding-provider-runtime.ts | 13 +- src/plugins/embedding-providers.test.ts | 33 - src/plugins/embedding-providers.ts | 23 +- src/plugins/externalized-bundled-plugins.ts | 2 +- src/plugins/gateway-startup-plugin-ids.ts | 4 +- src/plugins/git-install.ts | 6 +- src/plugins/hardlink-policy.test.ts | 17 +- src/plugins/hardlink-policy.ts | 5 +- src/plugins/hook-agent-context.test.ts | 53 - src/plugins/hook-agent-context.ts | 2 +- src/plugins/hook-decision-types.test.ts | 55 +- src/plugins/hook-decision-types.ts | 25 +- src/plugins/hook-registry.types.ts | 2 +- src/plugins/hook-resolve-exec-env.test.ts | 2 +- src/plugins/hook-runner-global-state.ts | 2 +- .../hook-runner-global.compose.test.ts | 4 +- src/plugins/hook-runner-global.test.ts | 4 +- .../hooks.before-agent-finalize.test.ts | 2 +- src/plugins/hooks.before-agent-reply.test.ts | 2 +- src/plugins/hooks.before-agent-start.test.ts | 2 +- src/plugins/hooks.before-install.test.ts | 2 +- src/plugins/hooks.before-tool-call.test.ts | 2 +- src/plugins/hooks.compaction-timeout.test.ts | 2 +- src/plugins/hooks.correlation.test.ts | 2 +- .../hooks.model-override-wiring.test.ts | 2 +- src/plugins/hooks.phase-hooks.test.ts | 2 +- src/plugins/hooks.security.test.ts | 2 +- src/plugins/hooks.sync-only.test.ts | 85 - src/plugins/hooks.test-fixtures.ts | 63 + src/plugins/hooks.test-helpers.ts | 75 +- src/plugins/hooks.ts | 84 +- src/plugins/host-hook-attachments.ts | 6 +- src/plugins/host-hook-cleanup.ts | 11 +- src/plugins/host-hook-json.ts | 4 +- .../host-hook-runtime.test-fixtures.ts | 42 + src/plugins/host-hook-runtime.ts | 25 +- src/plugins/host-hook-scheduled-turns.ts | 2 +- src/plugins/host-hook-state.ts | 3 +- src/plugins/host-hooks.ts | 11 +- src/plugins/host-tool-param-parsers.ts | 4 +- src/plugins/http-registry.test.ts | 2 +- src/plugins/install-channel-specs.ts | 2 +- src/plugins/install-policy-context.ts | 2 +- src/plugins/install-security-scan.ts | 4 +- src/plugins/install-source-info.ts | 12 +- src/plugins/install.test.ts | 53 +- src/plugins/install.ts | 6 +- .../installed-plugin-index-record-cache.ts | 2 +- src/plugins/installed-plugin-index-store.ts | 2 +- src/plugins/installed-plugin-index.test.ts | 97 - src/plugins/installed-plugin-index.ts | 23 +- .../interactive-registry.test-fixtures.ts | 6 + src/plugins/interactive-registry.ts | 2 +- src/plugins/legacy-npm-declaration.ts | 2 +- src/plugins/loader-cache-instances.ts | 15 + src/plugins/loader-cache-state.test.ts | 13 +- src/plugins/loader-cache-state.ts | 2 +- src/plugins/loader-channel-setup.ts | 2 +- src/plugins/loader-provenance.ts | 2 +- src/plugins/loader.prefer-over.test.ts | 2 +- src/plugins/loader.runtime-registry.test.ts | 781 +----- src/plugins/loader.test-fixtures.ts | 12 +- src/plugins/loader.test.ts | 338 +-- src/plugins/loader.ts | 66 +- src/plugins/management-service.test.ts | 173 +- src/plugins/management-service.ts | 12 +- src/plugins/manifest-command-aliases.ts | 2 +- src/plugins/manifest-contract-runtime.ts | 2 +- src/plugins/manifest-contribution-ids.ts | 16 +- .../manifest-model-id-normalization.test.ts | 192 +- src/plugins/manifest-registry.test.ts | 25 +- src/plugins/manifest-registry.ts | 5 - src/plugins/manifest-tool-availability.ts | 4 +- src/plugins/manifest.json5-tolerance.test.ts | 27 +- src/plugins/manifest.reserved-id.test.ts | 3 +- src/plugins/manifest.ts | 51 +- src/plugins/marketplace.ts | 10 +- .../memory-embedding-providers.test.ts | 12 - src/plugins/memory-embedding-providers.ts | 9 - src/plugins/memory-state.test-fixtures.ts | 23 + src/plugins/memory-state.test.ts | 94 +- src/plugins/memory-state.ts | 30 +- src/plugins/min-host-version.test.ts | 25 +- src/plugins/min-host-version.ts | 6 +- ...l-external-plugin-catalog-envelope.test.ts | 453 ++-- ...ficial-external-plugin-catalog-envelope.ts | 25 +- ...-external-plugin-catalog-snapshot-store.ts | 2 +- .../official-external-plugin-catalog.test.ts | 2107 +++-------------- .../official-external-plugin-catalog.ts | 55 +- ...enai-compatible-embedding-provider.test.ts | 23 +- .../openai-compatible-embedding-provider.ts | 6 +- src/plugins/package-compat.ts | 4 +- src/plugins/plugin-cache-primitives.ts | 2 +- .../plugin-control-plane-context.test.ts | 50 +- src/plugins/plugin-control-plane-context.ts | 16 +- .../plugin-graceful-init-failure.test.ts | 2 +- src/plugins/plugin-load-profile.ts | 10 +- src/plugins/plugin-lookup-table.test.ts | 143 -- src/plugins/plugin-lookup-table.ts | 11 +- .../plugin-module-loader-cache.test.ts | 57 - src/plugins/plugin-module-loader-cache.ts | 10 +- .../plugin-registration-transaction.test.ts | 5 +- .../plugin-registration-transaction.ts | 2 +- src/plugins/plugin-registry-contributions.ts | 32 +- src/plugins/plugin-registry-snapshot.ts | 18 +- src/plugins/plugin-registry.test.ts | 7 +- src/plugins/plugin-sdk-dist-alias.ts | 189 -- .../plugin-sdk-native-resolver.test.ts | 14 +- src/plugins/plugin-sdk-native-resolver.ts | 16 +- src/plugins/plugin-version-drift.ts | 2 +- src/plugins/provider-auth-choice.test.ts | 133 +- src/plugins/provider-auth-choice.ts | 148 +- src/plugins/provider-auth-choices.test.ts | 9 +- src/plugins/provider-auth-choices.ts | 4 +- src/plugins/provider-auth-input.test.ts | 57 - src/plugins/provider-auth-input.ts | 14 +- src/plugins/provider-auth-mode.ts | 2 +- src/plugins/provider-catalog-result.ts | 2 +- src/plugins/provider-discovery.test.ts | 95 +- src/plugins/provider-discovery.ts | 27 +- src/plugins/provider-hook-runtime.ts | 4 +- src/plugins/provider-model-compat.ts | 7 +- src/plugins/provider-oauth-flow.ts | 2 +- .../provider-openai-chatgpt-oauth-tls.ts | 2 +- src/plugins/provider-runtime.runtime.ts | 10 - src/plugins/provider-runtime.test-support.ts | 2 +- src/plugins/provider-runtime.test.ts | 35 - src/plugins/provider-thinking.types.ts | 6 +- src/plugins/provider-wizard.ts | 2 +- src/plugins/providers.ts | 17 +- src/plugins/public-surface-loader.ts | 6 - src/plugins/public-surface-runtime.test.ts | 9 - src/plugins/public-surface-runtime.ts | 2 +- .../register-plugin-cli-command-groups.ts | 4 +- src/plugins/registry-types.ts | 82 +- src/plugins/registry.channel-guard.test.ts | 2 +- src/plugins/registry.control-ui.test.ts | 2 +- .../registry.dual-kind-memory-gate.test.ts | 4 +- src/plugins/registry.test-fixtures.ts | 8 + src/plugins/registry.ts | 38 +- src/plugins/roots.ts | 2 +- src/plugins/runtime-channel-state.ts | 2 +- src/plugins/runtime-state.ts | 8 +- src/plugins/runtime.test.ts | 2 +- .../runtime/gateway-bindings.test-fixtures.ts | 7 + src/plugins/runtime/gateway-bindings.ts | 9 - .../gateway-request-scope.test-fixtures.ts | 8 + .../runtime/gateway-request-scope.test.ts | 2 +- src/plugins/runtime/gateway-request-scope.ts | 4 +- src/plugins/runtime/index.test.ts | 9 +- src/plugins/runtime/index.ts | 8 +- src/plugins/runtime/load-context.ts | 4 +- src/plugins/runtime/native-deps.ts | 2 +- .../runtime/runtime-plugin-boundary.ts | 22 - src/plugins/runtime/runtime-taskflow.types.ts | 8 +- src/plugins/runtime/runtime-tasks.types.ts | 9 +- ...standalone-runtime-registry-loader.test.ts | 79 +- src/plugins/runtime/types-channel.ts | 8 +- src/plugins/runtime/types-core.ts | 11 +- src/plugins/runtime/types.ts | 22 +- src/plugins/sdk-alias.test.ts | 289 +-- src/plugins/sdk-alias.ts | 44 +- src/plugins/session-entry-slot-keys.ts | 14 +- src/plugins/setup-registry-loader-state.ts | 10 + src/plugins/setup-registry.runtime.test.ts | 27 +- src/plugins/setup-registry.runtime.ts | 8 - src/plugins/setup-registry.test-fixtures.ts | 15 + src/plugins/setup-registry.test.ts | 15 +- src/plugins/setup-registry.ts | 32 +- src/plugins/slots.test.ts | 39 +- src/plugins/slots.ts | 6 +- src/plugins/source-checkout-runtime.test.ts | 11 +- src/plugins/source-display.ts | 1 - src/plugins/status-dependencies-core.ts | 2 +- src/plugins/status.test-fixtures.ts | 117 + src/plugins/status.test-helpers.ts | 116 - src/plugins/status.test.ts | 4 +- src/plugins/tool-descriptor-cache.test.ts | 4 +- src/plugins/tool-descriptor-cache.ts | 34 +- src/plugins/tools.optional.test.ts | 8 +- src/plugins/tools.test-fixtures.ts | 8 + src/plugins/tools.ts | 17 +- src/plugins/trusted-tool-policy.ts | 2 +- src/plugins/uninstall.test.ts | 234 +- src/plugins/uninstall.ts | 42 +- src/plugins/update.test.ts | 14 +- src/plugins/update.ts | 10 +- .../web-fetch-providers.runtime.test.ts | 70 - src/plugins/web-provider-resolution-shared.ts | 6 +- src/plugins/web-provider-runtime-shared.ts | 2 +- src/plugins/web-provider-types.ts | 4 +- .../web-search-providers.runtime.test.ts | 162 +- src/plugins/wired-hooks-gateway.test.ts | 2 +- src/plugins/wired-hooks-inbound-claim.test.ts | 2 +- src/plugins/wired-hooks-llm.test.ts | 2 +- src/plugins/wired-hooks-message.test.ts | 2 +- .../wired-hooks-reply-dispatch.test.ts | 2 +- .../wired-hooks-reply-payload-sending.test.ts | 2 +- src/plugins/wired-hooks-session.test.ts | 2 +- src/plugins/wired-hooks-subagent.test.ts | 2 +- src/plugins/worker-provider-registry.test.ts | 39 +- src/plugins/worker-provider-registry.ts | 12 +- .../runtime.integration.test-helpers.ts | 2 +- src/skills/lifecycle/archive-install.test.ts | 2 +- src/skills/lifecycle/install.test.ts | 2 +- .../bundled-plugin-public-surface.ts | 10 +- ...ndled-provider-auth-literal-parity.test.ts | 4 +- ...ndled-provider-auth-literal-parity.test.ts | 4 +- test/scripts/test-live-shard.test.ts | 3 + 342 files changed, 1758 insertions(+), 9707 deletions(-) delete mode 100644 src/agents/cli-runner.bundle-mcp.e2e.test.ts delete mode 100644 src/agents/runtime-plugins.registry-reuse.test.ts delete mode 100644 src/config/plugin-auto-enable.prefer-over.test.ts delete mode 100644 src/plugins/bundled-capability-runtime.test.ts delete mode 100644 src/plugins/contracts/loader.contract.test.ts create mode 100644 src/plugins/conversation-binding.test-fixtures.ts create mode 100644 src/plugins/doctor-contract-registry-loader-state.ts create mode 100644 src/plugins/doctor-contract-registry.test-fixtures.ts delete mode 100644 src/plugins/hooks.sync-only.test.ts create mode 100644 src/plugins/hooks.test-fixtures.ts create mode 100644 src/plugins/host-hook-runtime.test-fixtures.ts create mode 100644 src/plugins/interactive-registry.test-fixtures.ts create mode 100644 src/plugins/loader-cache-instances.ts create mode 100644 src/plugins/memory-state.test-fixtures.ts delete mode 100644 src/plugins/plugin-sdk-dist-alias.ts create mode 100644 src/plugins/registry.test-fixtures.ts create mode 100644 src/plugins/runtime/gateway-bindings.test-fixtures.ts create mode 100644 src/plugins/runtime/gateway-request-scope.test-fixtures.ts create mode 100644 src/plugins/setup-registry-loader-state.ts create mode 100644 src/plugins/setup-registry.test-fixtures.ts create mode 100644 src/plugins/status.test-fixtures.ts create mode 100644 src/plugins/tools.test-fixtures.ts diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index 0fb0b576b9ef..9ce0ead98190 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -3046,549 +3046,13 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/plugin-state/plugin-state-store.sqlite.ts: seedPluginStateDatabaseEntriesForTests", "src/plugin-state/plugin-state-store.sqlite.ts: setMaxPluginStateEntriesPerPluginForTests", "src/plugin-state/plugin-state-store.ts: clearPluginStateStoreForTests", - "src/plugins/activation-context.ts: BundledPluginCompatibleActivationInputs", - "src/plugins/activation-context.ts: BundledPluginCompatibleLoadValues", - "src/plugins/activation-context.ts: PluginActivationCompatConfig", - "src/plugins/activation-context.ts: PluginActivationInputs", - "src/plugins/activation-planner.ts: PluginActivationPlanEntry", - "src/plugins/activation-planner.ts: PluginActivationPlannerHintReason", - "src/plugins/activation-planner.ts: PluginActivationPlannerManifestReason", - "src/plugins/activation-planner.ts: PluginActivationPlannerReason", - "src/plugins/activation-planner.ts: PluginActivationPlannerTrigger", - "src/plugins/agent-tool-result-middleware-loader.ts: __testing", - "src/plugins/agent-tool-result-middleware-loader.ts: testing", - "src/plugins/agent-tool-result-middleware.ts: normalizeAgentToolResultMiddlewareHarnesses", - "src/plugins/api-builder.ts: BuildPluginApiParams", - "src/plugins/api-lifecycle.ts: PluginApiLifecyclePolicy", - "src/plugins/api-lifecycle.ts: PluginApiMethodName", - "src/plugins/bundle-commands.ts: ClaudeBundleCommandSpec", - "src/plugins/bundle-config-shared.ts: BundleServerRuntimeSupport", - "src/plugins/bundle-lsp.ts: BundleLspConfig", - "src/plugins/bundle-lsp.ts: BundleLspRuntimeSupport", - "src/plugins/bundle-manifest.ts: BundleManifestLoadResult", - "src/plugins/bundle-manifest.ts: BundlePluginManifest", - "src/plugins/bundle-mcp.ts: BundleMcpRuntimeSupport", - "src/plugins/bundle-mcp.ts: EnabledBundleMcpConfigResult", - "src/plugins/bundled-capability-runtime.ts: buildVitestCapabilityShimAliasMap", - "src/plugins/bundled-channel-runtime.ts: resolveBundledChannelWorkspacePath", - "src/plugins/bundled-dir.ts: setBundledPluginsDirOverrideForTest", - "src/plugins/bundled-dir.ts: SourceCheckoutDependencyDiagnostic", - "src/plugins/bundled-load-path-aliases.ts: BundledPluginLoadPathAlias", - "src/plugins/bundled-load-path-aliases.ts: BundledPluginLoadPathAliasKind", - "src/plugins/bundled-load-path-aliases.ts: LegacyBundledPluginPath", - "src/plugins/bundled-load-path-aliases.ts: PackagedBundledPluginPath", - "src/plugins/bundled-plugin-metadata.ts: BundledPluginMetadata", - "src/plugins/bundled-plugin-startup-metadata.ts: BundledPluginStartupMetadata", - "src/plugins/bundled-sources.ts: BundledPluginLookup", - "src/plugins/clawhub.ts: ClawHubInstallErrorCode", - "src/plugins/clawhub.ts: formatClawHubSpecifier", - "src/plugins/cli-backend.types.ts: CliBackendToolAvailability", - "src/plugins/cli-backends.runtime.ts: PluginCliBackendEntry", - "src/plugins/cli-gateway-nodes-runtime.ts: resolvePluginCliNodeInvokeGatewayTimeoutMs", - "src/plugins/command-registration.ts: CommandRegistrationResult", - "src/plugins/command-registration.ts: RegisteredPluginCommand", - "src/plugins/command-specs.ts: PluginCommandEntrySpec", - "src/plugins/compaction-provider.ts: CompactionProviderSummarizationInstructions", - "src/plugins/compaction-provider.ts: RegisteredCompactionProvider", - "src/plugins/compat/registry.ts: isPluginCompatCode", - "src/plugins/compat/registry.ts: KnownPluginCompatRecord", - "src/plugins/compat/registry.ts: listDeprecatedPluginCompatRecords", - "src/plugins/compat/registry.ts: PLUGIN_COMPAT_RECORDS", - "src/plugins/compat/types.ts: PluginCompatOwner", - "src/plugins/compat/types.ts: PluginCompatStatus", - "src/plugins/config-activation-shared.ts: PluginActivationCause", - "src/plugins/config-activation-shared.ts: PluginActivationDecision", - "src/plugins/config-activation-shared.ts: PluginExplicitSelectionCause", - "src/plugins/config-contract-matches.ts: PluginConfigContractMatch", - "src/plugins/config-contracts.ts: PluginConfigContractMatch", - "src/plugins/config-contracts.ts: PluginConfigContractMetadata", - "src/plugins/config-policy.ts: isBundledChannelEnabledByChannelConfig", - "src/plugins/config-policy.ts: NormalizedPluginsConfig", - "src/plugins/config-policy.ts: PluginActivationSource", - "src/plugins/config-policy.ts: PluginActivationState", - "src/plugins/config-policy.ts: resolvePluginActivationState", - "src/plugins/contracts/inventory/bundled-capability-metadata.ts: buildBundledPluginContractSnapshot", - "src/plugins/contracts/inventory/bundled-capability-metadata.ts: BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS", - "src/plugins/contracts/inventory/bundled-capability-metadata.ts: BUNDLED_LEGACY_PLUGIN_ID_ALIASES", - "src/plugins/contracts/inventory/bundled-capability-metadata.ts: BundledCapabilityManifest", - "src/plugins/contracts/inventory/bundled-capability-metadata.ts: hasBundledPluginContractSnapshotCapabilities", - "src/plugins/contracts/inventory/bundled-capability-metadata.ts: resolveBundledContractSnapshotPluginIds", - "src/plugins/contracts/registry.ts: providerContractPluginIds", - "src/plugins/contracts/registry.ts: requireProviderContractProvider", - "src/plugins/conversation-binding.ts: __testing", - "src/plugins/conversation-binding.ts: testing", - "src/plugins/current-plugin-metadata-snapshot.ts: resolvePluginMetadataControlPlaneFingerprint", - "src/plugins/default-enablement.ts: PluginDefaultEnablement", - "src/plugins/dependency-denylist.ts: blockedInstallDependencyPackageNames", - "src/plugins/dependency-denylist.ts: BlockedManifestDependencyFinding", - "src/plugins/doctor-contract-registry.ts: clearPluginDoctorContractRegistryCache", - "src/plugins/doctor-contract-registry.ts: PluginDoctorStateMigrationEntry", - "src/plugins/doctor-contract-registry.ts: setPluginDoctorContractRegistryModuleLoaderFactoryForTest", - "src/plugins/embedding-provider-runtime.ts: EmbeddingInput", - "src/plugins/embedding-provider-runtime.ts: EmbeddingProviderCallOptions", - "src/plugins/embedding-provider-runtime.ts: EmbeddingProviderCreateOptions", - "src/plugins/embedding-provider-runtime.ts: EmbeddingProviderCreateResult", - "src/plugins/embedding-provider-runtime.ts: EmbeddingProviderRuntime", - "src/plugins/embedding-provider-runtime.ts: listRegisteredEmbeddingProviders", - "src/plugins/embedding-provider-runtime.ts: RegisteredEmbeddingProvider", - "src/plugins/embedding-providers.ts: getEmbeddingProvider", - "src/plugins/embedding-providers.ts: listEmbeddingProviders", - "src/plugins/embedding-providers.ts: restoreEmbeddingProviders", - "src/plugins/externalized-bundled-plugins.ts: ExternalizedBundledPluginPreferredSource", - "src/plugins/gateway-startup-plugin-ids.ts: ConfiguredMemoryEmbeddingStartupProviderOwner", - "src/plugins/gateway-startup-plugin-ids.ts: MemoryEmbeddingStartupProviderSource", - "src/plugins/git-install.ts: GitPluginInstallResult", - "src/plugins/git-install.ts: GitPluginResolution", - "src/plugins/git-install.ts: ParsedGitPluginSpec", - "src/plugins/hardlink-policy.ts: isNixStorePluginRoot", - "src/plugins/hook-agent-context.ts: resolveAgentHookChannelId", - "src/plugins/hook-decision-types.ts: BLOCK_MESSAGE_PREFIX", - "src/plugins/hook-decision-types.ts: HookDecision", - "src/plugins/hook-decision-types.ts: HookDecisionBlock", - "src/plugins/hook-decision-types.ts: HookDecisionPass", - "src/plugins/hook-decision-types.ts: mergeHookDecisions", - "src/plugins/hook-registry.types.ts: PluginLegacyHookRegistration", - "src/plugins/hook-runner-global-state.ts: HookRunnerGlobalState", - "src/plugins/hooks.ts: HookFailurePolicy", - "src/plugins/hooks.ts: HookRunnerLogger", - "src/plugins/hooks.ts: HookRunnerOptions", - "src/plugins/hooks.ts: PluginHookAfterCompactionEvent", - "src/plugins/hooks.ts: PluginHookAfterToolCallEvent", - "src/plugins/hooks.ts: PluginHookAgentContext", - "src/plugins/hooks.ts: PluginHookAgentEndEvent", - "src/plugins/hooks.ts: PluginHookBeforeAgentFinalizeEvent", - "src/plugins/hooks.ts: PluginHookBeforeAgentFinalizeResult", - "src/plugins/hooks.ts: PluginHookBeforeAgentReplyEvent", - "src/plugins/hooks.ts: PluginHookBeforeAgentReplyResult", - "src/plugins/hooks.ts: PluginHookBeforeAgentRunEvent", - "src/plugins/hooks.ts: PluginHookBeforeAgentStartEvent", - "src/plugins/hooks.ts: PluginHookBeforeAgentStartResult", - "src/plugins/hooks.ts: PluginHookBeforeCompactionEvent", - "src/plugins/hooks.ts: PluginHookBeforeDispatchContext", - "src/plugins/hooks.ts: PluginHookBeforeDispatchEvent", - "src/plugins/hooks.ts: PluginHookBeforeDispatchResult", - "src/plugins/hooks.ts: PluginHookBeforeInstallContext", - "src/plugins/hooks.ts: PluginHookBeforeInstallEvent", - "src/plugins/hooks.ts: PluginHookBeforeInstallResult", - "src/plugins/hooks.ts: PluginHookBeforeMessageWriteEvent", - "src/plugins/hooks.ts: PluginHookBeforeMessageWriteResult", - "src/plugins/hooks.ts: PluginHookBeforeModelResolveEvent", - "src/plugins/hooks.ts: PluginHookBeforeModelResolveResult", - "src/plugins/hooks.ts: PluginHookBeforePromptBuildEvent", - "src/plugins/hooks.ts: PluginHookBeforePromptBuildResult", - "src/plugins/hooks.ts: PluginHookBeforeResetEvent", - "src/plugins/hooks.ts: PluginHookBeforeToolCallEvent", - "src/plugins/hooks.ts: PluginHookBeforeToolCallResult", - "src/plugins/hooks.ts: PluginHookCronChangedEvent", - "src/plugins/hooks.ts: PluginHookCronReconciledContext", - "src/plugins/hooks.ts: PluginHookCronReconciledEvent", - "src/plugins/hooks.ts: PluginHookGatewayContext", - "src/plugins/hooks.ts: PluginHookGatewayCronDeliveryStatus", - "src/plugins/hooks.ts: PluginHookGatewayCronJobState", - "src/plugins/hooks.ts: PluginHookGatewayCronRunStatus", - "src/plugins/hooks.ts: PluginHookGatewayStartEvent", - "src/plugins/hooks.ts: PluginHookGatewayStopEvent", - "src/plugins/hooks.ts: PluginHookInboundClaimContext", - "src/plugins/hooks.ts: PluginHookInboundClaimEvent", - "src/plugins/hooks.ts: PluginHookInboundClaimResult", - "src/plugins/hooks.ts: PluginHookLlmInputEvent", - "src/plugins/hooks.ts: PluginHookLlmOutputEvent", - "src/plugins/hooks.ts: PluginHookMessageContext", - "src/plugins/hooks.ts: PluginHookMessageReceivedEvent", - "src/plugins/hooks.ts: PluginHookMessageSendingEvent", - "src/plugins/hooks.ts: PluginHookMessageSendingResult", - "src/plugins/hooks.ts: PluginHookMessageSentEvent", - "src/plugins/hooks.ts: PluginHookModelCallEndedEvent", - "src/plugins/hooks.ts: PluginHookModelCallStartedEvent", - "src/plugins/hooks.ts: PluginHookReplyDispatchContext", - "src/plugins/hooks.ts: PluginHookReplyDispatchEvent", - "src/plugins/hooks.ts: PluginHookReplyDispatchResult", - "src/plugins/hooks.ts: PluginHookReplyPayload", - "src/plugins/hooks.ts: PluginHookReplyPayloadSendingContext", - "src/plugins/hooks.ts: PluginHookReplyPayloadSendingEvent", - "src/plugins/hooks.ts: PluginHookReplyPayloadSendingResult", - "src/plugins/hooks.ts: PluginHookResolveExecEnvContext", - "src/plugins/hooks.ts: PluginHookResolveExecEnvEvent", - "src/plugins/hooks.ts: PluginHookSessionContext", - "src/plugins/hooks.ts: PluginHookSessionEndEvent", - "src/plugins/hooks.ts: PluginHookSessionStartEvent", - "src/plugins/hooks.ts: PluginHookSubagentContext", - "src/plugins/hooks.ts: PluginHookSubagentDeliveryTargetEvent", - "src/plugins/hooks.ts: PluginHookSubagentDeliveryTargetResult", - "src/plugins/hooks.ts: PluginHookSubagentEndedEvent", - "src/plugins/hooks.ts: PluginHookSubagentSpawnedEvent", - "src/plugins/hooks.ts: PluginHookSubagentSpawningEvent", - "src/plugins/hooks.ts: PluginHookSubagentSpawningResult", - "src/plugins/hooks.ts: PluginHookToolContext", - "src/plugins/hooks.ts: PluginHookToolResultPersistContext", - "src/plugins/hooks.ts: PluginHookToolResultPersistEvent", - "src/plugins/hooks.ts: PluginHookToolResultPersistResult", - "src/plugins/hooks.ts: PluginTargetedInboundClaimOutcome", - "src/plugins/host-hook-attachments.ts: attachmentProbeFs", - "src/plugins/host-hook-attachments.ts: resolveAttachmentDelivery", - "src/plugins/host-hook-attachments.ts: resolveSessionAttachmentThreadId", - "src/plugins/host-hook-cleanup.ts: clearPluginOwnedSessionState", - "src/plugins/host-hook-cleanup.ts: PluginHostCleanupFailure", - "src/plugins/host-hook-cleanup.ts: PluginHostCleanupResult", - "src/plugins/host-hook-json.ts: PluginJsonPrimitive", - "src/plugins/host-hook-json.ts: PluginJsonValueLimits", - "src/plugins/host-hook-runtime.ts: listPluginSessionSchedulerJobs", - "src/plugins/host-hook-runtime.ts: PLUGIN_TERMINAL_EVENT_CLEANUP_WAIT_MS", - "src/plugins/host-hook-scheduled-turns.ts: buildPluginSchedulerCronName", - "src/plugins/host-hook-state.ts: clearPluginOwnedSessionState", - "src/plugins/host-hook-state.ts: drainPluginNextTurnInjections", - "src/plugins/host-hooks.ts: PluginControlUiTabGroup", - "src/plugins/host-hooks.ts: PluginJsonPrimitive", - "src/plugins/host-hooks.ts: PluginNextTurnInjectionPlacement", - "src/plugins/host-hooks.ts: PluginSessionAttachmentFile", - "src/plugins/host-hooks.ts: PluginSessionExtensionProjectionContext", - "src/plugins/host-hooks.ts: PluginToolPolicyDecision", - "src/plugins/host-tool-param-parsers.ts: HostToolDerivationOptions", - "src/plugins/host-tool-param-parsers.ts: HostToolDerivedParams", - "src/plugins/install-channel-specs.ts: ChannelInstallSpecs", - "src/plugins/install-policy-context.ts: BeforeInstallHookPayloadParams", - "src/plugins/install-security-scan.ts: PackageExecutableScanMetadata", - "src/plugins/install-security-scan.ts: PluginInstallRequestKind", - "src/plugins/install-source-info.ts: DescribePluginInstallSourceOptions", - "src/plugins/install-source-info.ts: PluginInstallClawHubSourceInfo", - "src/plugins/install-source-info.ts: PluginInstallLocalSourceInfo", - "src/plugins/install-source-info.ts: PluginInstallNpmPinState", - "src/plugins/install-source-info.ts: PluginInstallNpmSourceInfo", - "src/plugins/install-source-info.ts: PluginInstallSourceWarning", - "src/plugins/install.ts: installPluginFromDir", - "src/plugins/install.ts: PluginInstallErrorCode", - "src/plugins/install.ts: PluginNpmIntegrityDriftParams", - "src/plugins/installed-plugin-index-record-cache.ts: InstallRecordsCacheEntry", - "src/plugins/installed-plugin-index-store.ts: InstalledPluginIndexStoreState", - "src/plugins/installed-plugin-index.ts: CONFIG_PATH_ACTIVATION_COMPAT_CODE", - "src/plugins/installed-plugin-index.ts: InstalledPluginPackageChannelInfo", - "src/plugins/installed-plugin-index.ts: InstalledPluginStartupInfo", - "src/plugins/installed-plugin-index.ts: listEnabledInstalledPluginRecords", - "src/plugins/interactive-registry.ts: clearPluginInteractiveHandlerRegistrations", - "src/plugins/legacy-npm-declaration.ts: LegacyNpmPluginDeclaration", - "src/plugins/loader-cache-state.ts: PluginLoadReentryError", - "src/plugins/loader-channel-setup.ts: BundledRuntimeChannelRegistration", - "src/plugins/loader-provenance.ts: PluginProvenanceIndex", - "src/plugins/loader.ts: __testing", - "src/plugins/loader.ts: clearPluginLoaderCache", - "src/plugins/loader.ts: PluginLoadReentryError", - "src/plugins/loader.ts: testing", - "src/plugins/management-service.ts: derivePluginCategory", - "src/plugins/management-service.ts: ManagedPluginCatalog", - "src/plugins/management-service.ts: ManagedPluginCatalogEntry", - "src/plugins/management-service.ts: ManagedPluginInstallRequest", - "src/plugins/management-service.ts: overlayBundledOfficialPluginCatalogMetadata", - "src/plugins/manifest-command-aliases.ts: PluginManifestCommandAliasKind", - "src/plugins/manifest-contract-runtime.ts: ManifestContractRuntimePluginResolution", - "src/plugins/manifest-contribution-ids.ts: listManifestContributionIds", - "src/plugins/manifest-contribution-ids.ts: ListManifestContributionIdsParams", - "src/plugins/manifest-registry.ts: __testing", - "src/plugins/manifest-registry.ts: testing", - "src/plugins/manifest-tool-availability.ts: ManifestAuthAvailabilitySignal", - "src/plugins/manifest-tool-availability.ts: ManifestConfigAvailabilitySignal", - "src/plugins/manifest.ts: clearPluginManifestLoadCache", - "src/plugins/manifest.ts: ManifestKey", - "src/plugins/manifest.ts: MAX_PLUGIN_MANIFEST_BYTES", - "src/plugins/manifest.ts: OpenClawPackageCompat", - "src/plugins/manifest.ts: OpenClawPackageSetupFeatures", - "src/plugins/manifest.ts: OpenClawPackageStartup", - "src/plugins/manifest.ts: PluginManifestCapabilityProviderModeConfigSignal", - "src/plugins/manifest.ts: PluginManifestConfigLiteral", - "src/plugins/manifest.ts: PluginManifestDangerousConfigFlag", - "src/plugins/manifest.ts: PluginManifestDefaultPlatform", - "src/plugins/manifest.ts: PluginManifestLoadResult", - "src/plugins/manifest.ts: PluginManifestMediaUnderstandingCapability", - "src/plugins/manifest.ts: PluginManifestModelIdPrefixRule", - "src/plugins/manifest.ts: PluginManifestOnboardingScope", - "src/plugins/manifest.ts: PluginManifestProviderAuthChoice", - "src/plugins/manifest.ts: PluginManifestProviderBaseUrlGuard", - "src/plugins/manifest.ts: PluginManifestProviderRequestProvider", - "src/plugins/manifest.ts: PluginManifestSecretInputContracts", - "src/plugins/manifest.ts: PluginManifestSecretInputPath", - "src/plugins/manifest.ts: PluginManifestSetupProvider", - "src/plugins/manifest.ts: PluginManifestSetupProviderAuthEvidence", - "src/plugins/manifest.ts: PluginPackageChannelCliOption", - "src/plugins/marketplace.ts: MarketplaceInstallResult", - "src/plugins/marketplace.ts: MarketplaceManifest", - "src/plugins/marketplace.ts: MarketplacePluginEntry", - "src/plugins/marketplace.ts: MarketplacePluginListResult", - "src/plugins/marketplace.ts: MarketplaceShortcutResolution", - "src/plugins/memory-embedding-providers.ts: restoreMemoryEmbeddingProviders", - "src/plugins/memory-state.ts: MemoryPluginCapabilityRegistration", - "src/plugins/memory-state.ts: MemoryPluginPublicArtifactContentType", - "src/plugins/memory-state.ts: MemoryPromptSupplementRegistration", - "src/plugins/memory-state.ts: MemoryRuntimeBackendConfig", - "src/plugins/memory-state.ts: MemoryRuntimeQmdConfig", - "src/plugins/memory-state.ts: registerMemoryFlushPlanResolver", - "src/plugins/memory-state.ts: registerMemoryPromptSection", - "src/plugins/memory-state.ts: registerMemoryRuntime", - "src/plugins/min-host-version.ts: MIN_HOST_VERSION_FORMAT", - "src/plugins/min-host-version.ts: MinHostVersionCheckResult", - "src/plugins/min-host-version.ts: MinHostVersionRequirement", - "src/plugins/official-external-plugin-catalog-envelope.ts: createOfficialExternalPluginCatalogEnvelopePayload", - "src/plugins/official-external-plugin-catalog-envelope.ts: createOfficialExternalPluginCatalogEnvelopeSigningInput", - "src/plugins/official-external-plugin-catalog-envelope.ts: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE", - "src/plugins/official-external-plugin-catalog-envelope.ts: OfficialExternalPluginCatalogEnvelopeSignature", - "src/plugins/official-external-plugin-catalog-envelope.ts: OfficialExternalPluginCatalogEnvelopeVerificationResult", - "src/plugins/official-external-plugin-catalog-envelope.ts: OfficialExternalPluginCatalogSignedEnvelope", - "src/plugins/official-external-plugin-catalog-envelope.ts: OfficialExternalPluginCatalogTrustedSigningKey", - "src/plugins/official-external-plugin-catalog-snapshot-store.ts: HostedOfficialExternalPluginCatalogSnapshotStoreOptions", - "src/plugins/official-external-plugin-catalog.ts: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore", - "src/plugins/official-external-plugin-catalog.ts: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF", - "src/plugins/official-external-plugin-catalog.ts: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE", - "src/plugins/official-external-plugin-catalog.ts: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL", - "src/plugins/official-external-plugin-catalog.ts: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF", - "src/plugins/official-external-plugin-catalog.ts: filterOfficialExternalPluginCatalogEntriesBySourceRefs", - "src/plugins/official-external-plugin-catalog.ts: loadHostedOfficialExternalPluginCatalogEntries", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogFeedProfile", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogFeedSigningKey", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogFeedVerification", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogInstallCandidate", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogManifest", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogProfileConfig", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalPluginCatalogSourceProfile", - "src/plugins/official-external-plugin-catalog.ts: OfficialExternalProviderCatalogProvider", - "src/plugins/official-external-plugin-catalog.ts: parseOfficialExternalPluginCatalogEntries", - "src/plugins/official-external-plugin-catalog.ts: resolveOfficialExternalPluginCatalogProfileConfigFromConfig", - "src/plugins/official-external-plugin-catalog.ts: validateOfficialExternalPluginCatalogEntrySourceRefs", - "src/plugins/openai-compatible-embedding-provider.ts: createOpenAICompatibleEmbeddingProvider", - "src/plugins/openai-compatible-embedding-provider.ts: OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID", - "src/plugins/openai-compatible-embedding-provider.ts: OpenAICompatibleEmbeddingClient", - "src/plugins/package-compat.ts: PackagePluginApiRangeResult", - "src/plugins/plugin-cache-primitives.ts: ConfigScopedPromiseLoader", - "src/plugins/plugin-control-plane-context.ts: PluginControlPlaneContext", - "src/plugins/plugin-control-plane-context.ts: PluginDiscoveryContext", - "src/plugins/plugin-control-plane-context.ts: resolvePluginControlPlaneContext", - "src/plugins/plugin-control-plane-context.ts: ResolvePluginDiscoveryContextParams", - "src/plugins/plugin-control-plane-context.ts: resolvePluginDiscoveryFingerprint", - "src/plugins/plugin-load-profile.ts: PluginLoadProfileExtras", - "src/plugins/plugin-load-profile.ts: PluginLoadProfiler", - "src/plugins/plugin-load-profile.ts: PluginLoadProfileScope", - "src/plugins/plugin-lookup-table.ts: clearPluginLookUpTableMemoForTest", - "src/plugins/plugin-lookup-table.ts: LoadPluginLookUpTableParams", - "src/plugins/plugin-lookup-table.ts: PluginLookUpTableMetrics", - "src/plugins/plugin-module-loader-cache.ts: PluginModuleLoader", - "src/plugins/plugin-module-loader-cache.ts: PluginModuleLoaderCacheEntry", - "src/plugins/plugin-module-loader-cache.ts: PluginModuleLoaderStatsSnapshot", - "src/plugins/plugin-module-loader-cache.ts: resolvePluginModuleLoaderCacheEntry", - "src/plugins/plugin-module-loader-cache.ts: ResolvePluginModuleLoaderCacheEntryParams", - "src/plugins/plugin-registration-transaction.ts: PluginRegistrationTransaction", - "src/plugins/plugin-registry-contributions.ts: ListPluginContributionIdsParams", - "src/plugins/plugin-registry-contributions.ts: LoadPluginRegistryManifestParams", - "src/plugins/plugin-registry-contributions.ts: PluginLookUpTable", - "src/plugins/plugin-registry-contributions.ts: PluginRegistryContributionOptions", - "src/plugins/plugin-registry-contributions.ts: PluginRegistryIdNormalizerOptions", - "src/plugins/plugin-registry-contributions.ts: ResolveManifestContractOwnerPluginIdParams", - "src/plugins/plugin-registry-contributions.ts: ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams", - "src/plugins/plugin-registry-contributions.ts: ResolveManifestContractPluginIdsParams", - "src/plugins/plugin-registry-contributions.ts: ResolvePluginContributionOwnersParams", - "src/plugins/plugin-registry-contributions.ts: ResolveProviderOwnersParams", - "src/plugins/plugin-registry-snapshot.ts: DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV", - "src/plugins/plugin-registry-snapshot.ts: GetPluginRecordParams", - "src/plugins/plugin-registry-snapshot.ts: listPluginRecords", - "src/plugins/plugin-registry-snapshot.ts: PluginRegistryInspection", - "src/plugins/plugin-registry-snapshot.ts: PluginRegistrySnapshotDiagnosticCode", - "src/plugins/plugin-registry-snapshot.ts: PluginRegistrySnapshotResult", - "src/plugins/plugin-sdk-native-resolver.ts: InstallOpenClawPluginSdkNativeResolverOptions", - "src/plugins/plugin-sdk-native-resolver.ts: resetOpenClawPluginSdkNativeResolverForTest", - "src/plugins/plugin-version-drift.ts: PluginVersionDriftEntry", - "src/plugins/provider-auth-choice.ts: __testing", - "src/plugins/provider-auth-choice.ts: applyAuthChoicePluginProvider", - "src/plugins/provider-auth-choice.ts: ApplyProviderAuthChoiceParams", - "src/plugins/provider-auth-choice.ts: ApplyProviderAuthChoiceResult", - "src/plugins/provider-auth-choice.ts: PluginProviderAuthChoiceOptions", - "src/plugins/provider-auth-choice.ts: testing", - "src/plugins/provider-auth-choices.ts: ProviderOnboardAuthFlag (providerAuthChoices)", - "src/plugins/provider-auth-choices.ts: resolveManifestProviderOnboardAuthFlags (providerAuthChoices)", - "src/plugins/provider-auth-input.ts: extractEnvVarFromSourceLabel", - "src/plugins/provider-auth-input.ts: maybeApplyApiKeyFromOption", - "src/plugins/provider-auth-input.ts: resolveRefFallbackInput", - "src/plugins/provider-auth-input.ts: SecretInputModePromptCopy", - "src/plugins/provider-auth-input.ts: SecretRefSetupPromptCopy", - "src/plugins/provider-auth-mode.ts: SecretInputModePromptCopy", "src/plugins/provider-auth-token.ts: ANTHROPIC_SETUP_TOKEN_PREFIX", - "src/plugins/provider-catalog-result.ts: ProviderCatalogResultProjection", - "src/plugins/provider-discovery.ts: resolveInstalledPluginProviderContributionIds", - "src/plugins/provider-discovery.ts: ResolveInstalledPluginProviderContributionIdsParams", - "src/plugins/provider-discovery.ts: ResolveRuntimePluginDiscoveryProvidersParams", - "src/plugins/provider-hook-runtime.ts: ProviderRuntimePluginHandleParams", - "src/plugins/provider-hook-runtime.ts: ProviderRuntimePluginLookupParams", - "src/plugins/provider-model-compat.ts: shouldOmitEmptyArrayItems", - "src/plugins/provider-oauth-flow.ts: OAuthPrompt", - "src/plugins/provider-openai-chatgpt-oauth-tls.ts: OpenAIOAuthTlsPreflightResult", - "src/plugins/provider-runtime.runtime.ts: buildProviderMissingAuthMessageWithPlugin", - "src/plugins/provider-thinking.types.ts: ProviderThinkingLevel", - "src/plugins/provider-thinking.types.ts: ProviderThinkingLevelId", - "src/plugins/provider-thinking.types.ts: ProviderThinkingModelCompat", - "src/plugins/provider-wizard.ts: ProviderWizardOption (providerWizard)", - "src/plugins/providers.ts: __testing", - "src/plugins/providers.ts: ProviderRefOwnership", - "src/plugins/providers.ts: testing", - "src/plugins/providers.ts: UsageHookProviderPluginContract", - "src/plugins/public-surface-loader.ts: resetBundledPluginPublicArtifactLoaderForTest", - "src/plugins/public-surface-runtime.ts: normalizeBundledPluginDirName", - "src/plugins/register-plugin-cli-command-groups.ts: PluginCliCommandGroupEntry", - "src/plugins/register-plugin-cli-command-groups.ts: PluginCliCommandGroupMode", - "src/plugins/registry-types.ts: OpenClawPluginApi", - "src/plugins/registry-types.ts: OpenClawPluginHookOptions", - "src/plugins/registry-types.ts: OpenClawPluginHttpRouteParams", - "src/plugins/registry-types.ts: OpenClawPluginNodeHostCommand", - "src/plugins/registry-types.ts: OpenClawPluginToolContext", - "src/plugins/registry-types.ts: PluginAgentEventSubscriptionRegistryRegistration", - "src/plugins/registry-types.ts: PluginAgentHarnessRegistration", - "src/plugins/registry-types.ts: PluginChannelSetupRegistration", - "src/plugins/registry-types.ts: PluginCliBackendRegistration", - "src/plugins/registry-types.ts: PluginCliRegistration", - "src/plugins/registry-types.ts: PluginCodexAppServerExtensionFactoryRegistration", - "src/plugins/registry-types.ts: PluginControlUiDescriptorRegistryRegistration", - "src/plugins/registry-types.ts: PluginConversationBindingResolvedHandlerRegistration", - "src/plugins/registry-types.ts: PluginEmbeddingProviderRegistration", - "src/plugins/registry-types.ts: PluginHookHandlerMap", - "src/plugins/registry-types.ts: PluginHookRegistration", - "src/plugins/registry-types.ts: PluginHostedMediaResolverRegistration", - "src/plugins/registry-types.ts: PluginImageGenerationProviderRegistration", - "src/plugins/registry-types.ts: PluginInteractiveHandlerRegistryRegistration", - "src/plugins/registry-types.ts: PluginMediaUnderstandingProviderRegistration", - "src/plugins/registry-types.ts: PluginMemoryEmbeddingProviderRegistration", - "src/plugins/registry-types.ts: PluginMigrationProviderRegistration", - "src/plugins/registry-types.ts: PluginModelCatalogProviderRegistration", - "src/plugins/registry-types.ts: PluginMusicGenerationProviderRegistration", - "src/plugins/registry-types.ts: PluginNodeInvokePolicyRegistration", - "src/plugins/registry-types.ts: PluginProviderRegistration", - "src/plugins/registry-types.ts: PluginRealtimeTranscriptionProviderRegistration", - "src/plugins/registry-types.ts: PluginRealtimeVoiceProviderRegistration", - "src/plugins/registry-types.ts: PluginRegistrationMode", - "src/plugins/registry-types.ts: PluginReloadRegistration", - "src/plugins/registry-types.ts: PluginRuntimeLifecycleRegistryRegistration", - "src/plugins/registry-types.ts: PluginSecurityAuditCollectorRegistration", - "src/plugins/registry-types.ts: PluginSessionCatalogRegistration", - "src/plugins/registry-types.ts: PluginSessionExtensionRegistryRegistration", - "src/plugins/registry-types.ts: PluginSessionSchedulerJobRegistryRegistration", - "src/plugins/registry-types.ts: PluginSpeechProviderRegistration", - "src/plugins/registry-types.ts: PluginToolMetadataRegistryRegistration", - "src/plugins/registry-types.ts: PluginTranscriptsSourceProviderRegistration", - "src/plugins/registry-types.ts: PluginVideoGenerationProviderRegistration", - "src/plugins/registry-types.ts: PluginWebFetchProviderRegistration", - "src/plugins/registry-types.ts: PluginWebSearchProviderRegistration", - "src/plugins/registry.ts: PluginAgentHarnessRegistration", - "src/plugins/registry.ts: PluginChannelRegistration", - "src/plugins/registry.ts: PluginChannelSetupRegistration", - "src/plugins/registry.ts: PluginCliBackendRegistration", - "src/plugins/registry.ts: PluginCliRegistration", - "src/plugins/registry.ts: PluginCommandRegistration", - "src/plugins/registry.ts: PluginControlUiDescriptorRegistryRegistration", - "src/plugins/registry.ts: PluginConversationBindingResolvedHandlerRegistration", - "src/plugins/registry.ts: PluginHookRegistration", - "src/plugins/registry.ts: PluginHostedMediaResolverRegistration", - "src/plugins/registry.ts: PluginImageGenerationProviderRegistration", - "src/plugins/registry.ts: PluginMediaUnderstandingProviderRegistration", - "src/plugins/registry.ts: PluginMemoryEmbeddingProviderRegistration", - "src/plugins/registry.ts: PluginMusicGenerationProviderRegistration", - "src/plugins/registry.ts: PluginNodeHostCommandRegistration", - "src/plugins/registry.ts: PluginProviderRegistration", - "src/plugins/registry.ts: PluginRealtimeTranscriptionProviderRegistration", - "src/plugins/registry.ts: PluginRealtimeVoiceProviderRegistration", - "src/plugins/registry.ts: PluginRegistryParams", - "src/plugins/registry.ts: PluginReloadRegistration", - "src/plugins/registry.ts: PluginRuntimeLifecycleRegistryRegistration", - "src/plugins/registry.ts: PluginSecurityAuditCollectorRegistration", - "src/plugins/registry.ts: PluginServiceRegistration", - "src/plugins/registry.ts: PluginSessionExtensionRegistryRegistration", - "src/plugins/registry.ts: PluginSpeechProviderRegistration", - "src/plugins/registry.ts: PluginTextTransformsRegistration", - "src/plugins/registry.ts: PluginToolMetadataRegistryRegistration", - "src/plugins/registry.ts: PluginToolRegistration", - "src/plugins/registry.ts: PluginTrustedToolPolicyRegistryRegistration", - "src/plugins/registry.ts: PluginVideoGenerationProviderRegistration", - "src/plugins/registry.ts: PluginWebFetchProviderRegistration", - "src/plugins/registry.ts: PluginWebSearchProviderRegistration", - "src/plugins/registry.ts: PluginWorkerProviderRegistration", - "src/plugins/roots.ts: PluginCacheInputs", - "src/plugins/runtime-channel-state.ts: PLUGIN_REGISTRY_STATE", - "src/plugins/runtime-state.ts: getActivePluginChannelRegistryFromState", - "src/plugins/runtime-state.ts: RuntimeTrackedPluginRegistry", - "src/plugins/runtime/gateway-bindings.ts: clearGatewaySubagentRuntime", - "src/plugins/runtime/gateway-request-scope.ts: PluginRuntimeGatewayRequestScope", - "src/plugins/runtime/gateway-request-scope.ts: PluginRuntimePluginScope", - "src/plugins/runtime/index.ts: clearGatewaySubagentRuntime", "src/plugins/runtime/index.ts: createPluginRuntime", - "src/plugins/runtime/index.ts: CreatePluginRuntimeOptions", - "src/plugins/runtime/index.ts: setGatewayNodesRuntime", - "src/plugins/runtime/index.ts: setGatewaySubagentRuntime", - "src/plugins/runtime/load-context.ts: PluginRuntimeLoadContextOptions", - "src/plugins/runtime/load-context.ts: PluginRuntimeResolvedLoadValues", - "src/plugins/runtime/native-deps.ts: NativeDependencyHintParams", - "src/plugins/runtime/runtime-plugin-boundary.ts: resolvePluginRuntimeRecord", - "src/plugins/runtime/runtime-taskflow.types.ts: BoundTaskFlowCancelResult", - "src/plugins/runtime/runtime-taskflow.types.ts: BoundTaskFlowTaskRunResult", - "src/plugins/runtime/runtime-taskflow.types.ts: ManagedTaskFlowCreateParams", - "src/plugins/runtime/runtime-taskflow.types.ts: ManagedTaskFlowMutationErrorCode", - "src/plugins/runtime/runtime-tasks.types.ts: TaskFlowView", - "src/plugins/runtime/runtime-tasks.types.ts: TaskRunAggregateSummary", - "src/plugins/runtime/runtime-tasks.types.ts: TaskRunDetail", - "src/plugins/runtime/runtime-tasks.types.ts: TaskRunView", - "src/plugins/runtime/types-channel.ts: PluginRuntimeChannelContextEvent", - "src/plugins/runtime/types-channel.ts: PluginRuntimeChannelContextKey", - "src/plugins/runtime/types-channel.ts: PluginRuntimeChannelContextRegistry", - "src/plugins/runtime/types-channel.ts: RuntimeThreadBindingLifecycleRecord", - "src/plugins/runtime/types-core.ts: DeepReadonly", - "src/plugins/runtime/types-core.ts: HeartbeatRunResult", "src/plugins/runtime/types-core.ts: PluginRuntimeThinkingPolicy", "src/plugins/runtime/types-core.ts: PluginRuntimeThinkingPolicyLevel", "src/plugins/runtime/types-core.ts: PluginRuntimeThinkingPolicyRequest", - "src/plugins/runtime/types-core.ts: RuntimeRequestHeartbeatNowOptions", - "src/plugins/runtime/types-core.ts: RuntimeRequestHeartbeatOptions", - "src/plugins/runtime/types.ts: PluginManagedWorktree", - "src/plugins/runtime/types.ts: RuntimeNodeInvokeParams", - "src/plugins/runtime/types.ts: RuntimeNodeListParams", - "src/plugins/runtime/types.ts: RuntimeNodeListResult", - "src/plugins/runtime/types.ts: SubagentDeleteSessionParams", - "src/plugins/runtime/types.ts: SubagentGetSessionMessagesParams", - "src/plugins/runtime/types.ts: SubagentGetSessionMessagesResult", - "src/plugins/runtime/types.ts: SubagentGetSessionParams", - "src/plugins/runtime/types.ts: SubagentGetSessionResult", - "src/plugins/runtime/types.ts: SubagentWaitParams", - "src/plugins/runtime/types.ts: SubagentWaitResult", - "src/plugins/sdk-alias.ts: isBundledPluginExtensionPath", - "src/plugins/sdk-alias.ts: LoaderModuleResolveParams", - "src/plugins/sdk-alias.ts: normalizeJitiAliasTargetPath", - "src/plugins/sdk-alias.ts: resolvePluginLoaderJitiFsCacheDir", - "src/plugins/sdk-alias.ts: resolvePluginLoaderJitiFsCacheOption", - "src/plugins/session-entry-slot-keys.ts: _AssertAllSessionEntryKeysAreReserved", - "src/plugins/setup-registry.runtime.ts: testing", - "src/plugins/setup-registry.ts: clearPluginSetupRegistryCache", - "src/plugins/setup-registry.ts: PluginSetupRegistryDiagnosticCode", - "src/plugins/setup-registry.ts: setPluginSetupRegistryModuleLoaderFactoryForTest", - "src/plugins/slots.ts: normalizeKinds", - "src/plugins/slots.ts: slotKeysForPluginKind", - "src/plugins/slots.ts: SlotSelectionResult", "src/plugins/source-display.ts: formatPluginSourceForTable", - "src/plugins/source-display.ts: PluginSourceRoots", "src/plugins/source-display.ts: resolvePluginSourceRoots", - "src/plugins/status-dependencies-core.ts: PluginDependencyEntry", - "src/plugins/tools.ts: PluginToolMeta", - "src/plugins/tools.ts: resetPluginToolDescriptorCache", - "src/plugins/tools.ts: resetPluginToolFactoryCache", - "src/plugins/trusted-tool-policy.ts: TrustedToolPolicyDiagnosticEntry", - "src/plugins/uninstall.ts: createEmptyUninstallActions", - "src/plugins/uninstall.ts: PluginUninstallPlanResult", - "src/plugins/uninstall.ts: resolveUninstallDirectoryTarget", - "src/plugins/uninstall.ts: UninstallActions", - "src/plugins/uninstall.ts: uninstallPlugin", - "src/plugins/uninstall.ts: UninstallPluginParams", - "src/plugins/uninstall.ts: UninstallPluginResult", - "src/plugins/update.ts: PluginChannelSyncResult", - "src/plugins/update.ts: PluginChannelSyncSummary", - "src/plugins/update.ts: PluginUpdateLogger", - "src/plugins/update.ts: PluginUpdateStatus", - "src/plugins/update.ts: PluginUpdateSummary", - "src/plugins/web-provider-resolution-shared.ts: WebProviderCandidateResolution", - "src/plugins/web-provider-resolution-shared.ts: WebProviderConfigKey", - "src/plugins/web-provider-resolution-shared.ts: WebProviderContract", - "src/plugins/web-provider-runtime-shared.ts: ResolvePluginWebProvidersParams", - "src/plugins/web-provider-types.ts: WebFetchProviderConfiguredCredentialFallback", - "src/plugins/web-provider-types.ts: WebSearchProviderConfiguredCredentialFallback", - "src/plugins/worker-provider-registry.ts: listWorkerProviderRegistrations", "src/process/command-queue.ts: CommandLaneTaskTimeoutError", "src/process/command-queue.ts: resetCommandQueueStateForTest", "src/process/gateway-work-admission.ts: tryBeginGatewayIndependentRootWorkAdmission", diff --git a/scripts/lib/bundled-extension-manifest.ts b/scripts/lib/bundled-extension-manifest.ts index 11ab1f0899e0..084992915db9 100644 --- a/scripts/lib/bundled-extension-manifest.ts +++ b/scripts/lib/bundled-extension-manifest.ts @@ -1,8 +1,5 @@ // Bundled Extension Manifest script supports OpenClaw repository automation. -import { - MIN_HOST_VERSION_FORMAT, - parseMinHostVersionRequirement, -} from "../../src/plugins/min-host-version.ts"; +import { checkMinHostVersion } from "../../src/plugins/min-host-version.ts"; import { isRecord } from "../../src/utils.js"; export type ExtensionPackageJson = { @@ -38,11 +35,17 @@ export function collectBundledExtensionManifestErrors(extensions: BundledExtensi `bundled extension '${extension.id}' manifest invalid | openclaw.install.npmSpec must be a non-empty string`, ); } - const minHostVersionError = - install?.minHostVersion === undefined || - parseMinHostVersionRequirement(install.minHostVersion) + const minHostVersionCheck = + install?.minHostVersion === undefined ? null - : MIN_HOST_VERSION_FORMAT; + : checkMinHostVersion({ + currentVersion: "0.0.0", + minHostVersion: install.minHostVersion, + }); + const minHostVersionError = + minHostVersionCheck && !minHostVersionCheck.ok && minHostVersionCheck.kind === "invalid" + ? minHostVersionCheck.error + : null; if (minHostVersionError) { errors.push(`bundled extension '${extension.id}' manifest invalid | ${minHostVersionError}`); } diff --git a/scripts/load-channel-config-surface.ts b/scripts/load-channel-config-surface.ts index e8e5510a5e87..1baa9caf5048 100644 --- a/scripts/load-channel-config-surface.ts +++ b/scripts/load-channel-config-surface.ts @@ -6,9 +6,8 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import type { createJiti } from "jiti"; import { buildChannelConfigSchema } from "../src/channels/plugins/config-schema.js"; import { + buildPluginLoaderAliasMap, buildPluginLoaderJitiOptions, - resolvePluginSdkAliasFile, - resolvePluginSdkScopedAliasMap, } from "../src/plugins/sdk-alias.js"; type CreateJiti = typeof createJiti; @@ -152,19 +151,7 @@ export async function loadChannelConfigSurfaceModule( }; const loadViaJiti = (candidatePath: string) => { const resolvedPath = path.resolve(candidatePath); - const pluginSdkAlias = resolvePluginSdkAliasFile({ - srcFile: "root-alias.cjs", - distFile: "root-alias.cjs", - modulePath: resolvedPath, - pluginSdkResolution: "src", - }); - const aliasMap = { - ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), - ...resolvePluginSdkScopedAliasMap({ - modulePath: resolvedPath, - pluginSdkResolution: "src", - }), - }; + const aliasMap = buildPluginLoaderAliasMap(resolvedPath, "", undefined, "src"); const jiti = loadCreateJitiLoaderFactory()(import.meta.url, { ...buildPluginLoaderJitiOptions(aliasMap), interopDefault: true, diff --git a/scripts/plugin-boundary-report.ts b/scripts/plugin-boundary-report.ts index f202dfd0512d..29b2d9b43380 100644 --- a/scripts/plugin-boundary-report.ts +++ b/scripts/plugin-boundary-report.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node -// Plugin Boundary Report script supports OpenClaw repository automation. import { spawnSync } from "node:child_process"; import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs"; import { join, relative, resolve } from "node:path"; @@ -10,7 +9,7 @@ import { reservedBundledPluginSdkEntrypoints, supportedBundledFacadeSdkEntrypoints, } from "../src/plugin-sdk/entrypoints.ts"; -import { PLUGIN_COMPAT_RECORDS } from "../src/plugins/compat/registry.ts"; +import { listPluginCompatRecords } from "../src/plugins/compat/registry.ts"; import type { PluginCompatRecord } from "../src/plugins/compat/types.ts"; const REPO_ROOT = process.cwd(); @@ -374,7 +373,8 @@ function collectCompatDebt( today = new Date(), options: { includeReferenceFiles?: boolean } = {}, ): CompatDebtRecord[] { - return PLUGIN_COMPAT_RECORDS.filter((record) => record.status === "deprecated") + return listPluginCompatRecords() + .filter((record) => record.status === "deprecated") .map((record) => { const tokens = extractCompatTokens(record); const references = @@ -389,7 +389,7 @@ function collectCompatDebt( owner: record.owner, status: record.status, removeAfter: record.removeAfter, - replacement: record.replacement, + replacement: record.replacement as string, docsPath: record.docsPath, surfaces: record.surfaces, tokens, diff --git a/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts b/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts index e71d25dc5e52..275d157aef05 100644 --- a/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts +++ b/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts @@ -15,7 +15,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../plugins/hook-runner-global.js"; -import { addTestHook, createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; +import { addTestHook, createMockPluginRegistry } from "../plugins/hooks.test-fixtures.js"; import { patchPluginSessionExtension } from "../plugins/host-hook-state.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; diff --git a/src/agents/agent-tools.create-openclaw-coding-tools.test.ts b/src/agents/agent-tools.create-openclaw-coding-tools.test.ts index f827a4af5e87..b98a8b9842a2 100644 --- a/src/agents/agent-tools.create-openclaw-coding-tools.test.ts +++ b/src/agents/agent-tools.create-openclaw-coding-tools.test.ts @@ -18,7 +18,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../plugins/hooks.test-fixtures.js"; import "./test-helpers/fast-bash-tools.js"; import "./test-helpers/fast-coding-tools.js"; import "./test-helpers/fast-openclaw-tools.js"; diff --git a/src/agents/cli-runner.bundle-mcp.e2e.test.ts b/src/agents/cli-runner.bundle-mcp.e2e.test.ts deleted file mode 100644 index c6b0f6055bde..000000000000 --- a/src/agents/cli-runner.bundle-mcp.e2e.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -/** E2E proof for CLI runner bundle-MCP subprocess execution. */ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import type { CliBackendConfig } from "../config/types.js"; -import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; -import { - writeBundleProbeMcpServer, - writeClaudeBundle, - writeFakeClaudeCli, - writeFakeClaudeLiveCli, -} from "./bundle-mcp.test-harness.js"; -import type { - CliPreparedBackend, - PreparedCliRunContext, - RunCliAgentParams, -} from "./cli-runner/types.js"; - -// This e2e spins a real stdio MCP server plus a spawned CLI process. Keep the -// proof focused on bundle MCP config generation and subprocess execution; the -// full runCliAgent prepare graph has dedicated unit coverage and is expensive -// in cold Linux workers. -const E2E_TIMEOUT_MS = 30_000; - -type BundleMcpFixture = { - config: OpenClawConfig; - envSnapshot: ReturnType; - fakeClaudePath: string; - fakeClaudePidPath?: string; - pluginRoot: string; - sessionFile: string; - tempHome: string; - workspaceDir: string; -}; - -function isProcessAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ESRCH") { - return false; - } - throw error; - } -} - -async function resetBundleMcpPluginState() { - // Bundle MCP setup caches plugin discovery; reset between temp plugin roots. - const { resetPluginLoaderTestStateForTest } = await import("../plugins/loader.test-fixtures.js"); - const { clearPluginSetupRegistryCache } = await import("../plugins/setup-registry.js"); - resetPluginLoaderTestStateForTest(); - clearPluginSetupRegistryCache(); -} - -async function createBundleMcpFixture(params: { - liveSession?: boolean; - tempPrefix: string; -}): Promise { - // Fixture creates a real temp plugin + MCP server + fake CLI binary, but keeps - // it isolated from persisted plugin registry state. - await resetBundleMcpPluginState(); - const envSnapshot = captureEnv([ - "HOME", - "USERPROFILE", - "OPENCLAW_HOME", - "OPENCLAW_STATE_DIR", - "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY", - ]); - const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), params.tempPrefix)); - setTestEnvValue("HOME", tempHome); - setTestEnvValue("USERPROFILE", tempHome); - deleteTestEnvValue("OPENCLAW_HOME"); - deleteTestEnvValue("OPENCLAW_STATE_DIR"); - setTestEnvValue("OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY", "1"); - - const workspaceDir = path.join(tempHome, "workspace"); - const sessionFile = path.join(tempHome, "session.jsonl"); - const binDir = path.join(tempHome, "bin"); - const serverScriptPath = path.join(tempHome, "mcp", "bundle-probe.mjs"); - const fakeClaudePath = path.join( - binDir, - params.liveSession ? "fake-live-claude.mjs" : "fake-claude.mjs", - ); - const fakeClaudePidPath = params.liveSession - ? path.join(tempHome, "fake-live-claude.pid") - : undefined; - const pluginRoot = path.join(tempHome, ".openclaw", "extensions", "bundle-probe"); - await fs.mkdir(workspaceDir, { recursive: true }); - await writeBundleProbeMcpServer(serverScriptPath); - if (params.liveSession) { - await writeFakeClaudeLiveCli({ filePath: fakeClaudePath, pidPath: fakeClaudePidPath }); - } else { - await writeFakeClaudeCli(fakeClaudePath); - } - await writeClaudeBundle({ pluginRoot, serverScriptPath }); - - const config: OpenClawConfig = { - agents: { - defaults: { - workspace: workspaceDir, - }, - }, - plugins: { - load: { paths: [pluginRoot] }, - entries: { - "bundle-probe": { enabled: true }, - }, - }, - }; - - return { - config, - envSnapshot, - fakeClaudePath, - ...(fakeClaudePidPath ? { fakeClaudePidPath } : {}), - pluginRoot, - sessionFile, - tempHome, - workspaceDir, - }; -} - -function buildTestBackend(params: { - commandPath: string; - liveSession?: "claude-stdio"; -}): CliBackendConfig { - return { - command: "node", - args: [params.commandPath], - input: "stdin", - output: "jsonl", - clearEnv: [], - ...(params.liveSession ? { liveSession: params.liveSession } : {}), - }; -} - -async function prepareBundleMcpExecutionContext(params: { - backend: CliBackendConfig; - config: OpenClawConfig; - model: string; - prompt: string; - runId: string; - sessionFile: string; - sessionId: string; - workspaceDir: string; -}): Promise { - // Exercise bundle MCP config preparation while bypassing unrelated full - // runCliAgent context assembly. - const { prepareCliBundleMcpConfig } = await import("./cli-runner/bundle-mcp.js"); - const preparedBackend = (await prepareCliBundleMcpConfig({ - enabled: true, - mode: "claude-config-file", - backend: params.backend, - workspaceDir: params.workspaceDir, - config: params.config, - })) as CliPreparedBackend; - const runParams: RunCliAgentParams = { - sessionId: params.sessionId, - sessionFile: params.sessionFile, - workspaceDir: params.workspaceDir, - config: params.config, - prompt: params.prompt, - provider: "claude-cli", - model: params.model, - timeoutMs: 20_000, - runId: params.runId, - }; - - return { - params: runParams, - started: Date.now(), - workspaceDir: params.workspaceDir, - cwd: params.workspaceDir, - backendResolved: { - id: "claude-cli", - config: params.backend, - bundleMcp: true, - bundleMcpMode: "claude-config-file", - }, - preparedBackend, - reusableCliSession: { mode: "none" }, - hadSessionFile: false, - contextEngineConfig: params.config, - modelId: params.model, - normalizedModel: params.model, - systemPrompt: "Bundle MCP e2e test prompt.", - systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"], - claudeSkillsPluginArgs: [], - bootstrapPromptWarningLines: [], - authEpochVersion: 1, - }; -} - -async function cleanupFixture(fixture: BundleMcpFixture): Promise { - await fs.rm(fixture.tempHome, { recursive: true, force: true }); - fixture.envSnapshot.restore(); -} - -afterEach(async () => { - await resetBundleMcpPluginState(); -}); - -describe("CLI bundle MCP e2e", () => { - it( - "routes enabled bundle MCP config into the claude-cli backend and executes the tool", - { timeout: E2E_TIMEOUT_MS }, - async () => { - const { executePreparedCliRun } = await import("./cli-runner/execute.js"); - const fixture = await createBundleMcpFixture({ - tempPrefix: "openclaw-cli-bundle-mcp-", - }); - const context = await prepareBundleMcpExecutionContext({ - backend: buildTestBackend({ commandPath: fixture.fakeClaudePath }), - config: fixture.config, - model: "test-bundle", - prompt: "Use your configured MCP tools and report the bundle probe text.", - runId: "bundle-mcp-e2e", - sessionFile: fixture.sessionFile, - sessionId: "session:test", - workspaceDir: fixture.workspaceDir, - }); - - try { - const result = await executePreparedCliRun(context); - - expect(result.text).toContain("BUNDLE MCP OK FROM-BUNDLE"); - } finally { - await context.preparedBackend.cleanup?.(); - await cleanupFixture(fixture); - } - }, - ); - - it( - "exits one-shot Claude live-session runs and closes the live process", - { timeout: E2E_TIMEOUT_MS }, - async () => { - const { executePreparedCliRun } = await import("./cli-runner/execute.js"); - const { closeClaudeLiveSessionForContext } = - await import("./cli-runner/claude-live-session.js"); - const fixture = await createBundleMcpFixture({ - liveSession: true, - tempPrefix: "openclaw-cli-live-cleanup-", - }); - const context = await prepareBundleMcpExecutionContext({ - backend: buildTestBackend({ - commandPath: fixture.fakeClaudePath, - liveSession: "claude-stdio", - }), - config: fixture.config, - model: "test-live-bundle", - prompt: "Use your configured MCP tools and report the bundle probe text.", - runId: "bundle-mcp-live-cleanup-e2e", - sessionFile: fixture.sessionFile, - sessionId: "session:test-live-cleanup", - workspaceDir: fixture.workspaceDir, - }); - - try { - const result = await executePreparedCliRun(context); - await closeClaudeLiveSessionForContext(context); - - expect(result.text).toContain("LIVE BUNDLE MCP OK FROM-BUNDLE"); - expect(fixture.fakeClaudePidPath).toBeDefined(); - const fakeClaudePid = Number.parseInt( - await fs.readFile(fixture.fakeClaudePidPath!, "utf-8"), - 10, - ); - expect(Number.isFinite(fakeClaudePid)).toBe(true); - expect(isProcessAlive(fakeClaudePid)).toBe(false); - } finally { - await closeClaudeLiveSessionForContext(context); - await context.preparedBackend.cleanup?.(); - await cleanupFixture(fixture); - } - }, - ); -}); diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts index afe6db492299..44601067566f 100644 --- a/src/agents/cli-runner/prepare.test.ts +++ b/src/agents/cli-runner/prepare.test.ts @@ -22,7 +22,10 @@ import type { McpLoopbackRequestContext, } from "../../gateway/mcp-grant-store.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; -import { clearMemoryPluginState, registerMemoryPromptSection } from "../../plugins/memory-state.js"; +import { + clearMemoryPluginState, + registerMemoryPromptSection, +} from "../../plugins/memory-state.test-fixtures.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createChannelTestPluginBase, diff --git a/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts index 46f6beaa4e2c..61928d2a1d9e 100644 --- a/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts @@ -22,7 +22,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../../../plugins/hook-runner-global.js"; -import { createHookRunnerWithRegistry } from "../../../plugins/hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "../../../plugins/hooks.test-fixtures.js"; import { withEnvAsync } from "../../../test-utils/env.js"; import { wrapStreamFnWithDiagnosticModelCallEvents } from "./attempt.model-diagnostic-events.js"; diff --git a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts index 1b7068cab0ea..a75d7b57089c 100644 --- a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -14,7 +14,7 @@ import { buildMemorySystemPromptAddition } from "../../../context-engine/delegat import { clearMemoryPluginState, registerMemoryPromptSection, -} from "../../../plugins/memory-state.js"; +} from "../../../plugins/memory-state.test-fixtures.js"; import { addSubagentRunForTests, leasePendingAgentSteeringItems, diff --git a/src/agents/embedded-agent-runner/system-prompt.test.ts b/src/agents/embedded-agent-runner/system-prompt.test.ts index 3c237b5ca4d0..5f565a52bcfe 100644 --- a/src/agents/embedded-agent-runner/system-prompt.test.ts +++ b/src/agents/embedded-agent-runner/system-prompt.test.ts @@ -1,7 +1,10 @@ // Embedded system prompt tests cover prompt assembly for provider guidance, // delegation mode, workspace-only safety, memory sections, and active processes. import { afterEach, describe, expect, it, vi } from "vitest"; -import { clearMemoryPluginState, registerMemoryPromptSection } from "../../plugins/memory-state.js"; +import { + clearMemoryPluginState, + registerMemoryPromptSection, +} from "../../plugins/memory-state.test-fixtures.js"; import type { AgentSession } from "../sessions/index.js"; import { applySystemPromptToSession, buildEmbeddedSystemPrompt } from "./system-prompt.js"; diff --git a/src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts b/src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts index 8861782db051..c6fcc68822b2 100644 --- a/src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import { createInlineCodeState } from "../../packages/markdown-core/src/code-spans.js"; import { createHookRunner } from "../plugins/hooks.js"; -import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "../plugins/hooks.test-fixtures.js"; import { handleAgentEnd, handleAgentStart } from "./embedded-agent-subscribe.handlers.lifecycle.js"; import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; diff --git a/src/agents/harness/native-hook-relay.test.ts b/src/agents/harness/native-hook-relay.test.ts index 5c77fb79a130..e42da947eea6 100644 --- a/src/agents/harness/native-hook-relay.test.ts +++ b/src/agents/harness/native-hook-relay.test.ts @@ -12,7 +12,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../../plugins/hooks.test-fixtures.js"; import { patchPluginSessionExtension } from "../../plugins/host-hook-state.js"; import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; diff --git a/src/agents/harness/prompt-compaction-hook-helpers.test.ts b/src/agents/harness/prompt-compaction-hook-helpers.test.ts index 39f2543296ed..e4e210862a0a 100644 --- a/src/agents/harness/prompt-compaction-hook-helpers.test.ts +++ b/src/agents/harness/prompt-compaction-hook-helpers.test.ts @@ -3,7 +3,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../../plugins/hooks.test-fixtures.js"; import { resolveAgentHarnessBeforePromptBuildResult } from "./prompt-compaction-hook-helpers.js"; afterEach(() => { diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 3669ecd7b9c1..c5167fa68bbb 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -14,13 +14,9 @@ import { resetLogger, setLoggerOverride } from "../logging/logger.js"; import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js"; import { clearCurrentPluginMetadataSnapshot, - resolvePluginMetadataControlPlaneFingerprint, setCurrentPluginMetadataSnapshot, } from "../plugins/current-plugin-metadata-snapshot.js"; -import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; -import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js"; import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; -import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; import { CommandLaneTaskTimeoutError } from "../process/command-queue.js"; import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; @@ -229,68 +225,6 @@ function setDefaultPluginMetadataSnapshot(): void { }); } -function createModelNormalizerSnapshot(params: { - manifestHash: string; - prefix: string; -}): PluginMetadataSnapshot { - // Builds a process-stable plugin metadata snapshot with one model normalizer - // so fallback can prove manifest-policy cache invalidation. - const policyHash = resolveInstalledPluginIndexPolicyHash({}); - const index: InstalledPluginIndex = { - version: 1, - hostContractVersion: "test-host", - compatRegistryVersion: "test-compat", - migrationVersion: 1, - policyHash, - generatedAtMs: 0, - installRecords: {}, - plugins: [ - { - pluginId: "fallback-normalizer", - manifestPath: `/tmp/fallback-normalizer-${params.manifestHash}/openclaw.plugin.json`, - manifestHash: params.manifestHash, - source: `/tmp/fallback-normalizer-${params.manifestHash}/index.ts`, - rootDir: `/tmp/fallback-normalizer-${params.manifestHash}`, - origin: "global", - enabled: true, - startup: { - sidecar: false, - memory: false, - deferConfiguredChannelFullLoadUntilAfterListen: false, - agentHarnesses: [], - }, - compat: [], - }, - ], - diagnostics: [], - }; - return { - policyHash, - configFingerprint: resolvePluginMetadataControlPlaneFingerprint( - {}, - { - env: process.env, - index, - policyHash, - }, - ), - index, - registryDiagnostics: [], - plugins: [ - { - id: "fallback-normalizer", - modelIdNormalization: { - providers: { - demo: { - prefixWhenBare: params.prefix, - }, - }, - }, - }, - ], - } as unknown as PluginMetadataSnapshot; -} - afterEach(resetModelFallbackTestState); beforeEach(() => { @@ -3129,54 +3063,6 @@ describe("runWithModelFallback", () => { ).toEqual([{ provider: "ollama", model: "llama3" }]); }); - it("does not reuse fallback candidate cache entries across manifest normalization snapshots", () => { - const cfg = makeCfg({ - agents: { - defaults: { - model: { - fallbacks: [], - }, - }, - }, - }); - - try { - setCurrentPluginMetadataSnapshot( - createModelNormalizerSnapshot({ - manifestHash: "alpha", - prefix: "alpha", - }), - { config: {}, env: process.env }, - ); - expect( - testing.resolveFallbackCandidates({ - cfg, - provider: "demo", - model: "demo-model", - fallbacksOverride: [], - }), - ).toEqual([{ provider: "demo", model: "alpha/demo-model" }]); - - setCurrentPluginMetadataSnapshot( - createModelNormalizerSnapshot({ - manifestHash: "bravo", - prefix: "bravo", - }), - { config: {}, env: process.env }, - ); - expect( - testing.resolveFallbackCandidates({ - cfg, - provider: "demo", - model: "demo-model", - fallbacksOverride: [], - }), - ).toEqual([{ provider: "demo", model: "bravo/demo-model" }]); - } finally { - setDefaultPluginMetadataSnapshot(); - } - }); - it("defaults provider/model when missing (regression #946)", () => { const cfg = makeCfg({ agents: { diff --git a/src/agents/model-selection-cli.test.ts b/src/agents/model-selection-cli.test.ts index 6646175d22dc..ae80f512de61 100644 --- a/src/agents/model-selection-cli.test.ts +++ b/src/agents/model-selection-cli.test.ts @@ -1,82 +1,9 @@ // Verifies model-selection CLI provider detection from plugin metadata. -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/types.js"; -import { - clearCurrentPluginMetadataSnapshot, - resolvePluginMetadataControlPlaneFingerprint, - setCurrentPluginMetadataSnapshot, -} from "../plugins/current-plugin-metadata-snapshot.js"; -import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; -import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js"; -import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; -import { testing as setupRegistryRuntimeTesting } from "../plugins/setup-registry.runtime.js"; import { isCliProvider } from "./model-selection-cli.js"; -function setCliBackendMetadataSnapshot(cliBackends: string[]) { - // Builds a minimal current plugin metadata snapshot so isCliProvider can use - // descriptor metadata without loading setup runtime modules. - const policyHash = resolveInstalledPluginIndexPolicyHash({}); - const index: InstalledPluginIndex = { - version: 1, - hostContractVersion: "test-host", - compatRegistryVersion: "test-compat", - migrationVersion: 1, - policyHash, - generatedAtMs: 0, - installRecords: {}, - plugins: [ - { - pluginId: "anthropic", - manifestPath: "/tmp/anthropic/openclaw.plugin.json", - manifestHash: "test-manifest", - source: "/tmp/anthropic/index.ts", - rootDir: "/tmp/anthropic", - origin: "bundled", - enabled: true, - startup: { - sidecar: false, - memory: false, - deferConfiguredChannelFullLoadUntilAfterListen: false, - agentHarnesses: [], - }, - compat: [], - }, - ], - diagnostics: [], - }; - const snapshot = { - policyHash, - configFingerprint: resolvePluginMetadataControlPlaneFingerprint( - {}, - { - env: process.env, - index, - policyHash, - }, - ), - index, - plugins: [ - { - id: "anthropic", - origin: "bundled", - cliBackends, - }, - ], - } as unknown as PluginMetadataSnapshot; - setCurrentPluginMetadataSnapshot(snapshot, { config: {}, env: process.env }); -} - describe("isCliProvider", () => { - beforeEach(() => { - setupRegistryRuntimeTesting.resetDescriptorCache(); - setCliBackendMetadataSnapshot(["claude-cli"]); - }); - - afterEach(() => { - clearCurrentPluginMetadataSnapshot(); - setupRegistryRuntimeTesting.resetDescriptorCache(); - }); - it("returns true for setup-registered cli backends", () => { expect(isCliProvider("claude-cli", {} as OpenClawConfig)).toBe(true); }); diff --git a/src/agents/openclaw-tools.media-factory-plan.test.ts b/src/agents/openclaw-tools.media-factory-plan.test.ts index 36abe02398a2..7c7899df8036 100644 --- a/src/agents/openclaw-tools.media-factory-plan.test.ts +++ b/src/agents/openclaw-tools.media-factory-plan.test.ts @@ -2,7 +2,6 @@ import path from "node:path"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js"; import { clearCurrentPluginMetadataSnapshot, getCurrentPluginMetadataSnapshot, @@ -170,7 +169,7 @@ describe("optional media tool factory planning", () => { workflow: { "1": { inputs: {} } }, promptNodeId: "1", }); - setBundledPluginsDirOverrideForTest(path.join(process.cwd(), "extensions")); + vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(process.cwd(), "extensions")); legacyComfyToolNames = ( await createOpenClawToolsForTest({ config, @@ -181,7 +180,7 @@ describe("optional media tool factory planning", () => { clearCurrentPluginMetadataSnapshot(); resetPluginRuntimeStateForTest(); clearSecretsRuntimeSnapshot(); - setBundledPluginsDirOverrideForTest(undefined); + vi.unstubAllEnvs(); }); beforeEach(() => { @@ -193,7 +192,6 @@ describe("optional media tool factory planning", () => { clearCurrentPluginMetadataSnapshot(); resetPluginRuntimeStateForTest(); clearSecretsRuntimeSnapshot(); - setBundledPluginsDirOverrideForTest(undefined); vi.unstubAllEnvs(); }); @@ -238,7 +236,7 @@ describe("optional media tool factory planning", () => { it("does not plan media factories from workspace-scoped metadata without workspace context", () => { // Workspace snapshots are process-local facts and must not leak to unrelated runs. const config: OpenClawConfig = {}; - setBundledPluginsDirOverrideForTest("/nonexistent/bundled/plugins"); + vi.stubEnv("OPENCLAW_DISABLE_BUNDLED_PLUGINS", "1"); installSnapshot( config, [ diff --git a/src/agents/runtime-plugins.registry-reuse.test.ts b/src/agents/runtime-plugins.registry-reuse.test.ts deleted file mode 100644 index e554210ae546..000000000000 --- a/src/agents/runtime-plugins.registry-reuse.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Verifies runtime plugin loading can reuse a compatible gateway startup registry. -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; -import type { PluginRegistry } from "../plugins/registry-types.js"; -import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; - -const mocks = vi.hoisted(() => ({ - getCurrentPluginMetadataSnapshot: vi.fn(), - loadOpenClawPlugins: vi.fn(), -})); - -vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({ - getCurrentPluginMetadataSnapshot: mocks.getCurrentPluginMetadataSnapshot, -})); - -vi.mock("../plugins/loader.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadOpenClawPlugins: (...args: Parameters) => - mocks.loadOpenClawPlugins(...args), - }; -}); - -const [{ ensureRuntimePluginsLoaded }, { clearPluginLoaderCache, testing }] = await Promise.all([ - import("./runtime-plugins.js"), - import("../plugins/loader.js"), -]); - -function createRegistryWithPlugin(pluginId: string): PluginRegistry { - // Minimal active registry carrying just enough plugin identity for reuse checks. - const registry = createEmptyPluginRegistry(); - registry.plugins.push({ - id: pluginId, - status: "loaded", - } as never); - return registry; -} - -beforeEach(() => { - clearPluginLoaderCache(); - resetPluginRuntimeStateForTest(); - mocks.getCurrentPluginMetadataSnapshot.mockReset(); - mocks.loadOpenClawPlugins.mockReset(); -}); - -afterEach(() => { - clearPluginLoaderCache(); - resetPluginRuntimeStateForTest(); -}); - -describe("ensureRuntimePluginsLoaded registry reuse", () => { - it("reuses the compatible gateway startup registry on the dispatch caller path", () => { - // Matching cache key plus gateway-bindable mode means no second plugin load. - const config = { plugins: { allow: ["telegram"] } }; - const activeRegistry = createRegistryWithPlugin("telegram"); - activeRegistry.coreGatewayMethodNames = ["sessions.get", "sessions.list"]; - const startupLoadOptions = { - config, - activationSourceConfig: config, - autoEnabledReasons: {}, - workspaceDir: "/tmp/workspace", - onlyPluginIds: ["telegram"], - coreGatewayMethodNames: ["sessions.get", "sessions.list"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - preferBuiltPluginArtifacts: true, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupLoadOptions); - setActivePluginRegistry(activeRegistry, cacheKey, "gateway-bindable", "/tmp/workspace"); - mocks.getCurrentPluginMetadataSnapshot.mockReturnValue({ - startup: { - pluginIds: ["telegram"], - }, - }); - mocks.loadOpenClawPlugins.mockImplementation(() => { - throw new Error("dispatch should reuse the active gateway startup registry"); - }); - - ensureRuntimePluginsLoaded({ - config, - workspaceDir: "/tmp/workspace", - }); - - expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({ - config, - workspaceDir: "/tmp/workspace", - }); - expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled(); - }); -}); diff --git a/src/agents/system-prompt.memory.test.ts b/src/agents/system-prompt.memory.test.ts index 6c4a07a4ce27..38678a777509 100644 --- a/src/agents/system-prompt.memory.test.ts +++ b/src/agents/system-prompt.memory.test.ts @@ -1,7 +1,10 @@ // System prompt memory tests cover opt-out behavior when context engines own // memory prompt assembly for a run. import { afterEach, describe, expect, it } from "vitest"; -import { clearMemoryPluginState, registerMemoryPromptSection } from "../plugins/memory-state.js"; +import { + clearMemoryPluginState, + registerMemoryPromptSection, +} from "../plugins/memory-state.test-fixtures.js"; import { buildAgentSystemPrompt } from "./system-prompt.js"; describe("buildAgentSystemPrompt memory guidance", () => { diff --git a/src/agents/test-helpers/temp-plugin-extension-fixtures.ts b/src/agents/test-helpers/temp-plugin-extension-fixtures.ts index e67613543fda..c72e11ce5afa 100644 --- a/src/agents/test-helpers/temp-plugin-extension-fixtures.ts +++ b/src/agents/test-helpers/temp-plugin-extension-fixtures.ts @@ -6,7 +6,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { clearPluginLoaderCache } from "../../plugins/loader.js"; +import { clearPluginLoaderCache } from "../../plugins/loader.test-fixtures.js"; import { createEmptyPluginRegistry } from "../../plugins/registry.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; diff --git a/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts b/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts index 607a0cea8f72..df087cc50427 100644 --- a/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts +++ b/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts @@ -5,7 +5,7 @@ import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles.j import { clearSessionStoreCacheForTest } from "../config/sessions.js"; import { resetSystemEventsForTest } from "../infra/system-events.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; -import type { PluginProviderRegistration } from "../plugins/registry.js"; +import type { PluginRegistry } from "../plugins/registry.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import type { ProviderPlugin } from "../plugins/types.js"; import { resetSkillsRefreshForTest } from "../skills/runtime/refresh.js"; @@ -22,6 +22,8 @@ import { runReplyAgentMock, } from "./reply.directive.directive-behavior.e2e-mocks.js"; +type PluginProviderRegistration = PluginRegistry["providers"][number]; + const DEFAULT_TEST_MODEL_CATALOG: Array<{ id: string; name: string; diff --git a/src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts b/src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts index adcedef9fd23..785abbdebbb9 100644 --- a/src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts @@ -8,7 +8,7 @@ import { clearMemoryPluginState, registerMemoryCapability, type MemoryFlushPlanResolver, -} from "../../plugins/memory-state.js"; +} from "../../plugins/memory-state.test-fixtures.js"; import { runPreflightCompactionIfNeeded, setAgentRunnerMemoryTestDeps, diff --git a/src/auto-reply/reply/agent-runner-memory.test.ts b/src/auto-reply/reply/agent-runner-memory.test.ts index 5b031fd677ec..81d7cf0ba343 100644 --- a/src/auto-reply/reply/agent-runner-memory.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.test.ts @@ -12,7 +12,7 @@ import { clearMemoryPluginState, registerMemoryCapability, type MemoryFlushPlanResolver, -} from "../../plugins/memory-state.js"; +} from "../../plugins/memory-state.test-fixtures.js"; import type { TemplateContext } from "../templating.js"; import type { ReplyPayload } from "../types.js"; import { diff --git a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts index cce17c75190e..927648ad3469 100644 --- a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts @@ -24,7 +24,7 @@ import { clearMemoryPluginState, registerMemoryCapability, type MemoryFlushPlanResolver, -} from "../../plugins/memory-state.js"; +} from "../../plugins/memory-state.test-fixtures.js"; import { GatewayDrainingError } from "../../process/command-queue.js"; import { getReplyPayloadMetadata, type ReplyPayload } from "../reply-payload.js"; import type { TemplateContext } from "../templating.js"; diff --git a/src/auto-reply/reply/commands-diagnostics.test.ts b/src/auto-reply/reply/commands-diagnostics.test.ts index 4eb68c526bad..ea2c30a839e4 100644 --- a/src/auto-reply/reply/commands-diagnostics.test.ts +++ b/src/auto-reply/reply/commands-diagnostics.test.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import { clearPluginCommands, registerPluginCommand } from "../../plugins/commands.js"; import { createPluginRegistry } from "../../plugins/registry.js"; import type { PluginRuntime } from "../../plugins/runtime/types.js"; -import { createBundledPluginRecord } from "../../plugins/status.test-helpers.js"; +import { createBundledPluginRecord } from "../../plugins/status.test-fixtures.js"; import type { PluginCommandContext, PluginCommandHandler } from "../../plugins/types.js"; import type { MsgContext } from "../templating.js"; import { createDiagnosticsCommandHandler } from "./commands-diagnostics.js"; diff --git a/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts b/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts index 628eeafb2e99..ab6181df2db6 100644 --- a/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts @@ -1,7 +1,7 @@ // Tests dispatch-from-config reply dispatch integration and final payload routing. import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { clearAgentHarnesses } from "../../agents/harness/registry.js"; -import type { PluginHookReplyDispatchResult } from "../../plugins/hooks.js"; +import type { PluginHookReplyDispatchResult } from "../../plugins/hooks.test-fixtures.js"; import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; import { withReplyDispatcher } from "../dispatch-dispatcher.js"; import { setReplyPayloadMetadata } from "../reply-payload.js"; diff --git a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts index 3e5e8cbcdd38..9c820dac2b18 100644 --- a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts @@ -6,8 +6,8 @@ import type { StuckSessionRecoveryOutcome } from "../../logging/diagnostic-sessi import type { PluginHookBeforeDispatchResult, PluginHookReplyDispatchResult, - PluginTargetedInboundClaimOutcome, -} from "../../plugins/hooks.js"; +} from "../../plugins/hook-types.js"; +import type { createHookRunner } from "../../plugins/hooks.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createChannelTestPluginBase, @@ -25,6 +25,9 @@ type AbortResult = { rejectionReason?: "finalizing"; stoppedSubagents?: number; }; +type PluginTargetedInboundClaimOutcome = Awaited< + ReturnType["runInboundClaimForPluginOutcome"]> +>; const mocks = vi.hoisted(() => ({ isRoutableChannel: vi.fn((_channel: string | undefined) => true), diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 86a75709fe0e..7d5aaec534c6 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -23,7 +23,7 @@ import type { AcpRuntimeTurnInput, } from "../../plugin-sdk/acp-runtime.js"; import { clearPluginCommands, registerPluginCommand } from "../../plugins/commands.js"; -import type { PluginTargetedInboundClaimOutcome } from "../../plugins/hooks.js"; +import type { PluginTargetedInboundClaimOutcome } from "../../plugins/hooks.test-fixtures.js"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { interruptSessionWorkAdmissions, diff --git a/src/channels/plugins/contracts/test-helpers/runtime-artifacts.ts b/src/channels/plugins/contracts/test-helpers/runtime-artifacts.ts index bcf29e526812..05a7fb391fab 100644 --- a/src/channels/plugins/contracts/test-helpers/runtime-artifacts.ts +++ b/src/channels/plugins/contracts/test-helpers/runtime-artifacts.ts @@ -6,11 +6,8 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { resolveBundledChannelWorkspacePath } from "../../../../plugins/bundled-channel-runtime.js"; -import { - resolvePluginRuntimeModulePath, - resolvePluginRuntimeRecord, -} from "../../../../plugins/runtime/runtime-plugin-boundary.js"; +import { listBundledChannelPluginMetadata } from "../../../../plugins/bundled-channel-runtime.js"; +import { resolvePluginRuntimeModulePath } from "../../../../plugins/runtime/runtime-plugin-boundary.js"; const REPO_ROOT = fileURLToPath(new URL("../../../../../", import.meta.url)); @@ -19,10 +16,11 @@ function resolveBundledChannelWorkspaceArtifactPath( entryBaseName: string, ): string | null { const normalizedEntryBaseName = entryBaseName.replace(/\.(?:[cm]?js|ts)$/u, ""); - const pluginRoot = resolveBundledChannelWorkspacePath({ + const pluginRoot = listBundledChannelPluginMetadata({ rootDir: REPO_ROOT, - pluginId, - }); + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }).find((metadata) => metadata.manifest.id === pluginId)?.rootDir; if (!pluginRoot) { return null; } @@ -37,15 +35,19 @@ function resolveBundledChannelWorkspaceArtifactPath( function resolveBundledChannelContractArtifactUrl(pluginId: string, entryBaseName: string): string { const normalizedEntryBaseName = entryBaseName.replace(/\.(?:[cm]?js|ts)$/u, ""); - const record = resolvePluginRuntimeRecord(pluginId, () => { - throw new Error(`missing bundled channel plugin '${pluginId}'`); - }); - if (!record) { + const metadata = listBundledChannelPluginMetadata({ + rootDir: REPO_ROOT, + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }).find((entry) => entry.manifest.id === pluginId); + if (!metadata) { throw new Error(`missing bundled channel plugin '${pluginId}'`); } const modulePath = - resolvePluginRuntimeModulePath(record, normalizedEntryBaseName) ?? - resolveBundledChannelWorkspaceArtifactPath(pluginId, entryBaseName); + resolvePluginRuntimeModulePath( + { rootDir: metadata.rootDir, source: metadata.source.built }, + normalizedEntryBaseName, + ) ?? resolveBundledChannelWorkspaceArtifactPath(pluginId, entryBaseName); if (!modulePath) { throw new Error(`missing ${entryBaseName} for bundled channel plugin '${pluginId}'`); } diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index ef0605ae021a..61ffbb5d175f 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -5,7 +5,6 @@ import { vi } from "vitest"; import { getRuntimeConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; -import { createEmptyUninstallActions } from "../plugins/uninstall.js"; import type { CliMockOutputRuntime } from "./test-runtime-capture.js"; type UnknownMock = Mock<(...args: unknown[]) => unknown>; @@ -27,6 +26,20 @@ type UpdateNpmInstalledHookPacksFn = (typeof import("../hooks/update.js"))["updateNpmInstalledHookPacks"]; type PluginInstallRecordMap = Record; +function createEmptyUninstallActions() { + return { + entry: false, + install: false, + allowlist: false, + denylist: false, + loadPath: false, + memorySlot: false, + contextEngineSlot: false, + channelConfig: false, + directory: false, + }; +} + let mockInstalledPluginIndexInstallRecords: PluginInstallRecordMap = {}; function clonePluginInstallRecords(records: PluginInstallRecordMap): PluginInstallRecordMap { @@ -77,7 +90,6 @@ export const clearPluginRegistryLoadCache: UnknownMock = vi.fn(); export const applyExclusiveSlotSelection: UnknownMock = vi.fn(); export const planPluginUninstall: UnknownMock = vi.fn(); export const applyPluginUninstallDirectoryRemoval: AsyncUnknownMock = vi.fn(); -const uninstallPlugin: AsyncUnknownMock = vi.fn(); export const updateNpmInstalledPlugins: Mock = vi.fn(); export const updateNpmInstalledHookPacks: Mock = vi.fn(); export const promptYesNo: AsyncUnknownMock = vi.fn(); @@ -482,18 +494,6 @@ vi.mock("../plugins/uninstall.js", async (importOriginal) => { applyPluginUninstallDirectoryRemoval, ...args, )) as (typeof import("../plugins/uninstall.js"))["applyPluginUninstallDirectoryRemoval"], - uninstallPlugin: (( - ...args: Parameters<(typeof import("../plugins/uninstall.js"))["uninstallPlugin"]> - ) => - invokeMock< - Parameters<(typeof import("../plugins/uninstall.js"))["uninstallPlugin"]>, - ReturnType<(typeof import("../plugins/uninstall.js"))["uninstallPlugin"]> - >(uninstallPlugin, ...args)) as (typeof import("../plugins/uninstall.js"))["uninstallPlugin"], - resolveUninstallDirectoryTarget: ({ - installRecord, - }: { - installRecord?: { installPath?: string; sourcePath?: string }; - }) => installRecord?.installPath ?? installRecord?.sourcePath ?? null, }; }); @@ -732,7 +732,6 @@ export function resetPluginsCliTestState() { applyExclusiveSlotSelection.mockReset(); planPluginUninstall.mockReset(); applyPluginUninstallDirectoryRemoval.mockReset(); - uninstallPlugin.mockReset(); updateNpmInstalledPlugins.mockReset(); updateNpmInstalledHookPacks.mockReset(); promptText.mockReset(); @@ -860,12 +859,6 @@ export function resetPluginsCliTestState() { directoryRemoved: false, warnings: [], }); - uninstallPlugin.mockResolvedValue({ - ok: true, - config: {} as OpenClawConfig, - warnings: [], - actions: createEmptyUninstallActions(), - }); updateNpmInstalledPlugins.mockResolvedValue({ outcomes: [], changed: false, diff --git a/src/cli/plugins-cli.list.test.ts b/src/cli/plugins-cli.list.test.ts index 23011ec45d98..2cd7b53d5156 100644 --- a/src/cli/plugins-cli.list.test.ts +++ b/src/cli/plugins-cli.list.test.ts @@ -1,7 +1,7 @@ // Plugins CLI list tests cover plugin listing output and installed-state formatting. import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { createPluginRecord } from "../plugins/status.test-helpers.js"; +import { createPluginRecord } from "../plugins/status.test-fixtures.js"; import { buildPluginDiagnosticsReport, buildPluginInspectReport, diff --git a/src/cli/plugins-list-format.test.ts b/src/cli/plugins-list-format.test.ts index 53e795e8d1f9..42a26298fbbd 100644 --- a/src/cli/plugins-list-format.test.ts +++ b/src/cli/plugins-list-format.test.ts @@ -1,6 +1,6 @@ // Plugin list format tests cover installed plugin table and JSON formatting. import { describe, expect, it } from "vitest"; -import { createPluginRecord } from "../plugins/status.test-helpers.js"; +import { createPluginRecord } from "../plugins/status.test-fixtures.js"; import { formatPluginLine } from "./plugins-list-format.js"; describe("formatPluginLine", () => { diff --git a/src/cli/plugins-location-bridges.test.ts b/src/cli/plugins-location-bridges.test.ts index 5812c10297c3..ceeea918cee7 100644 --- a/src/cli/plugins-location-bridges.test.ts +++ b/src/cli/plugins-location-bridges.test.ts @@ -1,7 +1,7 @@ // Plugin location bridge tests cover CLI plugin path bridging between install surfaces. import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { InstalledPluginStartupInfo } from "../plugins/installed-plugin-index-types.js"; import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js"; -import type { InstalledPluginStartupInfo } from "../plugins/installed-plugin-index.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; const readPersistedInstalledPluginIndexMock = vi.fn(); diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 351bd75ac0a6..76280ce41f03 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -32,7 +32,7 @@ import { resetAgentEventsForTest, resetAgentRunContextForTest, } from "../infra/agent-events.js"; -import type { PluginProviderRegistration } from "../plugins/registry.js"; +import type { PluginProviderRegistration } from "../plugins/registry.test-fixtures.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import type { RuntimeEnv } from "../runtime.js"; import { AGENT_HARNESS_SESSION_KEY_RESERVED_MESSAGE } from "../sessions/agent-harness-session-key.js"; diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 7b7a0849324f..80fcd7c99899 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../agents/auth-profiles.js"; import type { ProviderAuthChoiceMetadata } from "../plugins/provider-auth-choices.js"; -import type { ProviderWizardOption } from "../plugins/provider-wizard.js"; import { buildAuthChoiceGroups, buildAuthChoiceOptions, @@ -10,6 +9,10 @@ import { } from "./auth-choice-options.js"; import { formatStaticAuthChoiceChoicesForCli } from "./auth-choice-options.static.js"; +type ProviderWizardOption = ReturnType< + (typeof import("../plugins/provider-wizard.js"))["resolveProviderWizardOptions"] +>[number]; + const resolveManifestProviderAuthChoices = vi.hoisted(() => vi.fn<() => ProviderAuthChoiceMetadata[]>(() => []), ); diff --git a/src/commands/auth-choice.apply.plugin-provider.test.ts b/src/commands/auth-choice.apply.plugin-provider.test.ts index 69f160227c40..a1d890409133 100644 --- a/src/commands/auth-choice.apply.plugin-provider.test.ts +++ b/src/commands/auth-choice.apply.plugin-provider.test.ts @@ -3,7 +3,6 @@ import { expectDefined } from "@openclaw/normalization-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { applyAuthChoiceLoadedPluginProvider, - applyAuthChoicePluginProvider, runProviderPluginAuthMethod, } from "../plugins/provider-auth-choice.js"; import type { ProviderPlugin } from "../plugins/types.js"; @@ -678,131 +677,4 @@ describe("applyAuthChoiceLoadedPluginProvider", () => { "openai/gpt-5.2": {}, }); }); - - it("returns an agent-scoped override for plugin auth choices when default model application is deferred", async () => { - const provider = buildProvider(); - resolvePluginProviders.mockReturnValue([provider]); - - const note = vi.fn(async () => {}); - const result = await applyAuthChoicePluginProvider( - buildParams({ - authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`, - agentId: "worker", - setDefaultModel: false, - prompter: { - note, - } as unknown as ApplyAuthChoiceParams["prompter"], - }), - { - authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`, - pluginId: LOCAL_PROVIDER_ID, - providerId: LOCAL_PROVIDER_ID, - methodId: LOCAL_AUTH_METHOD_ID, - label: LOCAL_PROVIDER_LABEL, - }, - ); - - expect(result?.agentModelOverride).toBe(LOCAL_DEFAULT_MODEL); - expect(result?.config.plugins).toEqual({ - entries: { - [LOCAL_PROVIDER_ID]: { - enabled: true, - }, - }, - }); - expect(runProviderModelSelectedHook).not.toHaveBeenCalled(); - expect(note).toHaveBeenCalledWith( - `Default model set to ${LOCAL_DEFAULT_MODEL} for agent "worker".`, - "Model configured", - ); - }); - - it("preserves the existing primary model for plugin auth choices that patch defaults", async () => { - const provider = buildProviderWithDefaultModelPatch(); - resolvePluginProviders.mockReturnValue([provider]); - const note = vi.fn(async () => {}); - - const result = await applyAuthChoicePluginProvider( - buildParams({ - authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`, - config: { - agents: { - defaults: { - model: { primary: EXISTING_DEFAULT_MODEL }, - models: { - [EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" }, - }, - }, - }, - }, - prompter: { - note, - } as unknown as ApplyAuthChoiceParams["prompter"], - preserveExistingDefaultModel: true, - }), - { - authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`, - pluginId: LOCAL_PROVIDER_ID, - providerId: LOCAL_PROVIDER_ID, - methodId: LOCAL_AUTH_METHOD_ID, - label: LOCAL_PROVIDER_LABEL, - }, - ); - - expect(result?.config.agents?.defaults?.model).toEqual({ - primary: EXISTING_DEFAULT_MODEL, - }); - expect(result?.config.agents?.defaults?.models).toEqual({ - [EXISTING_DEFAULT_MODEL]: { alias: "Bedrock" }, - [LOCAL_DEFAULT_MODEL]: { alias: "Local default" }, - }); - expect(result?.config.plugins).toEqual({ - entries: { - [LOCAL_PROVIDER_ID]: { - enabled: true, - }, - }, - }); - expect(runProviderModelSelectedHook).not.toHaveBeenCalled(); - expect(note).toHaveBeenCalledWith( - `Kept existing default model ${EXISTING_DEFAULT_MODEL}; ${LOCAL_DEFAULT_MODEL} is available.`, - "Model configured", - ); - }); - - it("stops early when the plugin is disabled in config", async () => { - const note = vi.fn(async () => {}); - - const result = await applyAuthChoicePluginProvider( - buildParams({ - config: { - plugins: { - enabled: false, - }, - }, - prompter: { - note, - } as unknown as ApplyAuthChoiceParams["prompter"], - }), - { - authChoice: LOCAL_PROVIDER_ID, - pluginId: LOCAL_PROVIDER_ID, - providerId: LOCAL_PROVIDER_ID, - label: LOCAL_PROVIDER_LABEL, - }, - ); - - expect(result).toEqual({ - config: { - plugins: { - enabled: false, - }, - }, - }); - expect(resolvePluginProviders).not.toHaveBeenCalled(); - expect(note).toHaveBeenCalledWith( - "Local Provider plugin is disabled (plugins disabled).", - LOCAL_PROVIDER_LABEL, - ); - }); }); diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 248e9990f254..add4fed4bcac 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -6,7 +6,6 @@ import { resolveAgentDir } from "../agents/agent-scope.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { ModelProviderConfig } from "../config/types.models.js"; -import { testing as providerAuthChoiceTesting } from "../plugins/provider-auth-choice.js"; import * as providerAuthChoices from "../plugins/provider-auth-choices.js"; import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../plugins/types.js"; import type { WizardPrompter } from "../wizard/prompts.js"; @@ -47,6 +46,13 @@ vi.mock("../plugins/provider-install-catalog.js", () => ({ resolveProviderInstallCatalogEntry: vi.fn(() => undefined), })); +vi.mock("../plugins/provider-auth-choice.runtime.js", () => ({ + resolvePluginProviders, + resolvePluginSetupProvider: () => undefined, + resolveProviderPluginChoice, + runProviderModelSelectedHook, +})); + vi.mock("./auth-choice.apply.api-providers.js", () => { const normalizeProviderIdLocal = (value: string) => value.trim().toLowerCase(); const resolveChoiceByKind = (params: { @@ -673,18 +679,9 @@ describe("applyAuthChoice", () => { authTestRoot = (await setupAuthTestEnv("openclaw-auth-")).stateDir; defaultProviderPlugins = await createDefaultProviderPlugins(); resolvePluginProviders.mockReturnValue(defaultProviderPlugins); - providerAuthChoiceTesting.setDepsForTest({ - loadPluginProviderRuntime: async () => ({ - resolvePluginProviders, - resolvePluginSetupProvider: () => undefined, - resolveProviderPluginChoice, - runProviderModelSelectedHook, - }), - }); }); afterAll(async () => { - providerAuthChoiceTesting.resetDepsForTest(); if (authTestRoot) { await fs.rm(authTestRoot, { recursive: true, force: true }); } @@ -1117,81 +1114,6 @@ describe("applyAuthChoice", () => { ); }); - it("enables the owning plugin for manifest provider auth choices", async () => { - await setupTempState(); - const provider = createFixedChoiceProvider({ - providerId: "github-copilot", - label: "GitHub Copilot", - choiceId: "github-copilot-github", - method: { - id: "github", - label: "GitHub Copilot", - kind: "token", - run: vi.fn( - async (): Promise => ({ - profiles: [ - { - profileId: "github-copilot:github", - credential: { - type: "token", - provider: "github-copilot", - token: "gho_copilot_test", - }, - }, - ], - defaultModel: "github-copilot/claude-opus-4.7", - }), - ), - }, - }); - const manifestSpy = vi - .spyOn(providerAuthChoices, "resolveManifestProviderAuthChoice") - .mockReturnValue({ - pluginId: "github-copilot", - providerId: "github-copilot", - methodId: "github", - choiceId: "github-copilot-github", - choiceLabel: "GitHub Copilot", - }); - providerAuthChoiceTesting.setDepsForTest({ - loadPluginProviderRuntime: async () => ({ - resolvePluginProviders, - resolvePluginSetupProvider: () => provider, - resolveProviderPluginChoice, - runProviderModelSelectedHook, - }), - }); - try { - const result = await applyAuthChoice({ - authChoice: "github-copilot-github", - config: { plugins: { entries: { "github-copilot": { enabled: false } } } }, - prompter: createPrompter({}), - runtime: createExitThrowingRuntime(), - setDefaultModel: true, - preserveExistingDefaultModel: true, - }); - - expect(result.config.plugins?.entries?.["github-copilot"]).toEqual({ enabled: true }); - expectAuthProfileConfig(result, "github-copilot:github", { - provider: "github-copilot", - mode: "token", - }); - expect(resolveAgentModelPrimaryValue(result.config.agents?.defaults?.model)).toBe( - "github-copilot/claude-opus-4.7", - ); - } finally { - manifestSpy.mockRestore(); - providerAuthChoiceTesting.setDepsForTest({ - loadPluginProviderRuntime: async () => ({ - resolvePluginProviders, - resolvePluginSetupProvider: () => undefined, - resolveProviderPluginChoice, - runProviderModelSelectedHook, - }), - }); - } - }); - it("uses explicit env for plugin auth resolution instead of host env", async () => { await setupTempState(); process.env.OPENAI_API_KEY = "sk-openai-host"; // pragma: allowlist secret diff --git a/src/commands/config-validation.test.ts b/src/commands/config-validation.test.ts index c47cee4553ee..7f7189976f27 100644 --- a/src/commands/config-validation.test.ts +++ b/src/commands/config-validation.test.ts @@ -1,7 +1,7 @@ // Config validation tests cover config snapshot validation and command error handling. import { beforeEach, describe, expect, it, vi } from "vitest"; import type { PluginCompatibilityNotice } from "../plugins/status.js"; -import { createCompatibilityNotice } from "../plugins/status.test-helpers.js"; +import { createCompatibilityNotice } from "../plugins/status.test-fixtures.js"; import { requireValidConfigSnapshot } from "./config-validation.js"; const { readConfigFileSnapshot, buildPluginCompatibilitySnapshotNotices } = vi.hoisted(() => ({ diff --git a/src/commands/doctor-workspace-status.test.ts b/src/commands/doctor-workspace-status.test.ts index 86fc794c9d8e..8b1228fcd849 100644 --- a/src/commands/doctor-workspace-status.test.ts +++ b/src/commands/doctor-workspace-status.test.ts @@ -8,7 +8,7 @@ import { createPluginLoadResult, createPluginRecord, createTypedHook, -} from "../plugins/status.test-helpers.js"; +} from "../plugins/status.test-fixtures.js"; import { collectWorkspaceStatusHealthFindings, noteWorkspaceStatus, diff --git a/src/commands/health.snapshot.test.ts b/src/commands/health.snapshot.test.ts index 4b8fb83c93b2..7ab5a2b8f25e 100644 --- a/src/commands/health.snapshot.test.ts +++ b/src/commands/health.snapshot.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelAccountSnapshot } from "../channels/plugins/types.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; -import { createPluginRecord } from "../plugins/status.test-helpers.js"; +import { createPluginRecord } from "../plugins/status.test-fixtures.js"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import type { HealthSummary } from "./health.js"; diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index 80a4b8a67417..7acb3f23fb9f 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -2,7 +2,7 @@ import type { Mock } from "vitest"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import type { PluginCompatibilityNotice } from "../plugins/status.js"; -import { createCompatibilityNotice } from "../plugins/status.test-helpers.js"; +import { createCompatibilityNotice } from "../plugins/status.test-fixtures.js"; import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; let envSnapshot: ReturnType; diff --git a/src/config/config.pruning-defaults.test.ts b/src/config/config.pruning-defaults.test.ts index 5e18df84c1bf..c919e44a0e4b 100644 --- a/src/config/config.pruning-defaults.test.ts +++ b/src/config/config.pruning-defaults.test.ts @@ -1,8 +1,5 @@ // Verifies pruning-related config defaults and migrations. -import path from "node:path"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js"; -import { resetBundledPluginPublicArtifactLoaderForTest } from "../plugins/public-surface-loader.js"; +import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "./config.js"; import { applyProviderConfigDefaultsForConfig } from "./provider-policy.js"; @@ -17,27 +14,6 @@ function applyAnthropicDefaultsForTest(config: OpenClawConfig) { } describe("config pruning defaults", () => { - beforeAll(() => { - setBundledPluginsDirOverrideForTest(path.resolve(import.meta.dirname, "../../extensions")); - // Provider public artifacts are process-stable. Keep their one-time load out of assertions. - applyAnthropicDefaultsForTest({ agents: { defaults: {} } }); - }); - - beforeEach(() => { - setBundledPluginsDirOverrideForTest(path.resolve(import.meta.dirname, "../../extensions")); - resetBundledPluginPublicArtifactLoaderForTest(); - vi.stubEnv( - "OPENCLAW_BUNDLED_PLUGINS_DIR", - path.resolve(import.meta.dirname, "../../extensions"), - ); - }); - - afterEach(() => { - setBundledPluginsDirOverrideForTest(undefined); - resetBundledPluginPublicArtifactLoaderForTest(); - vi.unstubAllEnvs(); - }); - it("does not enable contextPruning by default", () => { const cfg = applyAnthropicDefaultsForTest({ agents: { defaults: {} } }); diff --git a/src/config/io.best-effort.test.ts b/src/config/io.best-effort.test.ts index b7e7d07a0e83..07c31f396e61 100644 --- a/src/config/io.best-effort.test.ts +++ b/src/config/io.best-effort.test.ts @@ -1,9 +1,7 @@ // Covers best-effort config IO reads and warning behavior. import fs from "node:fs/promises"; -import path from "node:path"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely } from "../infra/kysely-sync.js"; -import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js"; import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; import { closeOpenClawStateDatabaseForTest, @@ -16,7 +14,6 @@ import { readConfigFileSnapshot, readSourceConfigBestEffort, } from "./config.js"; -import { applyProviderConfigDefaultsForConfig } from "./provider-policy.js"; import { withTempHome, writeOpenClawConfig } from "./test-helpers.js"; type ConfigHealthDatabase = Pick; @@ -34,20 +31,10 @@ function readConfigHealthRow(env: NodeJS.ProcessEnv, configPath: string) { } describe("readBestEffortConfig", () => { - beforeAll(() => { - setBundledPluginsDirOverrideForTest(path.resolve(import.meta.dirname, "../../extensions")); - // Materialized reads use the process-stable provider policy cache. - applyProviderConfigDefaultsForConfig({ provider: "anthropic", config: {}, env: {} }); - }); - afterEach(() => { closeOpenClawStateDatabaseForTest(); }); - afterAll(() => { - setBundledPluginsDirOverrideForTest(undefined); - }); - it("can read snapshots without updating config observation state", async () => { await withTempHome(async (home) => { const configPath = await writeOpenClawConfig(home, { diff --git a/src/config/plugin-auto-enable.prefer-over.test.ts b/src/config/plugin-auto-enable.prefer-over.test.ts deleted file mode 100644 index e3a544853243..000000000000 --- a/src/config/plugin-auto-enable.prefer-over.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Verifies plugin auto-enable prefer-over precedence rules. -import fs from "node:fs"; -import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; -import { cleanupTrackedTempDirs } from "../plugins/test-helpers/fs-fixtures.js"; - -vi.mock("../plugins/bundled-dir.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveBundledPluginsDir: (env: NodeJS.ProcessEnv = process.env) => - env.OPENCLAW_BUNDLED_PLUGINS_DIR, - }; -}); - -const tempDirs: string[] = []; - -function makeTempDir(): string { - const trustedRoot = path.resolve("dist-runtime", "extensions"); - fs.mkdirSync(trustedRoot, { recursive: true }); - const dir = fs.mkdtempSync(path.join(trustedRoot, ".openclaw-plugin-prefer-over-")); - tempDirs.push(dir); - return dir; -} - -function writeBundledChannelPackage(rootDir: string, channelId: string): void { - const pluginDir = path.join(rootDir, channelId); - fs.mkdirSync(pluginDir, { recursive: true }); - fs.writeFileSync( - path.join(pluginDir, "package.json"), - JSON.stringify({ - openclaw: { - channel: { - id: channelId, - label: "Cache Drift", - selectionLabel: "Cache Drift", - docsPath: `/channels/${channelId}`, - blurb: "Cache drift fixture", - }, - }, - }), - "utf-8", - ); - fs.writeFileSync( - path.join(pluginDir, "openclaw.plugin.json"), - JSON.stringify({ - id: channelId, - configSchema: { type: "object" }, - channels: [channelId], - }), - "utf-8", - ); - fs.writeFileSync( - path.join(pluginDir, "index.js"), - "export default { register() {} };\n", - "utf-8", - ); -} - -const EMPTY_MANIFEST_REGISTRY: PluginManifestRegistry = { - plugins: [], - diagnostics: [], -}; - -async function setBundledPluginsDirFixture(dir: string | undefined): Promise { - const { setBundledPluginsDirOverrideForTest } = await import("../plugins/bundled-dir.js"); - setBundledPluginsDirOverrideForTest(dir); -} - -afterEach(async () => { - await setBundledPluginsDirFixture(undefined); - vi.unstubAllEnvs(); - vi.resetModules(); - cleanupTrackedTempDirs(tempDirs); -}); - -describe("plugin auto-enable preferOver", () => { - it("tolerates bundled channel id metadata drift during auto-enable", async () => { - vi.resetModules(); - const rootDir = makeTempDir(); - const channelId = "cache-drift-channel"; - writeBundledChannelPackage(rootDir, channelId); - - vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", rootDir); - await setBundledPluginsDirFixture(rootDir); - const { normalizeChatChannelId } = await import("../channels/ids.js"); - expect(normalizeChatChannelId(channelId)).toBe(channelId); - - vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(rootDir, "missing")); - await setBundledPluginsDirFixture(undefined); - const { materializePluginAutoEnableCandidates } = await import("./plugin-auto-enable.js"); - - const result = materializePluginAutoEnableCandidates({ - config: { - channels: { - [channelId]: { token: "configured" }, - fallback: { token: "configured" }, - }, - }, - candidates: [ - { - pluginId: channelId, - kind: "channel-configured", - channelId, - }, - { - pluginId: "fallback", - kind: "channel-configured", - channelId: "fallback", - }, - ], - env: { - OPENCLAW_STATE_DIR: path.join(rootDir, "state"), - OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(rootDir, "missing"), - }, - manifestRegistry: EMPTY_MANIFEST_REGISTRY, - }); - - expect(result.config.channels?.[channelId]?.enabled).toBe(true); - }); -}); diff --git a/src/config/plugin-auto-enable.test-helpers.ts b/src/config/plugin-auto-enable.test-helpers.ts index a0876b8d49d4..e8c0bb765000 100644 --- a/src/config/plugin-auto-enable.test-helpers.ts +++ b/src/config/plugin-auto-enable.test-helpers.ts @@ -3,9 +3,9 @@ import path from "node:path"; import { clearCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; +import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import type { PluginOrigin } from "../plugins/plugin-origin.types.js"; -import { clearPluginSetupRegistryCache } from "../plugins/setup-registry.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "../plugins/test-helpers/fs-fixtures.js"; import type { OpenClawConfig } from "./types.openclaw.js"; @@ -14,7 +14,7 @@ const tempDirs: string[] = []; /** Clears auto-enable plugin caches and temp dirs between tests. */ export function resetPluginAutoEnableTestState(): void { clearCurrentPluginMetadataSnapshot(); - clearPluginSetupRegistryCache(); + clearPluginMetadataLifecycleCaches(); cleanupTrackedTempDirs(tempDirs); } diff --git a/src/config/test-helpers.ts b/src/config/test-helpers.ts index 9542ca53afa4..e5f8de676a3f 100644 --- a/src/config/test-helpers.ts +++ b/src/config/test-helpers.ts @@ -3,13 +3,13 @@ import fs from "node:fs/promises"; import path from "node:path"; import { withTempHome as withTempHomeBase } from "openclaw/plugin-sdk/test-env"; import { resetPluginLoaderTestStateForTest } from "../plugins/loader.test-fixtures.js"; -import { clearPluginSetupRegistryCache } from "../plugins/setup-registry.js"; +import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js"; import { resetConfigRuntimeState, type OpenClawConfig } from "./config.js"; function resetConfigTestRuntimeState(): void { resetConfigRuntimeState(); resetPluginLoaderTestStateForTest(); - clearPluginSetupRegistryCache(); + clearPluginMetadataLifecycleCaches(); } export async function withTempHome(fn: (home: string) => Promise): Promise { diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 478f01b3e9d9..2562d5f9273d 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -3,7 +3,10 @@ import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { MemoryCitationsMode } from "../config/types.memory.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { clearMemoryPluginState, registerMemoryPromptSection } from "../plugins/memory-state.js"; +import { + clearMemoryPluginState, + registerMemoryPromptSection, +} from "../plugins/memory-state.test-fixtures.js"; // --------------------------------------------------------------------------- // We dynamically import the registry so we can get a fresh module per test // group when needed. For most groups we use the shared singleton directly. diff --git a/src/cron/isolated-agent.model-overrides.test.ts b/src/cron/isolated-agent.model-overrides.test.ts index 0bb3ae1b4fe3..b2a43f284236 100644 --- a/src/cron/isolated-agent.model-overrides.test.ts +++ b/src/cron/isolated-agent.model-overrides.test.ts @@ -6,7 +6,7 @@ import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import type { PluginProviderRegistration } from "../plugins/registry.js"; +import type { PluginProviderRegistration } from "../plugins/registry.test-fixtures.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { diff --git a/src/gateway/gateway-acp-bind.live.test.ts b/src/gateway/gateway-acp-bind.live.test.ts index 926eb6c38cb0..1149bfea621a 100644 --- a/src/gateway/gateway-acp-bind.live.test.ts +++ b/src/gateway/gateway-acp-bind.live.test.ts @@ -14,7 +14,7 @@ import { getRuntimeConfig, } from "../config/config.js"; import { isTruthyEnvValue } from "../infra/env.js"; -import { clearPluginLoaderCache } from "../plugins/loader.js"; +import { clearPluginLoaderCache } from "../plugins/loader.test-fixtures.js"; import { pinActivePluginChannelRegistry, releasePinnedPluginChannelRegistry, diff --git a/src/gateway/gateway-acp-spawn-defaults.live.test.ts b/src/gateway/gateway-acp-spawn-defaults.live.test.ts index 22f568e55913..88cbea553202 100644 --- a/src/gateway/gateway-acp-spawn-defaults.live.test.ts +++ b/src/gateway/gateway-acp-spawn-defaults.live.test.ts @@ -21,7 +21,7 @@ import { loadSessionStore } from "../config/sessions/store.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isTruthyEnvValue } from "../infra/env.js"; -import { clearPluginLoaderCache } from "../plugins/loader.js"; +import { clearPluginLoaderCache } from "../plugins/loader.test-fixtures.js"; import { resetPluginRuntimeStateForTest } from "../plugins/runtime.js"; import { setTestEnvValue } from "../test-utils/env.js"; import { sleep } from "../utils.js"; diff --git a/src/gateway/gateway-codex-bind.live.test.ts b/src/gateway/gateway-codex-bind.live.test.ts index a7cb380abd09..f42d6e71a871 100644 --- a/src/gateway/gateway-codex-bind.live.test.ts +++ b/src/gateway/gateway-codex-bind.live.test.ts @@ -13,7 +13,7 @@ import { isTruthyEnvValue } from "../infra/env.js"; import { getSessionBindingService } from "../infra/outbound/session-binding-service.js"; import { findBundledPluginMetadataById } from "../plugins/bundled-plugin-metadata.js"; import { pluginCommands } from "../plugins/command-registry-state.js"; -import { clearPluginLoaderCache } from "../plugins/loader.js"; +import { clearPluginLoaderCache } from "../plugins/loader.test-fixtures.js"; import { pinActivePluginChannelRegistry, releasePinnedPluginChannelRegistry, diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index e42a3eadf8e8..2c4057ea5cdf 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -62,7 +62,6 @@ import { isTruthyEnvValue } from "../infra/env.js"; import type { ModelRegistry } from "../llm/model-registry.js"; import { normalizeGoogleModelId } from "../plugin-sdk/google-model-id.js"; import { resolveProviderThinkingProfile } from "../plugins/provider-runtime.js"; -import type { ProviderThinkingModelCompat } from "../plugins/provider-thinking.types.js"; import { DEFAULT_AGENT_ID } from "../routing/session-key.js"; import { stripAssistantInternalScaffolding } from "../shared/text/assistant-visible-text.js"; import { findFinalTagMatches, stripFinalTags } from "../shared/text/final-tags.js"; @@ -70,6 +69,11 @@ import { deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; import { getFreePort, isPortFree } from "../test-utils/ports.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { GatewayClient } from "./client.js"; + +type ProviderThinkingModelCompat = { + thinkingFormat?: string; + supportedReasoningEfforts?: readonly string[] | null; +}; import { hasExpectedSingleNonce, hasExpectedToolNonce, diff --git a/src/gateway/gateway.test.ts b/src/gateway/gateway.test.ts index efd29d75c096..1aa4bb7878a2 100644 --- a/src/gateway/gateway.test.ts +++ b/src/gateway/gateway.test.ts @@ -17,7 +17,7 @@ import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import type { GatewayAuthConfig, GatewayTailscaleConfig } from "../config/types.gateway.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resetAgentRunContextForTest } from "../infra/agent-events.js"; -import { clearGatewaySubagentRuntime } from "../plugins/runtime/index.js"; +import { clearGatewaySubagentRuntime } from "../plugins/runtime/gateway-bindings.test-fixtures.js"; import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; import { startGatewayServer } from "./server.js"; import { diff --git a/src/gateway/server-network-runtime.e2e.test.ts b/src/gateway/server-network-runtime.e2e.test.ts index dddd493d929b..17d2b1e2f5f9 100644 --- a/src/gateway/server-network-runtime.e2e.test.ts +++ b/src/gateway/server-network-runtime.e2e.test.ts @@ -9,7 +9,7 @@ import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.j import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import { resetAgentRunContextForTest } from "../infra/agent-events.js"; import { PROXY_ENV_KEYS } from "../infra/net/proxy-env.js"; -import { clearGatewaySubagentRuntime } from "../plugins/runtime/index.js"; +import { clearGatewaySubagentRuntime } from "../plugins/runtime/gateway-bindings.test-fixtures.js"; import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; import { startGatewayServer } from "./server.js"; import { getFreeGatewayPort } from "./test-helpers.e2e.js"; diff --git a/src/gateway/server-plugin-bootstrap.browser-plugin.integration.test.ts b/src/gateway/server-plugin-bootstrap.browser-plugin.integration.test.ts index 03f0a60f136d..133b88a326dd 100644 --- a/src/gateway/server-plugin-bootstrap.browser-plugin.integration.test.ts +++ b/src/gateway/server-plugin-bootstrap.browser-plugin.integration.test.ts @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createBundledBrowserPluginFixture } from "../../test/helpers/browser-bundled-plugin-fixture.js"; import type { OpenClawConfig } from "../config/config.js"; -import { clearPluginLoaderCache } from "../plugins/loader.js"; +import { clearPluginLoaderCache } from "../plugins/loader.test-fixtures.js"; import { resetPluginRuntimeStateForTest } from "../plugins/runtime.js"; import { loadGatewayStartupPlugins } from "./server-plugin-bootstrap.js"; diff --git a/src/gateway/server-plugins.subagent-ended-hook.test.ts b/src/gateway/server-plugins.subagent-ended-hook.test.ts index 2590c0e2a1ca..be9542f92449 100644 --- a/src/gateway/server-plugins.subagent-ended-hook.test.ts +++ b/src/gateway/server-plugins.subagent-ended-hook.test.ts @@ -3,7 +3,7 @@ */ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import type { PluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; +import type { PluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.test-fixtures.js"; import type { GatewayRequestContext, GatewayRequestOptions } from "./server-methods/types.js"; type HandleGatewayRequestOptions = GatewayRequestOptions & { diff --git a/src/gateway/server-plugins.test.ts b/src/gateway/server-plugins.test.ts index 93fda41abd4a..bb1ef36d8b95 100644 --- a/src/gateway/server-plugins.test.ts +++ b/src/gateway/server-plugins.test.ts @@ -5,7 +5,8 @@ import { createPluginRecord } from "../plugins/loader-records.js"; import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import type { PluginRegistry } from "../plugins/registry.js"; -import type { PluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; +import { clearGatewaySubagentRuntime } from "../plugins/runtime/gateway-bindings.test-fixtures.js"; +import type { PluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.test-fixtures.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import type { PluginDiagnostic } from "../plugins/types.js"; import type { GatewayRequestContext, GatewayRequestOptions } from "./server-methods/types.js"; @@ -381,7 +382,7 @@ beforeEach(() => { pluginRuntimeLoaderLogger.error.mockClear(); pluginRuntimeLoaderLogger.debug.mockClear(); handleGatewayRequest.mockReset(); - runtimeModule.clearGatewaySubagentRuntime(); + clearGatewaySubagentRuntime(); handleGatewayRequest.mockImplementation(async (opts: HandleGatewayRequestOptions) => { switch (opts.req.method) { case "agent": @@ -404,7 +405,7 @@ beforeEach(() => { afterEach(() => { serverPluginsModule.clearFallbackGatewayContext(); - runtimeModule.clearGatewaySubagentRuntime(); + clearGatewaySubagentRuntime(); runtimeRegistryModule.resetPluginRuntimeStateForTest(); }); diff --git a/src/gateway/server.sessions.list-changed.test.ts b/src/gateway/server.sessions.list-changed.test.ts index 14364cc21863..186b2b01595e 100644 --- a/src/gateway/server.sessions.list-changed.test.ts +++ b/src/gateway/server.sessions.list-changed.test.ts @@ -15,7 +15,7 @@ import { releasePinnedPluginSessionExtensionRegistry, setActivePluginRegistry, } from "../plugins/runtime.js"; -import { createPluginRecord } from "../plugins/status.test-helpers.js"; +import { createPluginRecord } from "../plugins/status.test-fixtures.js"; import { buildGatewaySessionRow } from "./session-utils.js"; import { embeddedRunMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js"; import { diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index 49cc5a588fd0..2f6894f08bd4 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -36,7 +36,7 @@ import { __testing as restartTesting } from "../infra/restart.js"; import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js"; import { rawDataToString } from "../infra/ws.js"; import { resetLogger, setLoggerOverride } from "../logging.js"; -import { clearGatewaySubagentRuntime } from "../plugins/runtime/gateway-bindings.js"; +import { clearGatewaySubagentRuntime } from "../plugins/runtime/gateway-bindings.test-fixtures.js"; import { resetGatewayWorkAdmission } from "../process/gateway-work-admission.js"; import { DEFAULT_AGENT_ID, diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 83b823d61101..dd23b76ed019 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -16,7 +16,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { SessionTranscriptAppendResult } from "../../config/sessions/transcript.js"; import * as mediaCapabilityModule from "../../media/read-capability.js"; import { createHookRunner } from "../../plugins/hooks.js"; -import { addTestHook } from "../../plugins/hooks.test-helpers.js"; +import { addTestHook } from "../../plugins/hooks.test-fixtures.js"; import { createEmptyPluginRegistry } from "../../plugins/registry.js"; import { pinActivePluginChannelRegistry, diff --git a/src/mcp/plugin-tools-serve.test.ts b/src/mcp/plugin-tools-serve.test.ts index 0c77ef542337..2a4f86ac0801 100644 --- a/src/mcp/plugin-tools-serve.test.ts +++ b/src/mcp/plugin-tools-serve.test.ts @@ -12,7 +12,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../plugins/hooks.test-fixtures.js"; import { PluginApprovalResolutions } from "../plugins/types.js"; import { createPluginToolsMcpHandlers } from "./plugin-tools-handlers.js"; diff --git a/src/media-understanding/runner.vision-skip.test.ts b/src/media-understanding/runner.vision-skip.test.ts index a5a39ea9dc06..f2a0898f8e24 100644 --- a/src/media-understanding/runner.vision-skip.test.ts +++ b/src/media-understanding/runner.vision-skip.test.ts @@ -8,7 +8,7 @@ import { withBundledPluginEnablementCompat, withBundledPluginVitestCompat, } from "../plugins/bundled-compat.js"; -import { testing as loaderTesting } from "../plugins/loader.js"; +import { resolvePluginRegistryLoadCacheKey } from "../plugins/loader.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; @@ -92,7 +92,7 @@ function setCompatibleActiveMediaUnderstandingRegistry( pluginIds, env: process.env, }); - const { cacheKey } = loaderTesting.resolvePluginLoadCacheContext({ + const cacheKey = resolvePluginRegistryLoadCacheKey({ config: compatibleConfig, env: process.env, }); diff --git a/src/pairing/pairing-challenge.test.ts b/src/pairing/pairing-challenge.test.ts index 9854af0edc25..0f4a09814eab 100644 --- a/src/pairing/pairing-challenge.test.ts +++ b/src/pairing/pairing-challenge.test.ts @@ -4,7 +4,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../plugins/hooks.test-fixtures.js"; import { issuePairingChallenge } from "./pairing-challenge.js"; describe("issuePairingChallenge", () => { diff --git a/src/plugin-sdk/channel-pairing.test.ts b/src/plugin-sdk/channel-pairing.test.ts index a535b4e7d579..100bb36a5d71 100644 --- a/src/plugin-sdk/channel-pairing.test.ts +++ b/src/plugin-sdk/channel-pairing.test.ts @@ -6,7 +6,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../plugins/hooks.test-fixtures.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import { createChannelPairingChallengeIssuer, diff --git a/src/plugin-sdk/facade-runtime.test.ts b/src/plugin-sdk/facade-runtime.test.ts index 9c2d3f67137d..efcd5243f592 100644 --- a/src/plugin-sdk/facade-runtime.test.ts +++ b/src/plugin-sdk/facade-runtime.test.ts @@ -4,7 +4,6 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js"; import { createPluginActivationSource, normalizePluginsConfig } from "../plugins/config-state.js"; import { clearCurrentPluginMetadataSnapshot, @@ -72,7 +71,6 @@ function createBundledPluginDir(prefix: string, marker: string): string { function useBundledPluginDirOverrideForTest(dir: string): void { process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir; - setBundledPluginsDirOverrideForTest(dir); } function createThrowingPluginDir(prefix: string): string { @@ -102,7 +100,6 @@ afterEach(() => { clearRuntimeConfigSnapshot(); clearCurrentPluginMetadataSnapshot(); resetFacadeRuntimeStateForTest(); - setBundledPluginsDirOverrideForTest(undefined); vi.doUnmock("../plugins/manifest-registry.js"); if (originalBundledPluginsDir === undefined) { delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; diff --git a/src/plugin-sdk/memory-host-core.test.ts b/src/plugin-sdk/memory-host-core.test.ts index 6afcd4f221ec..801b52392654 100644 --- a/src/plugin-sdk/memory-host-core.test.ts +++ b/src/plugin-sdk/memory-host-core.test.ts @@ -9,7 +9,7 @@ import { clearMemoryPluginState, registerMemoryCapability, registerMemoryPromptSection, -} from "../plugins/memory-state.js"; +} from "../plugins/memory-state.test-fixtures.js"; import * as memoryCoreAlias from "./memory-core.js"; import { buildActiveMemoryPromptSection, diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 9d983273f6a2..7b1862ed9163 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -14,7 +14,7 @@ import { import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; import type { PluginDiscoveryResult } from "./discovery.js"; -export type PluginActivationCompatConfig = { +type PluginActivationCompatConfig = { enablementPluginIds?: readonly string[]; vitestPluginIds?: readonly string[]; }; @@ -24,7 +24,7 @@ export type PluginActivationBundledCompatMode = { vitest?: boolean; }; -export type PluginActivationInputs = { +type PluginActivationInputs = { rawConfig?: OpenClawConfig; config?: OpenClawConfig; normalized: NormalizedPluginsConfig; @@ -33,11 +33,11 @@ export type PluginActivationInputs = { autoEnabledReasons: Record; }; -export type BundledPluginCompatibleActivationInputs = PluginActivationInputs & { +type BundledPluginCompatibleActivationInputs = PluginActivationInputs & { compatPluginIds: string[]; }; -export type BundledPluginCompatibleLoadValues = Pick< +type BundledPluginCompatibleLoadValues = Pick< BundledPluginCompatibleActivationInputs, "rawConfig" | "config" | "activationSourceConfig" | "autoEnabledReasons" | "compatPluginIds" >; diff --git a/src/plugins/activation-planner.ts b/src/plugins/activation-planner.ts index 9d26fec8cf46..c3c6923e89e2 100644 --- a/src/plugins/activation-planner.ts +++ b/src/plugins/activation-planner.ts @@ -17,7 +17,7 @@ import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-c import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.js"; /** Runtime surface that can request a lazily activated plugin owner. */ -export type PluginActivationPlannerTrigger = +type PluginActivationPlannerTrigger = | { kind: "command"; command: string } | { kind: "provider"; provider: string } | { kind: "agentHarness"; runtime: string } @@ -25,7 +25,7 @@ export type PluginActivationPlannerTrigger = | { kind: "route"; route: string } | { kind: "capability"; capability: PluginManifestActivationCapability }; -export type PluginActivationPlannerHintReason = +type PluginActivationPlannerHintReason = | "activation-agent-harness-hint" | "activation-capability-hint" | "activation-channel-hint" @@ -33,7 +33,7 @@ export type PluginActivationPlannerHintReason = | "activation-provider-hint" | "activation-route-hint"; -export type PluginActivationPlannerManifestReason = +type PluginActivationPlannerManifestReason = | "manifest-channel-owner" | "manifest-command-alias" | "manifest-hook-owner" @@ -41,11 +41,11 @@ export type PluginActivationPlannerManifestReason = | "manifest-setup-provider-owner" | "manifest-tool-contract"; -export type PluginActivationPlannerReason = +type PluginActivationPlannerReason = | PluginActivationPlannerHintReason | PluginActivationPlannerManifestReason; -export type PluginActivationPlanEntry = { +type PluginActivationPlanEntry = { pluginId: string; origin: PluginOrigin; reasons: readonly PluginActivationPlannerReason[]; diff --git a/src/plugins/active-runtime-registry.test.ts b/src/plugins/active-runtime-registry.test.ts index a74c47357223..a4a3d464aedb 100644 --- a/src/plugins/active-runtime-registry.test.ts +++ b/src/plugins/active-runtime-registry.test.ts @@ -4,7 +4,7 @@ import { getLoadedRuntimePluginRegistry, listLoadedRuntimePluginIdsAcrossSurfaces, } from "./active-runtime-registry.js"; -import { testing, clearPluginLoaderCache } from "./loader.js"; +import { clearPluginLoaderCache } from "./loader.test-fixtures.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRegistry } from "./registry-types.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js"; @@ -149,38 +149,4 @@ describe("getLoadedRuntimePluginRegistry", () => { }), ).toBeUndefined(); }); - - it("validates full loader cache compatibility when load options are provided", () => { - const registry = createRegistryWithPlugin("demo"); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - }, - }, - onlyPluginIds: ["demo"], - workspaceDir: "/tmp/ws", - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "default", "/tmp/ws"); - - expect( - getLoadedRuntimePluginRegistry({ - loadOptions, - }), - ).toBe(registry); - - expect( - getLoadedRuntimePluginRegistry({ - loadOptions: { - ...loadOptions, - config: { - plugins: { - allow: ["other"], - }, - }, - }, - }), - ).toBeUndefined(); - }); }); diff --git a/src/plugins/agent-tool-result-middleware-loader.ts b/src/plugins/agent-tool-result-middleware-loader.ts index 5d7a466f8441..5cff1921936f 100644 --- a/src/plugins/agent-tool-result-middleware-loader.ts +++ b/src/plugins/agent-tool-result-middleware-loader.ts @@ -205,8 +205,3 @@ export async function loadAgentToolResultMiddlewaresForRuntime(params: { return listAgentToolResultMiddlewares(params.runtime); } } - -export const testing = { - listMiddlewareOwnerPluginIds, -}; -export { testing as __testing }; diff --git a/src/plugins/agent-tool-result-middleware.ts b/src/plugins/agent-tool-result-middleware.ts index 7d0f07c60749..f430012890b9 100644 --- a/src/plugins/agent-tool-result-middleware.ts +++ b/src/plugins/agent-tool-result-middleware.ts @@ -54,11 +54,6 @@ export function normalizeAgentToolResultMiddlewareRuntimes( } return normalized; } - -/** @deprecated Use normalizeAgentToolResultMiddlewareRuntimes. */ -export const normalizeAgentToolResultMiddlewareHarnesses = - normalizeAgentToolResultMiddlewareRuntimes; - export function normalizeAgentToolResultMiddlewareRuntimeIds( runtimes: readonly string[] | undefined, ): AgentToolResultMiddlewareRuntime[] { diff --git a/src/plugins/api-builder.ts b/src/plugins/api-builder.ts index 6ca1ac78bafd..1e7740bcba90 100644 --- a/src/plugins/api-builder.ts +++ b/src/plugins/api-builder.ts @@ -4,7 +4,7 @@ import { attachPluginApiFacades, type OpenClawPluginApiWithoutFacades } from "./ import type { PluginRuntime } from "./runtime/types.js"; import type { OpenClawPluginApi, PluginLogger } from "./types.js"; -export type BuildPluginApiParams = { +type BuildPluginApiParams = { id: string; name: string; version?: string; diff --git a/src/plugins/api-lifecycle.ts b/src/plugins/api-lifecycle.ts index b56f0de9361c..a99cdaf6d5c4 100644 --- a/src/plugins/api-lifecycle.ts +++ b/src/plugins/api-lifecycle.ts @@ -9,10 +9,10 @@ type FunctionPropertyNames = Extract< >; /** Names of plugin API methods exposed on the OpenClaw plugin API. */ -export type PluginApiMethodName = FunctionPropertyNames; +type PluginApiMethodName = FunctionPropertyNames; /** Lifecycle policy for whether a plugin API method can be called after registration. */ -export type PluginApiLifecyclePolicy = { +type PluginApiLifecyclePolicy = { phase: "registration" | "runtime"; lateCallable: boolean; }; diff --git a/src/plugins/bundle-commands.ts b/src/plugins/bundle-commands.ts index 79fb2be3db60..a55d8e8104bf 100644 --- a/src/plugins/bundle-commands.ts +++ b/src/plugins/bundle-commands.ts @@ -24,7 +24,7 @@ import { } from "./config-state.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions.js"; -export type ClaudeBundleCommandSpec = { +type ClaudeBundleCommandSpec = { pluginId: string; rawName: string; description: string; diff --git a/src/plugins/bundle-config-shared.ts b/src/plugins/bundle-config-shared.ts index 426e943d82f0..a3c82cbfe19a 100644 --- a/src/plugins/bundle-config-shared.ts +++ b/src/plugins/bundle-config-shared.ts @@ -12,7 +12,7 @@ type ReadBundleJsonResult = | { ok: true; raw: Record } | { ok: false; error: string }; -export type BundleServerRuntimeSupport = { +type BundleServerRuntimeSupport = { hasSupportedServer: boolean; supportedServerNames: string[]; unsupportedServerNames: string[]; diff --git a/src/plugins/bundle-lsp.ts b/src/plugins/bundle-lsp.ts index 0c2b177e4ce1..754c3a310a76 100644 --- a/src/plugins/bundle-lsp.ts +++ b/src/plugins/bundle-lsp.ts @@ -22,12 +22,12 @@ import type { PluginBundleFormat } from "./manifest-types.js"; export type BundleLspServerConfig = Record; /** Merged LSP config contributed by enabled plugin bundles. */ -export type BundleLspConfig = { +type BundleLspConfig = { lspServers: Record; }; /** Runtime support summary for bundle-declared LSP servers. */ -export type BundleLspRuntimeSupport = { +type BundleLspRuntimeSupport = { hasStdioServer: boolean; supportedServerNames: string[]; unsupportedServerNames: string[]; diff --git a/src/plugins/bundle-manifest.test.ts b/src/plugins/bundle-manifest.test.ts index 17982d85844c..40287d0405f5 100644 --- a/src/plugins/bundle-manifest.test.ts +++ b/src/plugins/bundle-manifest.test.ts @@ -9,13 +9,17 @@ import { detectBundleManifestFormat, loadBundleManifest, } from "./bundle-manifest.js"; -import type { BundlePluginManifest } from "./bundle-manifest.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir, mkdirSafeDir, } from "./test-helpers/fs-fixtures.js"; +type BundlePluginManifest = Extract< + ReturnType, + { ok: true } +>["manifest"]; + type ReadonlyBundleManifestExpectation = Omit< BundlePluginManifest, "capabilities" | "hooks" | "settingsFiles" | "skills" diff --git a/src/plugins/bundle-manifest.ts b/src/plugins/bundle-manifest.ts index fe0e64f6258f..ee9fe2d20198 100644 --- a/src/plugins/bundle-manifest.ts +++ b/src/plugins/bundle-manifest.ts @@ -24,7 +24,7 @@ export const CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH = ".claude-plugin/plugin.json" export const CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH = ".cursor-plugin/plugin.json"; /** Normalized bundle manifest shape consumed by plugin discovery. */ -export type BundlePluginManifest = { +type BundlePluginManifest = { id: string; name?: string; description?: string; @@ -38,7 +38,7 @@ export type BundlePluginManifest = { capabilities: string[]; }; -export type BundleManifestLoadResult = +type BundleManifestLoadResult = | { ok: true; manifest: BundlePluginManifest; manifestPath: string } | { ok: false; error: string; manifestPath: string }; diff --git a/src/plugins/bundle-mcp.ts b/src/plugins/bundle-mcp.ts index 199d7dab33d5..c3e77daa8f8a 100644 --- a/src/plugins/bundle-mcp.ts +++ b/src/plugins/bundle-mcp.ts @@ -32,11 +32,11 @@ export type BundleMcpDiagnostic = { message: string; }; -export type EnabledBundleMcpConfigResult = { +type EnabledBundleMcpConfigResult = { config: BundleMcpConfig; diagnostics: BundleMcpDiagnostic[]; }; -export type BundleMcpRuntimeSupport = { +type BundleMcpRuntimeSupport = { hasSupportedStdioServer: boolean; supportedServerNames: string[]; unsupportedServerNames: string[]; diff --git a/src/plugins/bundled-capability-metadata.test.ts b/src/plugins/bundled-capability-metadata.test.ts index b3408a78314c..eb33c80cce3f 100644 --- a/src/plugins/bundled-capability-metadata.test.ts +++ b/src/plugins/bundled-capability-metadata.test.ts @@ -1,21 +1,10 @@ // Verifies bundled capability metadata emitted by plugins. import fs from "node:fs"; import path from "node:path"; -import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it } from "vitest"; import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js"; import { listGitTrackedFiles, toRepoRelativePath } from "../test-utils/repo-files.js"; -import { normalizeBundledPluginStringList } from "./bundled-plugin-scan.js"; -import { - BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS, - BUNDLED_LEGACY_PLUGIN_ID_ALIASES, - BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS, - buildBundledPluginContractSnapshot, - hasBundledPluginContractSnapshotCapabilities, -} from "./contracts/inventory/bundled-capability-metadata.js"; import { pluginTestRepoRoot as repoRoot } from "./generated-plugin-test-helpers.js"; -import type { OpenClawPackageManifest } from "./manifest.js"; -import type { PluginManifest } from "./manifest.js"; function listGitExtensionPackagePaths(extensionsDir: string): string[] | null { const relativeDir = toRepoRelativePath(repoRoot, extensionsDir); @@ -45,24 +34,6 @@ function listExtensionPackagePaths(extensionsDir: string): string[] { .filter((packagePath) => fs.existsSync(packagePath)); } -function readManifestRecords(): PluginManifest[] { - const extensionsDir = path.join(repoRoot, "extensions"); - return listExtensionPackagePaths(extensionsDir) - .filter((packagePath) => { - const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8")) as { - openclaw?: OpenClawPackageManifest; - }; - return normalizeBundledPluginStringList(packageJson.openclaw?.extensions).length > 0; - }) - .map( - (packagePath) => - JSON.parse( - fs.readFileSync(path.join(path.dirname(packagePath), "openclaw.plugin.json"), "utf-8"), - ) as PluginManifest, - ) - .toSorted((left, right) => left.id.localeCompare(right.id)); -} - describe("bundled capability metadata", () => { it("lists bundled extension packages from git without scanning extension dirs", () => { const extensionsDir = path.join(repoRoot, "extensions"); @@ -73,69 +44,4 @@ describe("bundled capability metadata", () => { expect(packagePaths.every((file) => file.endsWith("package.json"))).toBe(true); }); }); - - it("keeps contract snapshots aligned with bundled plugin manifests", () => { - const expected = readManifestRecords() - .map(buildBundledPluginContractSnapshot) - .filter(hasBundledPluginContractSnapshotCapabilities) - .toSorted((left, right) => left.pluginId.localeCompare(right.pluginId)); - - expect(BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS).toEqual(expected); - expect( - BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.find((entry) => entry.pluginId === "migrate-hermes"), - ).toEqual({ - pluginId: "migrate-hermes", - cliBackendIds: [], - providerIds: [], - providerEnvVars: {}, - workerProviderIds: [], - embeddingProviderIds: [], - speechProviderIds: [], - realtimeTranscriptionProviderIds: [], - realtimeVoiceProviderIds: [], - mediaUnderstandingProviderIds: [], - transcriptSourceProviderIds: [], - documentExtractorIds: [], - imageGenerationProviderIds: [], - videoGenerationProviderIds: [], - musicGenerationProviderIds: [], - webContentExtractorIds: [], - webFetchProviderIds: [], - webSearchProviderIds: [], - migrationProviderIds: ["hermes"], - toolNames: [], - }); - }); - - it("keeps lightweight alias maps aligned with bundled plugin manifests", () => { - const manifests = readManifestRecords(); - const expectedLegacyAliases = Object.fromEntries( - manifests - .flatMap((manifest) => - (manifest.legacyPluginIds ?? []).map((legacyPluginId) => [legacyPluginId, manifest.id]), - ) - .toSorted(([left], [right]) => - expectDefined(left, "left test invariant").localeCompare( - expectDefined(right, "right test invariant"), - ), - ), - ); - const expectedAutoEnableProviderPluginIds = Object.fromEntries( - manifests - .flatMap((manifest) => - (manifest.autoEnableWhenConfiguredProviders ?? []).map((providerId) => [ - providerId, - manifest.id, - ]), - ) - .toSorted(([left], [right]) => - expectDefined(left, "left test invariant").localeCompare( - expectDefined(right, "right test invariant"), - ), - ), - ); - - expect(BUNDLED_LEGACY_PLUGIN_ID_ALIASES).toEqual(expectedLegacyAliases); - expect(BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS).toEqual(expectedAutoEnableProviderPluginIds); - }); }); diff --git a/src/plugins/bundled-capability-runtime.test.ts b/src/plugins/bundled-capability-runtime.test.ts deleted file mode 100644 index f946397bc100..000000000000 --- a/src/plugins/bundled-capability-runtime.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Verifies bundled capability runtime registration from plugin metadata. -import { describe, expect, it } from "vitest"; -import { buildVitestCapabilityShimAliasMap } from "./bundled-capability-runtime.js"; - -describe("buildVitestCapabilityShimAliasMap", () => { - it("keeps scoped and unscoped capability shim aliases aligned", () => { - const aliasMap = buildVitestCapabilityShimAliasMap(); - - expect(aliasMap["openclaw/plugin-sdk/config-runtime"]).toBe( - aliasMap["@openclaw/plugin-sdk/config-runtime"], - ); - expect(aliasMap["openclaw/plugin-sdk/media-runtime"]).toBe( - aliasMap["@openclaw/plugin-sdk/media-runtime"], - ); - expect(aliasMap["openclaw/plugin-sdk/provider-onboard"]).toBe( - aliasMap["@openclaw/plugin-sdk/provider-onboard"], - ); - expect(aliasMap["openclaw/plugin-sdk/speech-core"]).toBe( - aliasMap["@openclaw/plugin-sdk/speech-core"], - ); - }); -}); diff --git a/src/plugins/bundled-capability-runtime.ts b/src/plugins/bundled-capability-runtime.ts index 50170c87b34c..d02bdb4b8081 100644 --- a/src/plugins/bundled-capability-runtime.ts +++ b/src/plugins/bundled-capability-runtime.ts @@ -53,7 +53,7 @@ const CAPABILITY_VITEST_SHIM_ALIASES = [ }, ] as const; -export function buildVitestCapabilityShimAliasMap(): Record { +function buildVitestCapabilityShimAliasMap(): Record { return Object.fromEntries( CAPABILITY_VITEST_SHIM_ALIASES.flatMap(({ subpath, target }) => { const targetPath = fileURLToPath(target); diff --git a/src/plugins/bundled-channel-runtime.test.ts b/src/plugins/bundled-channel-runtime.test.ts index 3200322dd123..c04a6af1e077 100644 --- a/src/plugins/bundled-channel-runtime.test.ts +++ b/src/plugins/bundled-channel-runtime.test.ts @@ -6,7 +6,6 @@ import { afterEach, describe, expect, it } from "vitest"; import { listBundledChannelPluginMetadata, resolveBundledChannelGeneratedPath, - resolveBundledChannelWorkspacePath, } from "./bundled-channel-runtime.js"; const tempRoots: string[] = []; @@ -24,15 +23,6 @@ afterEach(() => { }); describe("bundled channel runtime metadata", () => { - it("preserves explicit empty bundled roots", () => { - const tempRoot = createTempRoot(); - - expect(listBundledChannelPluginMetadata({ rootDir: tempRoot })).toStrictEqual([]); - expect(resolveBundledChannelWorkspacePath({ rootDir: tempRoot, pluginId: "telegram" })).toBe( - null, - ); - }); - it("preserves explicit missing bundled scan roots", () => { const tempRoot = createTempRoot(); const missingScanDir = path.join(tempRoot, "missing-extensions"); diff --git a/src/plugins/bundled-channel-runtime.ts b/src/plugins/bundled-channel-runtime.ts index dfac7b4eb288..0c29692805e8 100644 --- a/src/plugins/bundled-channel-runtime.ts +++ b/src/plugins/bundled-channel-runtime.ts @@ -120,19 +120,3 @@ export function resolveBundledChannelGeneratedPath( ): string | null { return resolveBundledPluginGeneratedPath(rootDir, entry, pluginDirName, scanDir); } - -/** Resolves the source workspace path for a bundled channel plugin id. */ -export function resolveBundledChannelWorkspacePath(params: { - rootDir: string; - scanDir?: string; - pluginId: string; -}): string | null { - return ( - listBundledChannelPluginMetadata({ - rootDir: params.rootDir, - ...(params.scanDir ? { scanDir: params.scanDir } : {}), - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, - }).find((metadata) => metadata.manifest.id === params.pluginId)?.rootDir ?? null - ); -} diff --git a/src/plugins/bundled-dir.ts b/src/plugins/bundled-dir.ts index c2d0c62e614c..bdee865721e4 100644 --- a/src/plugins/bundled-dir.ts +++ b/src/plugins/bundled-dir.ts @@ -11,11 +11,10 @@ import { resolveUserPath } from "../utils.js"; const DISABLED_BUNDLED_PLUGINS_DIR = path.join(os.tmpdir(), "openclaw-empty-bundled-plugins"); const TEST_TRUST_BUNDLED_PLUGINS_DIR_ENV = "OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR"; -let bundledPluginsDirOverrideForTest: string | undefined; const bundledPluginsDirCache = new Map(); /** Diagnostic emitted when source-checkout bundled plugins lack dependency installs. */ -export type SourceCheckoutDependencyDiagnostic = { +type SourceCheckoutDependencyDiagnostic = { source: string; message: string; }; @@ -210,7 +209,6 @@ function createBundledPluginsDirCacheKey(env: NodeJS.ProcessEnv): string { openClawHome: env.OPENCLAW_HOME ?? "", home: env.HOME ?? "", userProfile: env.USERPROFILE ?? "", - testOverride: bundledPluginsDirOverrideForTest ?? "", }); } @@ -219,10 +217,6 @@ function resolveBundledPluginsDirUncached(env: NodeJS.ProcessEnv): string | unde return resolveDisabledBundledPluginsDir(); } - if (bundledPluginsDirOverrideForTest) { - return bundledPluginsDirOverrideForTest; - } - const override = env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim(); let rejectedExistingOverride: string | null = null; if (override) { @@ -309,11 +303,3 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env): bundledPluginsDirCache.set(cacheKey, resolved); return resolved; } - -export function setBundledPluginsDirOverrideForTest(dir: string | undefined): void { - if (process.env.VITEST !== "true" && process.env.NODE_ENV !== "test") { - throw new Error("setBundledPluginsDirOverrideForTest is only available in tests"); - } - bundledPluginsDirOverrideForTest = dir; - bundledPluginsDirCache.clear(); -} diff --git a/src/plugins/bundled-load-path-aliases.ts b/src/plugins/bundled-load-path-aliases.ts index 13de0741f79b..511e592f9d65 100644 --- a/src/plugins/bundled-load-path-aliases.ts +++ b/src/plugins/bundled-load-path-aliases.ts @@ -3,23 +3,23 @@ import path from "node:path"; import { isPathInside } from "./path-safety.js"; /** Alias class for current packaged paths and legacy bundled extension paths. */ -export type BundledPluginLoadPathAliasKind = "current" | "legacy"; +type BundledPluginLoadPathAliasKind = "current" | "legacy"; /** Load path alias used while resolving bundled plugins across package layouts. */ -export type BundledPluginLoadPathAlias = { +type BundledPluginLoadPathAlias = { kind: BundledPluginLoadPathAliasKind; path: string; }; /** Parsed path metadata for a bundled plugin in a packaged dist root. */ -export type PackagedBundledPluginPath = { +type PackagedBundledPluginPath = { packageRoot: string; bundledRoot: string; bundledLeaf: string; }; /** Parsed path metadata for a bundled plugin in the legacy extensions root. */ -export type LegacyBundledPluginPath = { +type LegacyBundledPluginPath = { packageRoot: string; legacyRoot: string; bundledLeaf: string; diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 6d2a05371bbc..f4cac2913bc0 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -7,11 +7,12 @@ import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js"; import { listGitTrackedFiles, toRepoRelativePath } from "../test-utils/repo-files.js"; import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.js"; import { - type BundledPluginMetadata, listBundledPluginMetadata, resolveBundledPluginGeneratedPath, resolveBundledPluginRepoEntryPath, } from "./bundled-plugin-metadata.js"; + +type BundledPluginMetadata = ReturnType[number]; import { resolveGatewayStartupPluginIdsFromRegistry } from "./gateway-startup-plugin-ids.js"; import { createGeneratedPluginTempRoot, diff --git a/src/plugins/bundled-plugin-metadata.ts b/src/plugins/bundled-plugin-metadata.ts index 1a168ae40f9e..3bcd3153bbab 100644 --- a/src/plugins/bundled-plugin-metadata.ts +++ b/src/plugins/bundled-plugin-metadata.ts @@ -39,7 +39,7 @@ type BundledPluginPathPair = { }; /** Metadata collected from a bundled plugin package and manifest. */ -export type BundledPluginMetadata = { +type BundledPluginMetadata = { dirName: string; idHint: string; source: BundledPluginPathPair; diff --git a/src/plugins/bundled-plugin-startup-metadata.ts b/src/plugins/bundled-plugin-startup-metadata.ts index 3960fc10e82b..64eea186ed35 100644 --- a/src/plugins/bundled-plugin-startup-metadata.ts +++ b/src/plugins/bundled-plugin-startup-metadata.ts @@ -8,7 +8,7 @@ import { resolveBundledPluginsDir } from "./bundled-dir.js"; const DOCTOR_CONTRACT_BASENAMES = ["doctor-contract-api", "contract-api"] as const; const MODULE_EXTENSIONS = ["js", "cjs", "mjs", "ts", "cts", "mts"] as const; -export type BundledPluginStartupMetadata = { +type BundledPluginStartupMetadata = { hasDoctorContract: boolean; }; diff --git a/src/plugins/bundled-sources.ts b/src/plugins/bundled-sources.ts index 1e77637a14e1..a8a7c8197bfc 100644 --- a/src/plugins/bundled-sources.ts +++ b/src/plugins/bundled-sources.ts @@ -13,9 +13,7 @@ export type BundledPluginSource = { requiresConfig?: boolean; }; -export type BundledPluginLookup = - | { kind: "npmSpec"; value: string } - | { kind: "pluginId"; value: string }; +type BundledPluginLookup = { kind: "npmSpec"; value: string } | { kind: "pluginId"; value: string }; export function findBundledPluginSourceInMap(params: { bundled: ReadonlyMap; diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index ade898eebc94..14793c21ea04 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -60,8 +60,7 @@ vi.mock("../infra/archive.js", async () => { const { ClawHubRequestError } = await import("../infra/clawhub.js"); type ClawHubResolvedArtifact = import("../infra/clawhub.js").ClawHubResolvedArtifact; type ClawHubRiskAcknowledgementRequest = import("./clawhub.js").ClawHubRiskAcknowledgementRequest; -const { CLAWHUB_INSTALL_ERROR_CODE, formatClawHubSpecifier, installPluginFromClawHub } = - await import("./clawhub.js"); +const { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } = await import("./clawhub.js"); const DEMO_ARCHIVE_INTEGRITY = "sha256-qerEjGEpvES2+Tyan0j2xwDRkbcnmh4ZFfKN9vWbsa8="; const DEMO_ARCHIVE_SHA256 = "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af"; @@ -370,11 +369,6 @@ describe("installPluginFromClawHub", () => { }); }); - it("formats clawhub specifiers", () => { - expect(formatClawHubSpecifier({ name: "demo" })).toBe("clawhub:demo"); - expect(formatClawHubSpecifier({ name: "demo", version: "1.2.3" })).toBe("clawhub:demo@1.2.3"); - }); - it("installs a ClawHub plugin through the archive installer", async () => { const logger = createLoggerSpies(); const result = await installPluginFromClawHub({ diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index 5c795005a769..b253edfcbfaa 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -57,7 +57,7 @@ import { } from "./install.js"; export { CLAWHUB_INSTALL_ERROR_CODE }; -export type { ClawHubInstallErrorCode, ClawHubRiskAcknowledgementRequest }; +export type { ClawHubRiskAcknowledgementRequest }; type PluginInstallLogger = { info?: (message: string) => void; @@ -331,7 +331,7 @@ function resolveTopLevelLegacyArchiveVerification( return integrity ? { kind: "archive-integrity", integrity } : null; } -export function formatClawHubSpecifier(params: { name: string; version?: string }): string { +function formatClawHubSpecifier(params: { name: string; version?: string }): string { return `clawhub:${params.name}${params.version ? `@${params.version}` : ""}`; } diff --git a/src/plugins/cli-backend.types.ts b/src/plugins/cli-backend.types.ts index 9404e4713879..048ad63f015a 100644 --- a/src/plugins/cli-backend.types.ts +++ b/src/plugins/cli-backend.types.ts @@ -54,7 +54,7 @@ export type CliBackendThinkingLevel = export type CliBackendExecutionMode = "agent" | "side-question"; /** Host-isolated tool grant for a CLI run with every native tool disabled. */ -export type CliBackendToolAvailability = { +type CliBackendToolAvailability = { native: readonly []; /** MCP tools already isolated by the host transport that may be auto-approved. */ mcp: readonly string[]; diff --git a/src/plugins/cli-backends.runtime.ts b/src/plugins/cli-backends.runtime.ts index c62d6e9c7d6f..ef0a5e1e4fbc 100644 --- a/src/plugins/cli-backends.runtime.ts +++ b/src/plugins/cli-backends.runtime.ts @@ -3,7 +3,7 @@ import { getActiveRuntimePluginRegistry } from "./active-runtime-registry.js"; import type { CliBackendPlugin } from "./cli-backend.types.js"; /** Runtime CLI backend registration with owning plugin id. */ -export type PluginCliBackendEntry = CliBackendPlugin & { +type PluginCliBackendEntry = CliBackendPlugin & { pluginId: string; }; diff --git a/src/plugins/cli-gateway-nodes-runtime.test.ts b/src/plugins/cli-gateway-nodes-runtime.test.ts index e8d65373233c..006800b0aedc 100644 --- a/src/plugins/cli-gateway-nodes-runtime.test.ts +++ b/src/plugins/cli-gateway-nodes-runtime.test.ts @@ -1,10 +1,7 @@ /** Tests plugin CLI node Gateway runtime timeout and invocation behavior. */ import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - createPluginCliGatewayNodesRuntime, - resolvePluginCliNodeInvokeGatewayTimeoutMs, -} from "./cli-gateway-nodes-runtime.js"; +import { createPluginCliGatewayNodesRuntime } from "./cli-gateway-nodes-runtime.js"; import { withPluginRuntimePluginScope } from "./runtime/gateway-request-scope.js"; const callGatewayMock = vi.fn(); @@ -76,15 +73,3 @@ describe("createPluginCliGatewayNodesRuntime", () => { ); }); }); - -describe("resolvePluginCliNodeInvokeGatewayTimeoutMs", () => { - it("preserves absent and non-positive timeout behavior", () => { - expect(resolvePluginCliNodeInvokeGatewayTimeoutMs(undefined)).toBeUndefined(); - expect(resolvePluginCliNodeInvokeGatewayTimeoutMs(0)).toBeUndefined(); - expect(resolvePluginCliNodeInvokeGatewayTimeoutMs(-1)).toBeUndefined(); - }); - - it("adds gateway grace for normal positive timeouts", () => { - expect(resolvePluginCliNodeInvokeGatewayTimeoutMs(10_000)).toBe(15_000); - }); -}); diff --git a/src/plugins/cli-gateway-nodes-runtime.ts b/src/plugins/cli-gateway-nodes-runtime.ts index df0ad889048d..c21ee8ed6a6c 100644 --- a/src/plugins/cli-gateway-nodes-runtime.ts +++ b/src/plugins/cli-gateway-nodes-runtime.ts @@ -11,7 +11,7 @@ import { getPluginRuntimeGatewayRequestScope } from "./runtime/gateway-request-s import type { PluginRuntime } from "./runtime/types.js"; /** Adds Gateway timer grace for plugin CLI node invoke calls. */ -export function resolvePluginCliNodeInvokeGatewayTimeoutMs( +function resolvePluginCliNodeInvokeGatewayTimeoutMs( timeoutMs: number | undefined, ): number | undefined { return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 diff --git a/src/plugins/cli.browser-plugin.integration.test.ts b/src/plugins/cli.browser-plugin.integration.test.ts index 96d2246aa589..1e2d903a95f0 100644 --- a/src/plugins/cli.browser-plugin.integration.test.ts +++ b/src/plugins/cli.browser-plugin.integration.test.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createBundledBrowserPluginFixture } from "../../test/helpers/browser-bundled-plugin-fixture.js"; import type { OpenClawConfig } from "../config/config.js"; -import { clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.test-fixtures.js"; import { resetPluginRuntimeStateForTest } from "./runtime.js"; function resetPluginState() { diff --git a/src/plugins/command-registration.ts b/src/plugins/command-registration.ts index f9d21199e85e..a9fc537e57cd 100644 --- a/src/plugins/command-registration.ts +++ b/src/plugins/command-registration.ts @@ -12,7 +12,6 @@ import { clearPluginCommandsForPlugin, isPluginCommandRegistryLocked, pluginCommands, - type RegisteredPluginCommand, } from "./command-registry-state.js"; import { AGENT_PROMPT_SURFACE_KINDS, @@ -79,7 +78,7 @@ function getAgentPromptSurfaces(): Set { } /** Result returned when a plugin command registration succeeds or fails validation. */ -export type CommandRegistrationResult = { +type CommandRegistrationResult = { ok: boolean; error?: string; }; @@ -388,4 +387,3 @@ export function registerPluginCommand( } export { clearPluginCommands, clearPluginCommandsForPlugin }; -export type { RegisteredPluginCommand }; diff --git a/src/plugins/command-specs.ts b/src/plugins/command-specs.ts index b0f48c99ba6c..e5ee42a93694 100644 --- a/src/plugins/command-specs.ts +++ b/src/plugins/command-specs.ts @@ -15,7 +15,7 @@ type PluginCommandSpecOptions = { config?: OpenClawConfig; }; -export type PluginCommandEntrySpec = { +type PluginCommandEntrySpec = { name: string; description: string; acceptsArgs: boolean; diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index 700b35dd8e16..adc354aecd98 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -16,7 +16,7 @@ import { import { createPluginRegistry } from "./registry.js"; import { setActivePluginRegistry } from "./runtime.js"; import type { PluginRuntime } from "./runtime/types.js"; -import { createBundledPluginRecord } from "./status.test-helpers.js"; +import { createBundledPluginRecord } from "./status.test-fixtures.js"; const completionMocks = vi.hoisted(() => ({ prepareSimpleCompletionModelForAgent: vi.fn(), diff --git a/src/plugins/compaction-provider.ts b/src/plugins/compaction-provider.ts index ce19c524b87b..73b7c141edf3 100644 --- a/src/plugins/compaction-provider.ts +++ b/src/plugins/compaction-provider.ts @@ -14,7 +14,7 @@ * A pluggable compaction provider that can replace the built-in * summarizeInStages pipeline. */ -export type CompactionProviderSummarizationInstructions = { +type CompactionProviderSummarizationInstructions = { identifierPolicy?: "strict" | "off" | "custom"; identifierInstructions?: string; }; @@ -38,7 +38,7 @@ export interface CompactionProvider { // --------------------------------------------------------------------------- /** A compaction provider with its owning plugin id for lifecycle tracking. */ -export type RegisteredCompactionProvider = { +type RegisteredCompactionProvider = { provider: CompactionProvider; ownerPluginId?: string; }; diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index e97dfeb68416..ff69c8007e50 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -2,12 +2,7 @@ import fs from "node:fs"; import { beforeAll, describe, expect, it } from "vitest"; import { listGitTrackedFiles } from "../../test-utils/repo-files.js"; -import { - getPluginCompatRecord, - isPluginCompatCode, - listDeprecatedPluginCompatRecords, - listPluginCompatRecords, -} from "./registry.js"; +import { listPluginCompatRecords } from "./registry.js"; const datePattern = /^\d{4}-\d{2}-\d{2}$/u; const sourceRootsForDeprecatedCallGuard = [ @@ -28,224 +23,6 @@ const deprecatedTargetParserCompatFiles = new Set([ "src/plugins/compat/registry.test.ts", ]); -const knownDeprecatedSurfaceMarkers = [ - { - code: "legacy-extension-api-import", - file: "src/extensionAPI.ts", - marker: "openclaw/extension-api is deprecated", - }, - { - code: "memory-split-registration", - file: "src/plugins/memory-state.ts", - marker: "registerMemoryPromptSection", - }, - { - code: "provider-static-capabilities-bag", - file: "src/plugins/types.ts", - marker: "Legacy static provider capability bag", - }, - { - code: "provider-discovery-type-aliases", - file: "src/plugins/types.ts", - marker: "ProviderPluginDiscovery = ProviderPluginCatalog", - }, - { - code: "provider-thinking-policy-hooks", - file: "src/plugins/types.ts", - marker: "Prefer `resolveThinkingProfile`", - }, - { - code: "provider-external-oauth-profiles-hook", - file: "src/plugins/types.ts", - marker: "resolveExternalOAuthProfiles", - }, - { - code: "agent-tool-result-harness-alias", - file: "src/plugins/agent-tool-result-middleware-types.ts", - marker: "AgentToolResultMiddlewareHarness", - }, - { - code: "embedded-pi-agent-sdk-aliases", - file: "src/plugins/runtime/types-core.ts", - marker: "runEmbeddedPiAgent", - }, - { - code: "runtime-config-load-write", - file: "src/plugins/runtime/runtime-config.ts", - marker: "RUNTIME_CONFIG_LOAD_WRITE_COMPAT_CODE", - }, - { - code: "runtime-taskflow-legacy-alias", - file: "src/plugins/runtime/types-core.ts", - marker: "taskFlow", - }, - { - code: "runtime-subagent-get-session-alias", - file: "src/plugins/runtime/types.ts", - marker: "getSessionMessages", - }, - { - code: "runtime-stt-alias", - file: "src/plugins/runtime/types-core.ts", - marker: "stt", - }, - { - code: "runtime-inbound-envelope-alias", - file: "src/plugins/runtime/types-channel.ts", - marker: "formatInboundEnvelope", - }, - { - code: "channel-native-message-schema-helpers", - file: "src/plugin-sdk/channel-actions.ts", - marker: "createMessageToolButtonsSchema", - }, - { - code: "channel-mention-gating-legacy-helpers", - file: "src/plugin-sdk/channel-inbound.ts", - marker: "resolveMentionGatingWithBypass", - }, - { - code: "provider-web-search-core-wrapper", - file: "src/plugin-sdk/provider-web-search.ts", - marker: "createPluginBackedWebSearchProvider", - }, - { - code: "approval-capability-approvals-alias", - file: "src/plugin-sdk/approval-delivery-helpers.ts", - marker: "approvals?: Partial", - }, - { - code: "plugin-sdk-test-utils-alias", - file: "src/plugin-sdk/test-utils.ts", - marker: "focused `openclaw/plugin-sdk/*` test subpaths", - }, - { - code: "plugin-install-config-ledger", - file: "src/config/plugin-install-config-migration.ts", - marker: "stripShippedPluginInstallConfigRecords", - }, - { - code: "bundled-plugin-load-path-aliases", - file: "src/commands/doctor/shared/bundled-plugin-load-paths.ts", - marker: "plugins.load.paths", - }, - { - code: "plugin-owned-web-search-config", - file: "src/commands/doctor/shared/legacy-web-search-migrate.ts", - marker: "tools.web.search", - }, - { - code: "plugin-owned-web-fetch-config", - file: "src/commands/doctor/shared/legacy-web-fetch-migrate.ts", - marker: "tools.web.fetch.firecrawl", - }, - { - code: "plugin-owned-x-search-config", - file: "src/commands/doctor/shared/legacy-x-search-migrate.ts", - marker: "tools.web.x_search", - }, - { - code: "bundled-channel-config-schema-legacy", - file: "src/plugin-sdk/channel-config-schema-legacy.ts", - marker: "Compatibility surface for bundled channel schemas", - }, - { - code: "plugin-sdk-testing-barrel", - file: "src/plugin-sdk/testing.ts", - marker: "@deprecated Broad compatibility barrel", - }, - { - code: "legacy-root-sdk-import", - file: "src/plugin-sdk/compat.ts", - marker: "@deprecated Use `openclaw/plugin-sdk/channel-outbound`.", - }, - { - code: "legacy-deactivate-hook-alias", - file: "src/plugins/hook-types.ts", - marker: "@deprecated Use gateway_stop", - }, - { - code: "legacy-subagent-spawning-hook", - file: "src/plugins/hook-types.ts", - marker: "@deprecated Core prepares thread-bound subagent bindings", - }, - { - code: "deprecated-memory-embedding-provider-api", - file: "src/plugins/types.ts", - marker: "registerMemoryEmbeddingProvider", - }, - { - code: "deprecated-session-store-beta5-api", - file: "src/plugin-sdk/session-store-runtime.ts", - marker: "@deprecated Use getSessionEntry or listSessionEntries", - }, - { - code: "deprecated-session-store-beta5-api", - file: "src/plugin-sdk/session-store-runtime.ts", - marker: "@deprecated Use patchSessionEntry, upsertSessionEntry, or deleteSessionEntry", - }, - { - code: "channel-route-key-aliases", - file: "src/plugin-sdk/channel-route.ts", - marker: "channelRouteIdentityKey", - }, - { - code: "channel-target-comparable-aliases", - file: "src/channels/plugins/target-parsing-loaded.ts", - marker: "ComparableChannelTarget", - }, - { - code: "channel-explicit-target-parser", - file: "src/channels/plugins/types.core.ts", - marker: "parseExplicitTarget?:", - }, - { - code: "channel-explicit-target-parser", - file: "src/plugin-sdk/channel-route.ts", - marker: "resolveChannelRouteTargetWithParser", - }, - { - code: "channel-explicit-target-parser", - file: "src/channels/plugins/target-parsing-loaded.ts", - marker: "ParsedChannelExplicitTarget", - }, - { - code: "channel-explicit-target-parser", - file: "src/channels/plugins/target-parsing-loaded.ts", - marker: "parseExplicitTargetForLoadedChannel", - }, - { - code: "channel-explicit-target-parser", - file: "src/channels/plugins/target-parsing-loaded.ts", - marker: "resolveRouteTargetForLoadedChannel", - }, - { - code: "channel-messaging-targets-subpath", - file: "src/plugin-sdk/messaging-targets.ts", - marker: "openclaw/plugin-sdk/channel-targets", - }, - { - code: "whatsapp-web-inbound-flat-message-aliases", - file: "extensions/whatsapp/src/inbound/types.ts", - marker: "DeprecatedWebInboundMessageFlatAliases", - }, - { - code: "whatsapp-web-inbound-admission-top-level-fields", - file: "extensions/whatsapp/src/inbound/types.ts", - marker: "DeprecatedWebInboundAdmissionTopLevelFields", - }, -] as const; - -function parseDate(date: string): Date { - return new Date(`${date}T00:00:00Z`); -} - -function addUtcMonths(date: Date, months: number): Date { - const next = new Date(date); - next.setUTCMonth(next.getUTCMonth() + months); - return next; -} - function expectNonEmptyStringList(values: readonly string[], label: string) { expect(values, label).toEqual([expect.stringMatching(/\S/u), ...values.slice(1)]); for (const value of values) { @@ -268,36 +45,13 @@ describe("plugin compatibility registry", () => { .filter((file) => deprecatedTargetParserCallPattern.test(fs.readFileSync(file, "utf8"))); }); - it("keeps compatibility codes unique and lookup-safe", () => { - const records = listPluginCompatRecords(); - const codes = records.map((record) => record.code); - - expect(new Set(codes).size).toBe(codes.length); - expect(isPluginCompatCode("legacy-root-sdk-import")).toBe(true); - expect(isPluginCompatCode("missing-code")).toBe(false); - expect(getPluginCompatRecord("legacy-root-sdk-import").owner).toBe("sdk"); - }); - - it("requires dated deprecation metadata for deprecated records", () => { - for (const record of listDeprecatedPluginCompatRecords()) { - expect(record.deprecated, record.code).toMatch(datePattern); - expect(record.warningStarts, record.code).toMatch(datePattern); - expect(record.removeAfter, record.code).toMatch(datePattern); - if (!record.warningStarts || !record.removeAfter) { - throw new Error(`${record.code} is missing deprecation window dates`); - } - const maxRemoveAfter = addUtcMonths(parseDate(record.warningStarts), 3); - const removeAfter = parseDate(record.removeAfter); - expect(removeAfter <= maxRemoveAfter, record.code).toBe(true); - expect(record.replacement, record.code).toMatch(/\S/u); - expect(record.docsPath, record.code).toMatch(/^\//u); - } - }); - it("keeps every record actionable", () => { for (const record of listPluginCompatRecords()) { expect(record.introduced, record.code).toMatch(datePattern); expect(record.docsPath, record.code).toMatch(/^\//u); + if (record.status === "deprecated") { + expect(record.replacement, record.code).toMatch(/\S/u); + } expectNonEmptyStringList(record.surfaces, `${record.code}: surfaces`); expectNonEmptyStringList(record.diagnostics, `${record.code}: diagnostics`); expectNonEmptyStringList(record.tests, `${record.code}: tests`); @@ -307,13 +61,6 @@ describe("plugin compatibility registry", () => { } }); - it("tracks known plugin-facing deprecated surfaces", () => { - for (const surface of knownDeprecatedSurfaceMarkers) { - expect(isPluginCompatCode(surface.code), surface.code).toBe(true); - expect(fs.readFileSync(surface.file, "utf8"), surface.file).toContain(surface.marker); - } - }); - it("keeps deprecated explicit target parser calls inside compatibility shims", () => { expect(deprecatedTargetParserOffenders).toEqual([]); }); diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index 616eb24041ab..93a74085d637 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -7,7 +7,7 @@ const LEGACY_CONFIG_MIGRATE_TEST_PATH = [ "migrate.test.ts", ].join("-"); -export const PLUGIN_COMPAT_RECORDS = [ +const PLUGIN_COMPAT_RECORDS = [ { code: "legacy-before-agent-start", status: "deprecated", @@ -1130,7 +1130,7 @@ export const PLUGIN_COMPAT_RECORDS = [ ] as const satisfies readonly PluginCompatRecord[]; export type PluginCompatCode = (typeof PLUGIN_COMPAT_RECORDS)[number]["code"]; -export type KnownPluginCompatRecord = PluginCompatRecord; +type KnownPluginCompatRecord = PluginCompatRecord; const pluginCompatRecordByCode = new Map( PLUGIN_COMPAT_RECORDS.map((record) => [record.code, record]), @@ -1147,13 +1147,3 @@ export function getPluginCompatRecord(code: PluginCompatCode): KnownPluginCompat } return record; } - -export function isPluginCompatCode(code: string): code is PluginCompatCode { - return pluginCompatRecordByCode.has(code as PluginCompatCode); -} - -export function listDeprecatedPluginCompatRecords(): readonly KnownPluginCompatRecord[] { - return PLUGIN_COMPAT_RECORDS.filter((record) => - (["deprecated", "removal-pending"] as readonly string[]).includes(record.status), - ); -} diff --git a/src/plugins/compat/types.ts b/src/plugins/compat/types.ts index 2be59f68dad2..bcb3849bffbb 100644 --- a/src/plugins/compat/types.ts +++ b/src/plugins/compat/types.ts @@ -1,7 +1,7 @@ // Plugin compatibility types describe lifecycle status for plugin migration and deprecation checks. -export type PluginCompatStatus = "active" | "deprecated" | "removal-pending" | "removed"; +type PluginCompatStatus = "active" | "deprecated" | "removal-pending" | "removed"; -export type PluginCompatOwner = +type PluginCompatOwner = | "agent-runtime" | "channel" | "config" diff --git a/src/plugins/config-activation-shared.ts b/src/plugins/config-activation-shared.ts index bfd59322ba2c..73a81d3bac87 100644 --- a/src/plugins/config-activation-shared.ts +++ b/src/plugins/config-activation-shared.ts @@ -8,14 +8,14 @@ type PluginKindLike = string | readonly string[] | undefined; export type PluginActivationSource = "disabled" | "explicit" | "auto" | "default"; -export type PluginExplicitSelectionCause = +type PluginExplicitSelectionCause = | "enabled-in-config" | "bundled-channel-enabled-in-config" | "selected-memory-slot" | "selected-context-engine-slot" | "selected-in-allowlist"; -export type PluginActivationCause = +type PluginActivationCause = | PluginExplicitSelectionCause | "plugins-disabled" | "blocked-by-denylist" @@ -35,7 +35,7 @@ export type PluginActivationStateLike = { reason?: string; }; -export type PluginActivationDecision = PluginActivationStateLike & { +type PluginActivationDecision = PluginActivationStateLike & { cause?: PluginActivationCause; }; diff --git a/src/plugins/config-contract-matches.ts b/src/plugins/config-contract-matches.ts index 5451a20a9548..7576b818b837 100644 --- a/src/plugins/config-contract-matches.ts +++ b/src/plugins/config-contract-matches.ts @@ -3,7 +3,7 @@ import { normalizeStringEntries } from "@openclaw/normalization-core/string-norm import { parseConfigPathArrayIndex } from "../shared/path-array-index.js"; import { isRecord } from "../utils.js"; -export type PluginConfigContractMatch = { +type PluginConfigContractMatch = { /** Concrete config path matched by the contract pattern. */ path: string; /** Config value stored at the matched path. */ diff --git a/src/plugins/config-contracts.ts b/src/plugins/config-contracts.ts index e5e06aee4c03..7ef7d2601799 100644 --- a/src/plugins/config-contracts.ts +++ b/src/plugins/config-contracts.ts @@ -7,12 +7,9 @@ import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginManifestConfigContracts } from "./manifest.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; -export { - collectPluginConfigContractMatches, - type PluginConfigContractMatch, -} from "./config-contract-matches.js"; +export { collectPluginConfigContractMatches } from "./config-contract-matches.js"; -export type PluginConfigContractMetadata = { +type PluginConfigContractMetadata = { /** Runtime origin that supplied the contract metadata. */ origin: PluginOrigin; /** Manifest-declared config contract paths used by secret/security/config scanners. */ diff --git a/src/plugins/config-policy.test.ts b/src/plugins/config-policy.test.ts index 0158cad28b94..3423f81fb58c 100644 --- a/src/plugins/config-policy.test.ts +++ b/src/plugins/config-policy.test.ts @@ -1,11 +1,6 @@ // Covers plugin config policy validation and ownership decisions. import { describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { - hasExplicitPluginConfig, - isBundledChannelEnabledByChannelConfig, - normalizePluginsConfigWithResolver, -} from "./config-policy.js"; +import { hasExplicitPluginConfig, normalizePluginsConfigWithResolver } from "./config-policy.js"; describe("normalizePluginsConfigWithResolver", () => { it("uses the provided plugin id resolver for allow deny and entry keys", () => { @@ -35,18 +30,3 @@ describe("hasExplicitPluginConfig", () => { expect(hasExplicitPluginConfig({})).toBe(false); }); }); - -describe("isBundledChannelEnabledByChannelConfig", () => { - it("only treats enabled channel entries as bundled plugin enablement", () => { - const cfg = { - channels: { - telegram: { enabled: true }, - slack: { enabled: false }, - }, - } as OpenClawConfig; - - expect(isBundledChannelEnabledByChannelConfig(cfg, "telegram")).toBe(true); - expect(isBundledChannelEnabledByChannelConfig(cfg, "slack")).toBe(false); - expect(isBundledChannelEnabledByChannelConfig(cfg, "not-a-channel")).toBe(false); - }); -}); diff --git a/src/plugins/config-policy.ts b/src/plugins/config-policy.ts index 65e5862aee93..2f0dbf690e57 100644 --- a/src/plugins/config-policy.ts +++ b/src/plugins/config-policy.ts @@ -4,7 +4,6 @@ import { resolveMemorySlotDecisionShared, resolvePluginActivationDecisionShared, toPluginActivationState, - type PluginActivationSource, type PluginActivationStateLike, } from "./config-activation-shared.js"; import { @@ -18,10 +17,9 @@ import { import type { PluginKind } from "./plugin-kind.types.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; -export type { PluginActivationSource }; -export type PluginActivationState = PluginActivationStateLike; +type PluginActivationState = PluginActivationStateLike; -export type NormalizedPluginsConfig = SharedNormalizedPluginsConfig; +type NormalizedPluginsConfig = SharedNormalizedPluginsConfig; export function normalizePluginsConfigWithResolver( config?: OpenClawConfig["plugins"], @@ -30,7 +28,7 @@ export function normalizePluginsConfigWithResolver( return normalizePluginsConfigWithResolverShared(config, normalizePluginId); } -export function resolvePluginActivationState(params: { +function resolvePluginActivationState(params: { id: string; origin: PluginOrigin; config: NormalizedPluginsConfig; @@ -53,7 +51,7 @@ export function resolvePluginActivationState(params: { } export const hasExplicitPluginConfig = hasExplicitPluginConfigShared; -export const isBundledChannelEnabledByChannelConfig = isBundledChannelEnabledByChannelConfigShared; +const isBundledChannelEnabledByChannelConfig = isBundledChannelEnabledByChannelConfigShared; type PolicyEffectiveActivationParams = { id: string; diff --git a/src/plugins/contracts/host-hooks.contract.test.ts b/src/plugins/contracts/host-hooks.contract.test.ts index 7ce422cf6caf..3090fa912091 100644 --- a/src/plugins/contracts/host-hooks.contract.test.ts +++ b/src/plugins/contracts/host-hooks.contract.test.ts @@ -13,7 +13,11 @@ import { validateSessionsPluginPatchParams, } from "../../../packages/gateway-protocol/src/index.js"; import type { SessionEntry } from "../../config/sessions.js"; -import { listSessionEntries, replaceSessionEntry } from "../../config/sessions/session-accessor.js"; +import { + clearPluginOwnedSessionState, + listSessionEntries, + replaceSessionEntry, +} from "../../config/sessions/session-accessor.js"; import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../../gateway/operator-scopes.js"; import { pluginHostHookHandlers } from "../../gateway/server-methods/plugin-host-hooks.js"; import { buildGatewaySessionRow } from "../../gateway/session-utils.js"; @@ -23,19 +27,15 @@ import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js" import { withEnvAsync } from "../../test-utils/env.js"; import { executePluginCommand, validatePluginCommandDefinition } from "../commands.js"; import { createHookRunner } from "../hooks.js"; -import { - cleanupReplacedPluginHostRegistry, - clearPluginOwnedSessionState, - runPluginHostCleanup, -} from "../host-hook-cleanup.js"; +import { cleanupReplacedPluginHostRegistry, runPluginHostCleanup } from "../host-hook-cleanup.js"; import { clearPluginHostRuntimeState, getPluginRunContext, - listPluginSessionSchedulerJobs, setPluginRunContext, } from "../host-hook-runtime.js"; +import { listPluginSessionSchedulerJobs } from "../host-hook-runtime.test-fixtures.js"; import { - drainPluginNextTurnInjections, + drainPluginNextTurnInjectionContext, enqueuePluginNextTurnInjection, patchPluginSessionExtension, projectPluginSessionExtensionsSync, @@ -1799,7 +1799,7 @@ describe("host-hook fixture plugin contract", () => { return undefined; }); - const drained = await drainPluginNextTurnInjections({ + const { queuedInjections: drained } = await drainPluginNextTurnInjectionContext({ cfg: tempConfig, sessionKey: "agent:main:main", now: 2, @@ -1877,7 +1877,7 @@ describe("host-hook fixture plugin contract", () => { return undefined; }); - const drained = await drainPluginNextTurnInjections({ + const { queuedInjections: drained } = await drainPluginNextTurnInjectionContext({ cfg: tempConfig, sessionKey: "agent:main:main", now: 4, diff --git a/src/plugins/contracts/inventory/bundled-capability-metadata.ts b/src/plugins/contracts/inventory/bundled-capability-metadata.ts index f4688b381f76..d1789df53acc 100644 --- a/src/plugins/contracts/inventory/bundled-capability-metadata.ts +++ b/src/plugins/contracts/inventory/bundled-capability-metadata.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { expectDefined } from "@openclaw/normalization-core"; import { tryReadJsonSync } from "../../../infra/json-files.js"; import { normalizeBundledPluginStringList, @@ -53,7 +52,7 @@ const RUNNING_FROM_BUILT_ARTIFACT = CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) || CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`); -export type BundledCapabilityManifest = Pick< +type BundledCapabilityManifest = Pick< PluginManifest, | "id" | "autoEnableWhenConfiguredProviders" @@ -122,7 +121,7 @@ function normalizeSetupProviderEnvVars(setup: PluginManifest["setup"]): Record left.pluginId.localeCompare(right.pluginId)); - -export const BUNDLED_LEGACY_PLUGIN_ID_ALIASES = Object.fromEntries( - BUNDLED_CAPABILITY_MANIFESTS.flatMap((manifest) => - (manifest.legacyPluginIds ?? []).map( - (legacyPluginId) => [legacyPluginId, manifest.id] as const, - ), - ).toSorted(([left], [right]) => left.localeCompare(right)), -) as Readonly>; - -export const BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS = Object.fromEntries( - BUNDLED_CAPABILITY_MANIFESTS.flatMap((manifest) => - (manifest.autoEnableWhenConfiguredProviders ?? []).map((providerId) => [ - providerId, - manifest.id, - ]), - ).toSorted(([left], [right]) => - expectDefined(left, "bundled capability metadata left").localeCompare( - expectDefined(right, "bundled capability metadata right"), - ), - ), -) as Readonly>; - -type BundledContractIdSnapshotKey = Exclude< - keyof Omit, - "providerEnvVars" ->; - -export function resolveBundledContractSnapshotPluginIds( - key: BundledContractIdSnapshotKey, -): string[] { - return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter((entry) => entry[key].length > 0) - .map((entry) => entry.pluginId) - .toSorted((left, right) => left.localeCompare(right)); -} diff --git a/src/plugins/contracts/loader.contract.test.ts b/src/plugins/contracts/loader.contract.test.ts deleted file mode 100644 index a92f462536ca..000000000000 --- a/src/plugins/contracts/loader.contract.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Loader contract tests cover plugin loader behavior, registry setup, and reset boundaries. -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { uniqueSortedStrings } from "../../plugin-sdk/test-helpers/string-utils.js"; -import { resolveManifestContractPluginIds } from "../plugin-registry.js"; -import { testing as providerTesting } from "../providers.js"; -import { resolveBundledContractSnapshotPluginIds } from "./inventory/bundled-capability-metadata.js"; -import { providerContractPluginIds } from "./registry.js"; - -function resolveBundledManifestProviderPluginIds() { - return uniqueSortedStrings(resolveBundledContractSnapshotPluginIds("providerIds")); -} - -const ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_IDS = ["codex", "qa-lab"] as const; -const ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_ID_SET = new Set( - ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_IDS, -); - -function expectPluginAllowlistEquals( - allow: string[] | undefined, - pluginIds: string[], - expectedExtraEntry?: string, -) { - expect(allow).toEqual(expectedExtraEntry ? [expectedExtraEntry, ...pluginIds] : pluginIds); -} - -describe("plugin loader contract", () => { - let providerPluginIds: string[] = []; - let manifestProviderPluginIds: string[] = []; - let vitestCompatConfig: ReturnType; - let webSearchPluginIds: string[] = []; - let bundledWebSearchPluginIds: string[] = []; - - beforeAll(() => { - providerPluginIds = uniqueSortedStrings(providerContractPluginIds); - manifestProviderPluginIds = resolveBundledManifestProviderPluginIds(); - vitestCompatConfig = providerTesting.withBundledProviderVitestCompat({ - config: undefined, - pluginIds: providerPluginIds, - env: { VITEST: "1" } as NodeJS.ProcessEnv, - }); - webSearchPluginIds = uniqueSortedStrings( - resolveBundledContractSnapshotPluginIds("webSearchProviderIds"), - ); - bundledWebSearchPluginIds = uniqueSortedStrings( - resolveManifestContractPluginIds({ - contract: "webSearchProviders", - origin: "bundled", - }), - ); - }); - - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("keeps bundled provider registry wired to the manifest inventory", () => { - expect(providerPluginIds).toEqual(manifestProviderPluginIds); - }); - - it("keeps vitest bundled provider enablement wired to the provider registry", () => { - expect(providerPluginIds).toEqual(manifestProviderPluginIds); - expect(vitestCompatConfig?.plugins?.enabled).toBe(true); - expectPluginAllowlistEquals(vitestCompatConfig?.plugins?.allow, providerPluginIds); - }); - - it("keeps bundled web search loading scoped to the web search registry", () => { - const loaderScopedPluginIds = bundledWebSearchPluginIds.filter( - (pluginId) => !ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_ID_SET.has(pluginId), - ); - const expectedPluginIds = uniqueSortedStrings([ - ...loaderScopedPluginIds, - ...ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_IDS, - ]); - - expect(webSearchPluginIds).toEqual(expectedPluginIds); - expect( - webSearchPluginIds.filter((pluginId) => !loaderScopedPluginIds.includes(pluginId)), - ).toEqual([...ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_IDS]); - }); -}); diff --git a/src/plugins/contracts/memory-embedding-provider.contract.test.ts b/src/plugins/contracts/memory-embedding-provider.contract.test.ts index 50356382e70f..3cfdd3b4ce48 100644 --- a/src/plugins/contracts/memory-embedding-provider.contract.test.ts +++ b/src/plugins/contracts/memory-embedding-provider.contract.test.ts @@ -8,7 +8,7 @@ import { getRegisteredMemoryEmbeddingProvider, type MemoryEmbeddingBatchOptions, } from "../memory-embedding-providers.js"; -import { createPluginRecord } from "../status.test-helpers.js"; +import { createPluginRecord } from "../status.test-fixtures.js"; describe("memory embedding provider registration", () => { it("rejects non-memory plugins that did not declare the capability contract", () => { diff --git a/src/plugins/contracts/registry.contract.test.ts b/src/plugins/contracts/registry.contract.test.ts index f748772ea9ea..3c4317c3d129 100644 --- a/src/plugins/contracts/registry.contract.test.ts +++ b/src/plugins/contracts/registry.contract.test.ts @@ -4,11 +4,7 @@ import { uniqueSortedStrings } from "../../plugin-sdk/test-helpers/string-utils. import { loadPluginManifestRegistry, type PluginManifestRecord } from "../manifest-registry.js"; import { resolveManifestContractPluginIds } from "../plugin-registry.js"; import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./inventory/bundled-capability-metadata.js"; -import { - pluginRegistrationContractRegistry, - providerContractLoadError, - providerContractPluginIds, -} from "./registry.js"; +import { pluginRegistrationContractRegistry, providerContractLoadError } from "./registry.js"; const ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_IDS = ["codex", "qa-lab"] as const; const ACTIVATION_SCOPED_WEB_SEARCH_PLUGIN_ID_SET = new Set( @@ -133,13 +129,6 @@ describe("plugin contract registry", () => { expectUniqueIds(pluginRegistrationContractRegistry.flatMap((entry) => entry.speechProviderIds)); }); - it("covers every bundled provider plugin discovered from manifests", () => { - expectRegistryPluginIds({ - actualPluginIds: providerContractPluginIds, - predicate: (plugin) => plugin.origin === "bundled" && plugin.providers.length > 0, - }); - }); - it("covers every bundled worker provider plugin discovered from manifests", () => { expectRegistryPluginIds({ actualPluginIds: pluginRegistrationContractRegistry diff --git a/src/plugins/contracts/registry.retry.test.ts b/src/plugins/contracts/registry.retry.test.ts index bf16e8d1f219..387fbee962c2 100644 --- a/src/plugins/contracts/registry.retry.test.ts +++ b/src/plugins/contracts/registry.retry.test.ts @@ -167,44 +167,6 @@ describe("plugin contract registry scoped retries", () => { expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(2); }); - it("reuses the single registered provider contract for paired manifest alias ids", async () => { - const loadBundledCapabilityRuntimeRegistry = vi.fn().mockReturnValue( - createMockRuntimeRegistry({ - plugin: { - id: "byteplus", - status: "loaded", - providerIds: ["byteplus"], - webFetchProviderIds: [], - webSearchProviderIds: [], - migrationProviderIds: [], - }, - providers: [ - { - pluginId: "byteplus", - provider: { - id: "byteplus", - label: "BytePlus", - docsPath: "/providers/byteplus", - auth: [], - } as ProviderPlugin, - }, - ], - }), - ); - - vi.doMock("../bundled-capability-runtime.js", () => ({ - loadBundledCapabilityRuntimeRegistry, - })); - vi.doMock("../provider-contract-public-artifacts.js", () => ({ - resolveBundledExplicitProviderContractsFromPublicArtifacts: () => null, - })); - - const { requireProviderContractProvider } = await import("./registry.js"); - - expect(requireProviderContractProvider("byteplus-plan").id).toBe("byteplus"); - expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(1); - }); - it("uses provider public artifacts before falling back to the bundled runtime registry", async () => { const loadBundledCapabilityRuntimeRegistry = vi.fn(() => { throw new Error("provider contract public artifact should not hit bundled runtime registry"); diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index d7006eee9c53..54027ae915bf 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -1,6 +1,5 @@ // Plugin contract registry assembles bundled plugin fixtures for shared contract tests. import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; -import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js"; import { discoverOpenClawPlugins } from "../discovery.js"; @@ -131,20 +130,6 @@ function resolveBundledManifestContracts(): PluginRegistrationContractEntry[] { })); } -function resolveBundledProviderContractPluginIdsByProviderId(): Map { - const result = new Map(); - for (const entry of resolveBundledManifestContracts()) { - for (const providerId of entry.providerIds) { - const existing = result.get(providerId) ?? []; - if (!existing.includes(entry.pluginId)) { - existing.push(entry.pluginId); - } - result.set(providerId, existing); - } - } - return result; -} - function resolveBundledProviderContractPluginIds(): string[] { return uniqueStrings( resolveBundledManifestContracts() @@ -263,10 +248,6 @@ function loadProviderContractEntriesForPluginId(pluginId: string): ProviderContr } } -function loadProviderContractPluginIds(): string[] { - return [...resolveBundledProviderContractPluginIds()]; -} - function resolveWebSearchCredentialValue(provider: WebSearchProviderPlugin): unknown { if (provider.requiresCredential === false) { return `${provider.id}-no-key-needed`; @@ -374,28 +355,6 @@ function createLazyArrayView(load: () => T[]): T[] { }, }); } - -export function requireProviderContractProvider(providerId: string): ProviderPlugin { - const pluginIds = resolveBundledProviderContractPluginIdsByProviderId().get(providerId) ?? []; - const entries = loadProviderContractEntriesForPluginIds(pluginIds); - const provider = entries.find((entry) => entry.provider.id === providerId)?.provider; - if (!provider) { - const pluginScopedProviders = [ - ...new Map(entries.map((entry) => [entry.provider.id, entry.provider])).values(), - ]; - if (pluginIds.length === 1 && pluginScopedProviders.length === 1) { - return expectDefined(pluginScopedProviders[0], "plugin scoped providers entry at 0"); - } - if (providerContractLoadError) { - throw new Error( - `provider contract entry missing for ${providerId}; bundled provider registry failed to load: ${providerContractLoadError.message}`, - ); - } - throw new Error(`provider contract entry missing for ${providerId}`); - } - return provider; -} - export function resolveProviderContractPluginIdsForProviderAlias( providerId: string, ): string[] | undefined { @@ -432,11 +391,6 @@ export function resolveProviderContractProvidersForPluginIds( ).values(), ]; } - -export const providerContractPluginIds: string[] = createLazyArrayView( - loadProviderContractPluginIds, -); - function loadPluginRegistrationContractRegistry(): PluginRegistrationContractEntry[] { return resolveBundledManifestContracts(); } diff --git a/src/plugins/contracts/run-context-lifecycle.contract.test.ts b/src/plugins/contracts/run-context-lifecycle.contract.test.ts index c761a350e9af..5a001d7d7779 100644 --- a/src/plugins/contracts/run-context-lifecycle.contract.test.ts +++ b/src/plugins/contracts/run-context-lifecycle.contract.test.ts @@ -14,12 +14,14 @@ import { runPluginHostCleanup } from "../host-hook-cleanup.js"; import { clearPluginHostRuntimeState, getPluginRunContext, - listPluginSessionSchedulerJobs, - PLUGIN_TERMINAL_EVENT_CLEANUP_WAIT_MS, dispatchPluginAgentEventSubscriptions, registerPluginSessionSchedulerJob, setPluginRunContext, } from "../host-hook-runtime.js"; +import { + listPluginSessionSchedulerJobs, + PLUGIN_TERMINAL_EVENT_CLEANUP_WAIT_MS, +} from "../host-hook-runtime.test-fixtures.js"; import { createEmptyPluginRegistry } from "../registry-empty.js"; import { setActivePluginRegistry } from "../runtime.js"; import { createPluginRecord } from "../status.test-helpers.js"; diff --git a/src/plugins/contracts/scheduled-turns.contract.test.ts b/src/plugins/contracts/scheduled-turns.contract.test.ts index 297a38f539e6..a3b5d63e61cc 100644 --- a/src/plugins/contracts/scheduled-turns.contract.test.ts +++ b/src/plugins/contracts/scheduled-turns.contract.test.ts @@ -16,15 +16,14 @@ import { cleanupReplacedPluginHostRegistry } from "../host-hook-cleanup.js"; import { clearPluginHostRuntimeState, cleanupPluginSessionSchedulerJobs, - listPluginSessionSchedulerJobs, } from "../host-hook-runtime.js"; +import { listPluginSessionSchedulerJobs } from "../host-hook-runtime.test-fixtures.js"; import { - buildPluginSchedulerCronName, schedulePluginSessionTurn, unschedulePluginSessionTurnsByTag, } from "../host-hook-scheduled-turns.js"; -import { clearPluginLoaderCache, loadOpenClawPlugins } from "../loader.js"; -import { makeTempDir, writePlugin } from "../loader.test-fixtures.js"; +import { loadOpenClawPlugins } from "../loader.js"; +import { clearPluginLoaderCache, makeTempDir, writePlugin } from "../loader.test-fixtures.js"; import { createEmptyPluginRegistry } from "../registry-empty.js"; import { setActivePluginRegistry } from "../runtime.js"; import { createPluginRecord } from "../status.test-helpers.js"; @@ -214,24 +213,6 @@ describe("plugin scheduled turns", () => { setActivePluginRegistry(createEmptyPluginRegistry()); }); - it("builds tagged and untagged cron names", () => { - expect( - buildPluginSchedulerCronName({ - pluginId: WORKFLOW_PLUGIN_ID, - sessionKey: MAIN_SESSION_KEY, - tag: "nudge", - uniqueId: "abc", - }), - ).toBe("plugin:workflow-plugin:tag:nudge:agent:main:main:abc"); - expect( - buildPluginSchedulerCronName({ - pluginId: WORKFLOW_PLUGIN_ID, - sessionKey: MAIN_SESSION_KEY, - uniqueId: "xyz", - }), - ).toBe("plugin:workflow-plugin:agent:main:main:xyz"); - }); - it("schedules session turns with cron-compatible tagged cleanup metadata", async () => { mockCronAdd(makeCronJob({ id: "job-tagged" })); diff --git a/src/plugins/contracts/session-actions.contract.test.ts b/src/plugins/contracts/session-actions.contract.test.ts index f697207f6c7c..101c4cb9376f 100644 --- a/src/plugins/contracts/session-actions.contract.test.ts +++ b/src/plugins/contracts/session-actions.contract.test.ts @@ -14,7 +14,7 @@ import { onAgentEvent, resetAgentEventsForTest } from "../../infra/agent-events. import { createEmptyPluginRegistry } from "../registry-empty.js"; import { createPluginRegistry } from "../registry.js"; import { setActivePluginRegistry } from "../runtime.js"; -import { createPluginRecord } from "../status.test-helpers.js"; +import { createPluginRecord } from "../status.test-fixtures.js"; import type { OpenClawPluginApi } from "../types.js"; const MAIN_SESSION_KEY = "agent:main:main"; diff --git a/src/plugins/contracts/session-attachments.contract.test.ts b/src/plugins/contracts/session-attachments.contract.test.ts index 6ac195e6d05b..b752573501ce 100644 --- a/src/plugins/contracts/session-attachments.contract.test.ts +++ b/src/plugins/contracts/session-attachments.contract.test.ts @@ -12,13 +12,8 @@ import { replaceSessionEntry } from "../../config/sessions/session-accessor.js"; import { withTempConfig } from "../../gateway/test-temp-config.js"; import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"; import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; -import { - attachmentProbeFs, - resolveAttachmentDelivery, - resolveSessionAttachmentThreadId, - sendPluginSessionAttachment, -} from "../host-hook-attachments.js"; -import { clearPluginLoaderCache } from "../loader.js"; +import { sendPluginSessionAttachment } from "../host-hook-attachments.js"; +import { clearPluginLoaderCache } from "../loader.test-fixtures.js"; import { createEmptyPluginRegistry } from "../registry-empty.js"; import { createPluginRegistry } from "../registry.js"; import { setActivePluginRegistry } from "../runtime.js"; @@ -147,71 +142,6 @@ describe("plugin session attachments", () => { delete (globalThis as { proofAttachmentLog?: unknown[] }).proofAttachmentLog; }); - it("resolves channel hint precedence for attachment delivery", () => { - expect( - resolveAttachmentDelivery({ - channel: "telegram", - captionFormat: "html", - channelHints: { telegram: { parseMode: "HTML" } }, - }), - ).toEqual({ parseMode: "HTML" }); - expect(resolveAttachmentDelivery({ channel: "telegram", captionFormat: "html" })).toEqual({ - parseMode: "HTML", - }); - expect(resolveAttachmentDelivery({ channel: "telegram", captionFormat: "plain" })).toEqual({ - parseMode: "HTML", - escapePlainHtmlCaption: true, - }); - expect( - resolveAttachmentDelivery({ - channel: "telegram", - captionFormat: "plain", - channelHints: { telegram: { parseMode: "HTML" } }, - }), - ).toEqual({ - parseMode: "HTML", - escapePlainHtmlCaption: true, - }); - expect( - resolveAttachmentDelivery({ - channel: "telegram", - channelHints: { - telegram: { disableNotification: true, forceDocumentMime: "application/pdf" }, - }, - }), - ).toEqual({ - disableNotification: true, - forceDocumentMime: "application/pdf", - }); - expect( - resolveAttachmentDelivery({ - channel: "slack", - channelHints: { slack: { threadTs: "1700000000.000100" } }, - }), - ).toEqual({ threadTs: "1700000000.000100" }); - expect( - resolveAttachmentDelivery({ - channel: "slack", - channelHints: { slack: { threadTs: " 1700000000.000100 " } }, - }), - ).toEqual({ threadTs: "1700000000.000100" }); - expect( - resolveAttachmentDelivery({ - channel: "slack", - channelHints: { slack: { threadTs: " " } }, - }), - ).toEqual({}); - expect(resolveAttachmentDelivery({ channel: "discord", captionFormat: "markdown" })).toEqual( - {}, - ); - expect( - resolveAttachmentDelivery({ - channel: "unknown", - channelHints: { telegram: { parseMode: "HTML" } }, - }), - ).toEqual({}); - }); - it("sends validated files through the session delivery route with channel hints", async () => { await withSessionStore(async ({ storePath, filePath }) => { await writeSessionEntry(storePath, { @@ -308,30 +238,6 @@ describe("plugin session attachments", () => { }); }); - it("prefers the thread encoded in a threaded session key over stale stored routes", () => { - expect( - resolveSessionAttachmentThreadId({ - deliveryThreadId: 42, - fallbackThreadId: "99", - }), - ).toBe("99"); - expect( - resolveSessionAttachmentThreadId({ - deliveryThreadId: 42, - explicitThreadId: 7, - fallbackThreadId: "99", - }), - ).toBe(7); - expect( - resolveSessionAttachmentThreadId({ - deliveryThreadId: 42, - explicitThreadId: 7, - fallbackThreadId: "99", - hintThreadTs: "1700000000.000100", - }), - ).toBe("1700000000.000100"); - }); - it("reports attachment delivery as failed when no delivery result is returned", async () => { await withSessionStore(async ({ storePath, filePath }) => { await writeSessionEntry(storePath); @@ -448,38 +354,6 @@ describe("plugin session attachments", () => { }); }); - it("returns validation errors for unreadable attachment MIME probes", async () => { - await withSessionStore(async ({ storePath, stateDir }) => { - const unreadablePath = path.join(stateDir, "unreadable.pdf"); - await fs.writeFile(unreadablePath, "%PDF-1.7\n", "utf8"); - await writeSessionEntry(storePath); - const originalOpen = attachmentProbeFs.open.bind(attachmentProbeFs); - const openSpy = vi.spyOn(attachmentProbeFs, "open").mockImplementation((async (...args) => { - const [target] = args; - if (path.resolve(String(target)) === unreadablePath) { - throw new Error("EACCES: permission denied, open 'unreadable.pdf'"); - } - return await originalOpen(...args); - }) as typeof fs.open); - - try { - const result = await sendBundledSessionAttachment({ - files: [{ path: unreadablePath }], - channelHints: { telegram: { forceDocumentMime: "application/pdf" } }, - }); - - expect(result.ok).toBe(false); - if (result.ok) { - throw new Error("expected unreadable attachment MIME probe to fail"); - } - expect(result.error).toContain(`attachment file MIME read failed for ${unreadablePath}`); - } finally { - openSpy.mockRestore(); - } - expect(workflowMocks.sendMessage).not.toHaveBeenCalled(); - }); - }); - it("validates force-document MIME using only the configured sniff window", async () => { await withSessionStore(async ({ storePath, stateDir }) => { const pdfPath = path.join(stateDir, "large.pdf"); diff --git a/src/plugins/contracts/session-entry-projection.contract.test.ts b/src/plugins/contracts/session-entry-projection.contract.test.ts index 31bd5bf4b031..61b3f862f1ac 100644 --- a/src/plugins/contracts/session-entry-projection.contract.test.ts +++ b/src/plugins/contracts/session-entry-projection.contract.test.ts @@ -17,7 +17,7 @@ import { patchPluginSessionExtension } from "../host-hook-state.js"; import type { PluginJsonValue } from "../host-hooks.js"; import { createEmptyPluginRegistry } from "../registry-empty.js"; import { setActivePluginRegistry } from "../runtime.js"; -import { createPluginRecord } from "../status.test-helpers.js"; +import { createPluginRecord } from "../status.test-fixtures.js"; import { runTrustedToolPolicies } from "../trusted-tool-policy.js"; function requireRecord(value: unknown, label: string): Record { diff --git a/src/plugins/conversation-binding.test-fixtures.ts b/src/plugins/conversation-binding.test-fixtures.ts new file mode 100644 index 000000000000..3330f1771cfb --- /dev/null +++ b/src/plugins/conversation-binding.test-fixtures.ts @@ -0,0 +1,26 @@ +/** Test-only reset for process-global plugin conversation binding state. */ +import { resolveGlobalMap, resolveGlobalSingleton } from "../shared/global-singleton.js"; + +type PluginBindingGlobalState = { + fallbackNoticeBindingIds: Set; + approvalsCache: unknown; + approvalsLoaded: boolean; + approvalsSaveChain: Promise; +}; + +export function resetPluginConversationBindingStateForTest(): void { + resolveGlobalMap(Symbol.for("openclaw.pluginBindingPendingRequests")).clear(); + const state = resolveGlobalSingleton( + Symbol.for("openclaw.plugins.binding.global-state"), + () => ({ + fallbackNoticeBindingIds: new Set(), + approvalsCache: null, + approvalsLoaded: false, + approvalsSaveChain: Promise.resolve(), + }), + ); + state.approvalsCache = null; + state.approvalsLoaded = false; + state.approvalsSaveChain = Promise.resolve(); + state.fallbackNoticeBindingIds.clear(); +} diff --git a/src/plugins/conversation-binding.test.ts b/src/plugins/conversation-binding.test.ts index 7aec8679c0bb..aaa3dbb56737 100644 --- a/src/plugins/conversation-binding.test.ts +++ b/src/plugins/conversation-binding.test.ts @@ -12,6 +12,7 @@ import { openOpenClawStateDatabase, runOpenClawStateWriteTransaction, } from "../state/openclaw-state-db.js"; +import { resetPluginConversationBindingStateForTest } from "./conversation-binding.test-fixtures.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRegistry } from "./registry.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; @@ -112,7 +113,6 @@ vi.mock("./runtime.js", async () => { }; }); -let testing: typeof import("./conversation-binding.js").testing; let buildPluginBindingApprovalCustomId: typeof import("./conversation-binding.js").buildPluginBindingApprovalCustomId; let detachPluginConversationBinding: typeof import("./conversation-binding.js").detachPluginConversationBinding; let getCurrentPluginConversationBinding: typeof import("./conversation-binding.js").getCurrentPluginConversationBinding; @@ -169,7 +169,6 @@ afterAll(() => { beforeAll(async () => { ({ - testing, buildPluginBindingApprovalCustomId, detachPluginConversationBinding, getCurrentPluginConversationBinding, @@ -282,7 +281,7 @@ async function approveBindingRequest( async function importDuplicateConversationBindingModules() { const first = await importConversationBindingModule(`first-${Date.now()}`); const second = await importConversationBindingModule(`second-${Date.now()}`); - first.testing.reset(); + resetPluginConversationBindingStateForTest(); return { first, second }; } @@ -466,7 +465,7 @@ describe("plugin conversation binding approvals", () => { process.env.OPENCLAW_STATE_DIR = tempRoot; clearPluginBindingApprovalRows(); sessionBindingState.reset(); - testing.reset(); + resetPluginConversationBindingStateForTest(); setActivePluginRegistry(createEmptyPluginRegistry()); unregisterSessionBindingAdapter({ channel: "discord", accountId: "default" }); unregisterSessionBindingAdapter({ channel: "discord", accountId: "work" }); @@ -633,7 +632,7 @@ describe("plugin conversation binding approvals", () => { expect(approved.binding.pluginRoot).toBe("/plugins/codex-a"); expect(approved.binding.conversationId).toBe("-10099:topic:77"); - second.testing.reset(); + resetPluginConversationBindingStateForTest(); }); it("shares persistent approvals across duplicate module instances", async () => { @@ -668,7 +667,7 @@ describe("plugin conversation binding approvals", () => { expect(rebound.status).toBe("bound"); - first.testing.reset(); + resetPluginConversationBindingStateForTest(); clearPluginBindingApprovalRows(); }); diff --git a/src/plugins/conversation-binding.ts b/src/plugins/conversation-binding.ts index d34423df2838..eb7137a5c567 100644 --- a/src/plugins/conversation-binding.ts +++ b/src/plugins/conversation-binding.ts @@ -1018,15 +1018,3 @@ export function buildPluginBindingResolvedText(params: PluginBindingResolveResul } return `Allowed ${params.request.pluginName ?? params.request.pluginId} to bind this conversation once.${summarySuffix}`; } - -export const testing = { - reset() { - pendingRequests.clear(); - const state = getPluginBindingGlobalState(); - state.approvalsCache = null; - state.approvalsLoaded = false; - state.approvalsSaveChain = Promise.resolve(); - state.fallbackNoticeBindingIds.clear(); - }, -}; -export { testing as __testing }; diff --git a/src/plugins/current-plugin-metadata-snapshot.ts b/src/plugins/current-plugin-metadata-snapshot.ts index c9e4736f2e86..4e0ea73a77da 100644 --- a/src/plugins/current-plugin-metadata-snapshot.ts +++ b/src/plugins/current-plugin-metadata-snapshot.ts @@ -25,7 +25,7 @@ registerPluginMetadataProcessMemoLifecycleClear(() => { setCurrentManifestModelIdNormalizationRecords(undefined); }); -export function resolvePluginMetadataControlPlaneFingerprint( +function resolvePluginMetadataControlPlaneFingerprint( config?: OpenClawConfig, options: Omit = {}, ): string { diff --git a/src/plugins/default-enablement.ts b/src/plugins/default-enablement.ts index a236ef781ab7..c5479bccc125 100644 --- a/src/plugins/default-enablement.ts +++ b/src/plugins/default-enablement.ts @@ -1,5 +1,5 @@ /** Manifest fields that control default plugin enablement. */ -export type PluginDefaultEnablement = { +type PluginDefaultEnablement = { enabledByDefault?: boolean; enabledByDefaultOnPlatforms?: readonly string[]; }; diff --git a/src/plugins/dependency-denylist.test.ts b/src/plugins/dependency-denylist.test.ts index 17874b13694f..f5bd0c9c574e 100644 --- a/src/plugins/dependency-denylist.test.ts +++ b/src/plugins/dependency-denylist.test.ts @@ -4,7 +4,6 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import YAML from "yaml"; import { - blockedInstallDependencyPackageNames, findBlockedPackageDirectoryInPath, findBlockedPackageFileAliasInPath, findBlockedManifestDependencies, @@ -35,10 +34,6 @@ function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig { ) as PnpmWorkspaceConfig; } -function readRootLockfile(): string { - return fs.readFileSync(path.resolve(process.cwd(), "pnpm-lock.yaml"), "utf8"); -} - describe("dependency denylist guardrails", () => { it("finds blocked package names on vendored manifests", () => { expect( @@ -206,12 +201,4 @@ describe("dependency denylist guardrails", () => { const manifest = readRootManifest(); expect(findBlockedManifestDependencies(manifest)).toStrictEqual([]); }); - - it("keeps blocked packages out of the lockfile graph", () => { - const lockfile = readRootLockfile(); - for (const packageName of blockedInstallDependencyPackageNames) { - expect(lockfile).not.toContain(`\n ${packageName}@`); - expect(lockfile).not.toContain(`\n ${packageName}: `); - } - }); }); diff --git a/src/plugins/dependency-denylist.ts b/src/plugins/dependency-denylist.ts index 6a8b5f919890..0dec4ac07a59 100644 --- a/src/plugins/dependency-denylist.ts +++ b/src/plugins/dependency-denylist.ts @@ -4,12 +4,10 @@ import { normalizeStringEntries } from "@openclaw/normalization-core/string-norm const BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAMES = ["plain-crypto-js"] as const; /** Package names blocked from installed plugin dependency trees. */ -export const blockedInstallDependencyPackageNames = [ - ...BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAMES, -] as const; +const blockedInstallDependencyPackageNames = [...BLOCKED_INSTALL_DEPENDENCY_PACKAGE_NAMES] as const; /** Finding for blocked dependencies declared in a plugin package manifest. */ -export type BlockedManifestDependencyFinding = { +type BlockedManifestDependencyFinding = { dependencyName: string; declaredAs?: string; field: "dependencies" | "name" | "optionalDependencies" | "overrides" | "peerDependencies"; diff --git a/src/plugins/doctor-contract-registry-loader-state.ts b/src/plugins/doctor-contract-registry-loader-state.ts new file mode 100644 index 000000000000..be91732b4c36 --- /dev/null +++ b/src/plugins/doctor-contract-registry-loader-state.ts @@ -0,0 +1,10 @@ +/** Shared loader state for plugin doctor contracts and test fixtures. */ +import { + createPluginModuleLoaderCache, + type PluginModuleLoaderFactory, +} from "./plugin-module-loader-cache.js"; + +export const pluginDoctorContractRegistryLoaderState = { + moduleLoaders: createPluginModuleLoaderCache(), + moduleLoaderFactory: undefined as PluginModuleLoaderFactory | undefined, +}; diff --git a/src/plugins/doctor-contract-registry.load-paths.test.ts b/src/plugins/doctor-contract-registry.load-paths.test.ts index 8c0c365b9bc6..5344f0bbcfbc 100644 --- a/src/plugins/doctor-contract-registry.load-paths.test.ts +++ b/src/plugins/doctor-contract-registry.load-paths.test.ts @@ -7,11 +7,11 @@ import { findLegacyConfigIssues } from "../config/legacy.js"; import type { OpenClawConfig } from "../config/types.js"; import { applyPluginDoctorCompatibilityMigrations, - clearPluginDoctorContractRegistryCache, listPluginDoctorLegacyConfigRules, listPluginDoctorSessionRouteStateOwners, listPluginDoctorSessionStoreAgentIds, } from "./doctor-contract-registry.js"; +import { clearPluginDoctorContractRegistryCache } from "./doctor-contract-registry.test-fixtures.js"; const tempDirs: string[] = []; const repoRoot = path.resolve(import.meta.dirname, "../.."); diff --git a/src/plugins/doctor-contract-registry.test-fixtures.ts b/src/plugins/doctor-contract-registry.test-fixtures.ts new file mode 100644 index 000000000000..c12f2463c3b8 --- /dev/null +++ b/src/plugins/doctor-contract-registry.test-fixtures.ts @@ -0,0 +1,14 @@ +import { pluginDoctorContractRegistryLoaderState } from "./doctor-contract-registry-loader-state.js"; +/** Test-only controls for plugin doctor contract loading. */ +import type { PluginModuleLoaderFactory } from "./plugin-module-loader-cache.js"; + +export function clearPluginDoctorContractRegistryCache(): void { + pluginDoctorContractRegistryLoaderState.moduleLoaders.clear(); +} + +export function setPluginDoctorContractRegistryModuleLoaderFactoryForTest( + factory: PluginModuleLoaderFactory | undefined, +): void { + pluginDoctorContractRegistryLoaderState.moduleLoaderFactory = factory; + clearPluginDoctorContractRegistryCache(); +} diff --git a/src/plugins/doctor-contract-registry.test.ts b/src/plugins/doctor-contract-registry.test.ts index 19ee248ad173..4d3773c1c726 100644 --- a/src/plugins/doctor-contract-registry.test.ts +++ b/src/plugins/doctor-contract-registry.test.ts @@ -14,14 +14,14 @@ const tempDirs: string[] = []; const mocks = getRegistryJitiMocks(); let applyPluginDoctorCompatibilityMigrations: typeof import("./doctor-contract-registry.js").applyPluginDoctorCompatibilityMigrations; -let clearPluginDoctorContractRegistryCache: typeof import("./doctor-contract-registry.js").clearPluginDoctorContractRegistryCache; +let clearPluginDoctorContractRegistryCache: typeof import("./doctor-contract-registry.test-fixtures.js").clearPluginDoctorContractRegistryCache; let collectRelevantDoctorPluginIds: typeof import("./doctor-contract-registry.js").collectRelevantDoctorPluginIds; let collectRelevantDoctorPluginIdsForTouchedPaths: typeof import("./doctor-contract-registry.js").collectRelevantDoctorPluginIdsForTouchedPaths; let listPluginDoctorLegacyConfigRules: typeof import("./doctor-contract-registry.js").listPluginDoctorLegacyConfigRules; let listPluginDoctorSessionRouteStateOwners: typeof import("./doctor-contract-registry.js").listPluginDoctorSessionRouteStateOwners; let listPluginDoctorSessionStoreAgentIds: typeof import("./doctor-contract-registry.js").listPluginDoctorSessionStoreAgentIds; let setPluginDoctorContractRegistryModuleLoaderFactoryForTest: - | typeof import("./doctor-contract-registry.js").setPluginDoctorContractRegistryModuleLoaderFactoryForTest + | typeof import("./doctor-contract-registry.test-fixtures.js").setPluginDoctorContractRegistryModuleLoaderFactoryForTest | undefined; function makeTempDir(): string { @@ -47,14 +47,16 @@ describe("doctor-contract-registry module loader", () => { vi.resetModules(); ({ applyPluginDoctorCompatibilityMigrations, - clearPluginDoctorContractRegistryCache, collectRelevantDoctorPluginIds, collectRelevantDoctorPluginIdsForTouchedPaths, listPluginDoctorLegacyConfigRules, listPluginDoctorSessionRouteStateOwners, listPluginDoctorSessionStoreAgentIds, - setPluginDoctorContractRegistryModuleLoaderFactoryForTest, } = await import("./doctor-contract-registry.js")); + ({ + clearPluginDoctorContractRegistryCache, + setPluginDoctorContractRegistryModuleLoaderFactoryForTest, + } = await import("./doctor-contract-registry.test-fixtures.js")); setPluginDoctorContractRegistryModuleLoaderFactoryForTest(mocks.createJiti); clearPluginDoctorContractRegistryCache(); }); diff --git a/src/plugins/doctor-contract-registry.ts b/src/plugins/doctor-contract-registry.ts index a045c78fac96..44c23e123982 100644 --- a/src/plugins/doctor-contract-registry.ts +++ b/src/plugins/doctor-contract-registry.ts @@ -11,14 +11,10 @@ import type { OpenKeyedStoreOptions, PluginStateKeyedStore, } from "../plugin-state/plugin-state-store.js"; +import { pluginDoctorContractRegistryLoaderState } from "./doctor-contract-registry-loader-state.js"; import type { DoctorSessionRouteStateOwner } from "./doctor-session-route-state-owner-types.js"; import type { PluginManifestRegistry } from "./manifest-registry.js"; -import { - createPluginModuleLoaderCache, - getCachedPluginModuleLoader, - type PluginModuleLoaderFactory, - type PluginModuleLoaderCache, -} from "./plugin-module-loader-cache.js"; +import { getCachedPluginModuleLoader } from "./plugin-module-loader-cache.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; const CONTRACT_API_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const; @@ -89,22 +85,21 @@ export type PluginDoctorStateMigration = { | { changes: string[]; warnings: string[]; notices?: string[] }; }; -export type PluginDoctorStateMigrationEntry = { +type PluginDoctorStateMigrationEntry = { pluginId: string; migration: PluginDoctorStateMigration; }; type PluginManifestRegistryRecord = PluginManifestRegistry["plugins"][number]; -const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); -let moduleLoaderFactoryForTest: PluginModuleLoaderFactory | undefined; - function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule { return getCachedPluginModuleLoader({ - cache: moduleLoaders, + cache: pluginDoctorContractRegistryLoaderState.moduleLoaders, modulePath, importerUrl: import.meta.url, - ...(moduleLoaderFactoryForTest ? { createLoader: moduleLoaderFactoryForTest } : {}), + ...(pluginDoctorContractRegistryLoaderState.moduleLoaderFactory + ? { createLoader: pluginDoctorContractRegistryLoaderState.moduleLoaderFactory } + : {}), })(modulePath) as PluginDoctorContractModule; } @@ -437,18 +432,6 @@ function resolvePluginDoctorContracts(params?: { return entries; } - -export function clearPluginDoctorContractRegistryCache(): void { - moduleLoaders.clear(); -} - -export function setPluginDoctorContractRegistryModuleLoaderFactoryForTest( - factory: PluginModuleLoaderFactory | undefined, -): void { - moduleLoaderFactoryForTest = factory; - moduleLoaders.clear(); -} - export function listPluginDoctorLegacyConfigRules(params?: { config?: OpenClawConfig; workspaceDir?: string; diff --git a/src/plugins/embedding-provider-runtime.ts b/src/plugins/embedding-provider-runtime.ts index b0019857056e..5bbaf432d4a6 100644 --- a/src/plugins/embedding-provider-runtime.ts +++ b/src/plugins/embedding-provider-runtime.ts @@ -12,8 +12,6 @@ import { type EmbeddingProviderAdapter, } from "./embedding-providers.js"; -export { listRegisteredEmbeddingProviders }; - /** Lists embedding provider adapters registered directly with the process registry. */ function listRegisteredEmbeddingProviderAdapters(): EmbeddingProviderAdapter[] { return listRegisteredEmbeddingProviders().map((entry) => entry.adapter); @@ -56,13 +54,4 @@ export function getEmbeddingProvider( }); } -export type { - EmbeddingInput, - EmbeddingProvider, - EmbeddingProviderAdapter, - EmbeddingProviderCallOptions, - EmbeddingProviderCreateOptions, - EmbeddingProviderCreateResult, - EmbeddingProviderRuntime, - RegisteredEmbeddingProvider, -} from "./embedding-providers.js"; +export type { EmbeddingProvider, EmbeddingProviderAdapter } from "./embedding-providers.js"; diff --git a/src/plugins/embedding-providers.test.ts b/src/plugins/embedding-providers.test.ts index 99d455620c5e..a5da593bf34b 100644 --- a/src/plugins/embedding-providers.test.ts +++ b/src/plugins/embedding-providers.test.ts @@ -3,12 +3,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { collectRegisteredEmbeddingProviderIds } from "./channel-plugin-ids.js"; import { clearEmbeddingProviders, - getEmbeddingProvider, getRegisteredEmbeddingProvider, - listEmbeddingProviders, listRegisteredEmbeddingProviders, registerEmbeddingProvider, - restoreEmbeddingProviders, restoreRegisteredEmbeddingProviders, type EmbeddingProviderAdapter, } from "./embedding-providers.js"; @@ -32,36 +29,6 @@ afterEach(() => { }); describe("embedding provider registry", () => { - it("registers and lists adapters in insertion order", () => { - const alpha = createAdapter("alpha"); - const beta = createAdapter("beta"); - - registerEmbeddingProvider(alpha); - registerEmbeddingProvider(beta); - - expect(listEmbeddingProviders().map((adapter) => adapter.id)).toEqual([ - "openai-compatible", - "alpha", - "beta", - ]); - expect(getEmbeddingProvider("alpha")).toBe(alpha); - }); - - it("restores adapter snapshots", () => { - const alpha = createAdapter("alpha"); - const beta = createAdapter("beta"); - registerEmbeddingProvider(alpha); - - restoreEmbeddingProviders([beta]); - - expect(getEmbeddingProvider("alpha")).toBeUndefined(); - expect(getEmbeddingProvider("beta")).toBe(beta); - expect(listEmbeddingProviders().map((adapter) => adapter.id)).toEqual([ - "openai-compatible", - "beta", - ]); - }); - it("preserves owner metadata in registered snapshots", () => { const adapter = createAdapter("local-compatible"); const entry = { diff --git a/src/plugins/embedding-providers.ts b/src/plugins/embedding-providers.ts index 9f96205a8d60..064a43a4b5c2 100644 --- a/src/plugins/embedding-providers.ts +++ b/src/plugins/embedding-providers.ts @@ -67,12 +67,6 @@ export function getRegisteredEmbeddingProvider( ): RegisteredEmbeddingProvider | undefined { return getCoreEmbeddingProvider(id) ?? getEmbeddingProviders().get(id); } - -/** Returns only the embedding provider adapter for callers that do not need ownership metadata. */ -export function getEmbeddingProvider(id: string): EmbeddingProviderAdapter | undefined { - return getRegisteredEmbeddingProvider(id)?.adapter; -} - /** Lists registered embedding providers with core defaults merged first. */ export function listRegisteredEmbeddingProviders(): RegisteredEmbeddingProvider[] { const merged = new Map( @@ -84,22 +78,7 @@ export function listRegisteredEmbeddingProviders(): RegisteredEmbeddingProvider[ } } return Array.from(merged.values()); -} - -/** Lists embedding provider adapters without registration metadata. */ -export function listEmbeddingProviders(): EmbeddingProviderAdapter[] { - return listRegisteredEmbeddingProviders().map((entry) => entry.adapter); -} - -/** Replaces non-core embedding providers with adapter-only test/runtime state. */ -export function restoreEmbeddingProviders(adapters: EmbeddingProviderAdapter[]): void { - getEmbeddingProviders().clear(); - for (const adapter of adapters) { - registerEmbeddingProvider(adapter); - } -} - -/** Replaces non-core embedding providers while preserving registration metadata. */ +} /** Replaces non-core embedding providers while preserving registration metadata. */ export function restoreRegisteredEmbeddingProviders(entries: RegisteredEmbeddingProvider[]): void { getEmbeddingProviders().clear(); for (const entry of entries) { diff --git a/src/plugins/externalized-bundled-plugins.ts b/src/plugins/externalized-bundled-plugins.ts index daf100098181..3dfc396e5b3f 100644 --- a/src/plugins/externalized-bundled-plugins.ts +++ b/src/plugins/externalized-bundled-plugins.ts @@ -1,5 +1,5 @@ // Defines metadata for bundled plugins that are installed externally. -export type ExternalizedBundledPluginPreferredSource = "npm" | "clawhub"; +type ExternalizedBundledPluginPreferredSource = "npm" | "clawhub"; export type ExternalizedBundledPluginBridge = { /** Plugin id used while the plugin was bundled in core. */ diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 661a1fa0a9cf..dc4a2b076d62 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -577,9 +577,9 @@ function isMemorySlotExplicitlyDisabled(config: OpenClawConfig): boolean { return normalizeOptionalLowercaseString(config.plugins?.slots?.memory) === "none"; } -export type MemoryEmbeddingStartupProviderSource = "provider" | "fallback"; +type MemoryEmbeddingStartupProviderSource = "provider" | "fallback"; -export type ConfiguredMemoryEmbeddingStartupProviderOwner = { +type ConfiguredMemoryEmbeddingStartupProviderOwner = { /** Raw memory-search provider id as configured (normalized). */ configuredId: string; /** diff --git a/src/plugins/git-install.ts b/src/plugins/git-install.ts index 7d6dd2562b31..de7ad6d3509e 100644 --- a/src/plugins/git-install.ts +++ b/src/plugins/git-install.ts @@ -43,19 +43,19 @@ type PluginInstallLogger = { }; /** Resolved Git source metadata persisted into plugin install records. */ -export type GitPluginResolution = { +type GitPluginResolution = { url: string; ref?: string; commit?: string; resolvedAt: string; }; -export type GitPluginInstallResult = +type GitPluginInstallResult = | (Extract & { git: GitPluginResolution }) | Extract; /** Normalized Git plugin install spec accepted by the Git installer. */ -export type ParsedGitPluginSpec = { +type ParsedGitPluginSpec = { input: string; url: string; ref?: string; diff --git a/src/plugins/hardlink-policy.test.ts b/src/plugins/hardlink-policy.test.ts index 3ba3c43e7075..b4e54bf8fb7f 100644 --- a/src/plugins/hardlink-policy.test.ts +++ b/src/plugins/hardlink-policy.test.ts @@ -1,6 +1,6 @@ // Verifies plugin hardlink policy decisions. import { describe, expect, it } from "vitest"; -import { isNixStorePluginRoot, shouldRejectHardlinkedPluginFiles } from "./hardlink-policy.js"; +import { shouldRejectHardlinkedPluginFiles } from "./hardlink-policy.js"; const nixEnv: NodeJS.ProcessEnv = { OPENCLAW_NIX_MODE: "1" }; @@ -34,19 +34,4 @@ describe("plugin hardlink policy", () => { }), ).toBe(true); }); - - it.runIf(process.platform !== "win32")( - "does not reject hardlinked external plugin files when Nix mode loads from the Nix store", - () => { - expect(isNixStorePluginRoot("/nix/store/abc-openclaw-plugin")).toBe(true); - expect(isNixStorePluginRoot("/tmp/nix/store/abc-openclaw-plugin")).toBe(false); - expect( - shouldRejectHardlinkedPluginFiles({ - origin: "config", - rootDir: "/nix/store/abc-openclaw-plugin", - env: nixEnv, - }), - ).toBe(false); - }, - ); }); diff --git a/src/plugins/hardlink-policy.ts b/src/plugins/hardlink-policy.ts index 07317a0bb6c5..d8c61c040a10 100644 --- a/src/plugins/hardlink-policy.ts +++ b/src/plugins/hardlink-policy.ts @@ -16,10 +16,7 @@ const NIX_STORE_ROOT = "/nix/store"; // - /nix/store in OPENCLAW_NIX_MODE: immutable Nix package outputs, where // hardlinked files are normal package-store layout rather than user mutation. /** Returns true when a plugin root resolves inside the immutable Nix store. */ -export function isNixStorePluginRoot( - rootDir: string, - realpathCache?: Map, -): boolean { +function isNixStorePluginRoot(rootDir: string, realpathCache?: Map): boolean { const rootRealPath = safeRealpathSync(rootDir, realpathCache) ?? path.resolve(rootDir); return rootRealPath === NIX_STORE_ROOT || rootRealPath.startsWith(`${NIX_STORE_ROOT}/`); } diff --git a/src/plugins/hook-agent-context.test.ts b/src/plugins/hook-agent-context.test.ts index 78502bb1fb18..c80b4f59aa4b 100644 --- a/src/plugins/hook-agent-context.test.ts +++ b/src/plugins/hook-agent-context.test.ts @@ -3,61 +3,8 @@ import { describe, expect, it } from "vitest"; import { buildAgentHookContextChannelFields, buildAgentHookContextIdentityFields, - resolveAgentHookChannelId, } from "./hook-agent-context.js"; -describe("resolveAgentHookChannelId", () => { - it("derives the conversation id from channel session keys", () => { - expect( - resolveAgentHookChannelId({ - sessionKey: "agent:main:discord:channel:1472750640760623226", - messageChannel: "discord", - messageProvider: "discord", - currentChannelId: "channel:1472750640760623226", - }), - ).toBe("1472750640760623226"); - }); - - it("uses target metadata when the session key is not a channel conversation", () => { - expect( - resolveAgentHookChannelId({ - sessionKey: "agent:main:main", - messageProvider: "telegram", - currentChannelId: "telegram:-1003841603622", - }), - ).toBe("-1003841603622"); - }); - - it("uses message channel prefixes when provider is a narrower route label", () => { - expect( - resolveAgentHookChannelId({ - sessionKey: "agent:main:main", - messageChannel: "discord", - messageProvider: "discord-voice", - currentChannelId: "discord:voice-room", - }), - ).toBe("voice-room"); - }); - - it("uses prefixed message targets before falling back to the provider", () => { - expect( - resolveAgentHookChannelId({ - messageChannel: "channel:1472750640760623226", - messageProvider: "discord", - }), - ).toBe("1472750640760623226"); - }); - - it("falls back to legacy channel/provider values when no conversation id is available", () => { - expect( - resolveAgentHookChannelId({ - messageChannel: "discord", - messageProvider: "discord", - }), - ).toBe("discord"); - }); -}); - describe("buildAgentHookContextChannelFields", () => { it("keeps provider and conversation id separate", () => { expect( diff --git a/src/plugins/hook-agent-context.ts b/src/plugins/hook-agent-context.ts index 6e9429bbbb23..dca2f2ecbd4a 100644 --- a/src/plugins/hook-agent-context.ts +++ b/src/plugins/hook-agent-context.ts @@ -67,7 +67,7 @@ function resolveAgentHookChannel(params: { } /** Resolves the channel id exposed to plugin agent hooks. */ -export function resolveAgentHookChannelId(params: { +function resolveAgentHookChannelId(params: { sessionKey?: string | null; messageChannel?: string | null; messageProvider?: string | null; diff --git a/src/plugins/hook-decision-types.test.ts b/src/plugins/hook-decision-types.test.ts index 975641497d86..713b4a4796ce 100644 --- a/src/plugins/hook-decision-types.test.ts +++ b/src/plugins/hook-decision-types.test.ts @@ -1,13 +1,6 @@ /** Verifies hook decision type guards and normalization helpers. */ import { describe, expect, it } from "vitest"; -import { - BLOCK_MESSAGE_PREFIX, - type HookDecision, - type HookDecisionBlock, - mergeHookDecisions, - isHookDecision, - resolveBlockMessage, -} from "./hook-decision-types.js"; +import { isHookDecision } from "./hook-decision-types.js"; describe("HookDecision helpers", () => { describe("isHookDecision", () => { @@ -33,50 +26,4 @@ describe("HookDecision helpers", () => { expect(isHookDecision({ outcome: "block", reason: "policy", metadata: [] })).toBe(false); }); }); - - describe("mergeHookDecisions", () => { - const passDecision: HookDecision = { outcome: "pass" }; - const blockDecision: HookDecision = { outcome: "block", reason: "policy" }; - - it("uses most-restrictive-wins ordering", () => { - expect(mergeHookDecisions(undefined, passDecision)).toBe(passDecision); - expect(mergeHookDecisions(passDecision, blockDecision)).toBe(blockDecision); - expect(mergeHookDecisions(blockDecision, passDecision)).toBe(blockDecision); - }); - - it("keeps the first decision when outcomes have the same severity", () => { - const secondBlock: HookDecision = { outcome: "block", reason: "second" }; - - expect(mergeHookDecisions(passDecision, { outcome: "pass" })).toBe(passDecision); - expect(mergeHookDecisions(blockDecision, secondBlock)).toBe(blockDecision); - }); - }); - - describe("resolveBlockMessage", () => { - it("returns explicit or default block messages", () => { - const explicit: HookDecisionBlock = { - outcome: "block", - reason: "policy", - message: "Please rephrase your request.", - }; - const fallback: HookDecisionBlock = { - outcome: "block", - reason: "policy", - }; - - expect(resolveBlockMessage(explicit)).toBe( - `${BLOCK_MESSAGE_PREFIX}: Please rephrase your request.`, - ); - expect(resolveBlockMessage(fallback)).toBe(`${BLOCK_MESSAGE_PREFIX}: blocked`); - expect(resolveBlockMessage(fallback, { blockedBy: "policy-plugin" })).toBe( - `${BLOCK_MESSAGE_PREFIX}: blocked by policy-plugin`, - ); - expect(resolveBlockMessage(explicit, { blockedBy: "policy-plugin" })).toBe( - `${BLOCK_MESSAGE_PREFIX}: Please rephrase your request. (blocked by policy-plugin)`, - ); - expect(resolveBlockMessage({ ...explicit, message: " " })).toBe( - `${BLOCK_MESSAGE_PREFIX}: blocked`, - ); - }); - }); }); diff --git a/src/plugins/hook-decision-types.ts b/src/plugins/hook-decision-types.ts index 253c7320b57d..401157070f0d 100644 --- a/src/plugins/hook-decision-types.ts +++ b/src/plugins/hook-decision-types.ts @@ -3,21 +3,21 @@ * Core is outcome-agnostic — it handles the mechanics of each outcome * without knowing *why* the decision was made. */ -export type HookDecision = HookDecisionPass | HookDecisionBlock; +type HookDecision = HookDecisionPass | HookDecisionBlock; /** Content is fine. Proceed normally. */ -export type HookDecisionPass = { +type HookDecisionPass = { outcome: "pass"; }; /** Prefix for user-facing replacement messages when a `block` decision stops a request. */ -export const BLOCK_MESSAGE_PREFIX = "Your message could not be sent"; +const BLOCK_MESSAGE_PREFIX = "Your message could not be sent"; /** * Content is blocked. `reason` is internal plugin-local detail; core must not log, * persist, broadcast, or expose it verbatim. `message` is user-facing detail. */ -export type HookDecisionBlock = { +type HookDecisionBlock = { outcome: "block"; /** Internal plugin-local reason. Do not log, persist, broadcast, or expose verbatim. */ reason: string; @@ -45,23 +45,6 @@ export function resolveBlockMessage( : `${BLOCK_MESSAGE_PREFIX}: blocked`; } -/** Outcome severity for most-restrictive-wins merging. Higher = more restrictive. */ -const HOOK_DECISION_SEVERITY: Record = { - pass: 0, - block: 2, -}; - -/** - * Merge two HookDecisions using most-restrictive-wins semantics. - * `block > pass` - */ -export function mergeHookDecisions(a: HookDecision | undefined, b: HookDecision): HookDecision { - if (!a) { - return b; - } - return HOOK_DECISION_SEVERITY[b.outcome] > HOOK_DECISION_SEVERITY[a.outcome] ? b : a; -} - /** * Type guard: does this object look like a HookDecision (has `outcome` field)? */ diff --git a/src/plugins/hook-registry.types.ts b/src/plugins/hook-registry.types.ts index fe8fef3638b6..16a452498c65 100644 --- a/src/plugins/hook-registry.types.ts +++ b/src/plugins/hook-registry.types.ts @@ -3,7 +3,7 @@ import type { HookEntry } from "../hooks/types.js"; import type { PluginHookRegistration as TypedPluginHookRegistration } from "./hook-types.js"; /** Legacy hook registration stored by the global hook runner registry. */ -export type PluginLegacyHookRegistration = { +type PluginLegacyHookRegistration = { pluginId: string; entry: HookEntry; events: string[]; diff --git a/src/plugins/hook-resolve-exec-env.test.ts b/src/plugins/hook-resolve-exec-env.test.ts index 026de4ac4ebf..19e7c03b0b09 100644 --- a/src/plugins/hook-resolve-exec-env.test.ts +++ b/src/plugins/hook-resolve-exec-env.test.ts @@ -1,7 +1,7 @@ // Verifies plugin hook exec environment resolution. import { afterEach, describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addStaticTestHooks, addTestHook } from "./hooks.test-helpers.js"; +import { addStaticTestHooks, addTestHook } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry } from "./registry.js"; import type { PluginHookResolveExecEnvContext } from "./types.js"; diff --git a/src/plugins/hook-runner-global-state.ts b/src/plugins/hook-runner-global-state.ts index 5c61c5f302da..40b95e83d2f2 100644 --- a/src/plugins/hook-runner-global-state.ts +++ b/src/plugins/hook-runner-global-state.ts @@ -14,7 +14,7 @@ type TrustedPolicyHookRunnerRegistry = GlobalHookRunnerRegistry & { trustedToolPolicies?: PluginTrustedToolPolicyRegistryRegistration[]; }; -export type HookRunnerGlobalState = { +type HookRunnerGlobalState = { hookRunner: HookRunner | null; registry: TrustedPolicyHookRunnerRegistry | null; }; diff --git a/src/plugins/hook-runner-global.compose.test.ts b/src/plugins/hook-runner-global.compose.test.ts index f4bf4ff8dc52..caab21733f7e 100644 --- a/src/plugins/hook-runner-global.compose.test.ts +++ b/src/plugins/hook-runner-global.compose.test.ts @@ -13,14 +13,14 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "./hook-runner-global.js"; -import { addTestHook, createMockPluginRegistry } from "./hooks.test-helpers.js"; +import { addTestHook, createMockPluginRegistry } from "./hooks.test-fixtures.js"; import type { PluginRegistry } from "./registry.js"; import { pinActivePluginChannelRegistry, resetPluginRuntimeStateForTest, setActivePluginRegistry, } from "./runtime.js"; -import { createPluginRecord } from "./status.test-helpers.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; function runner() { const value = getGlobalHookRunner(); diff --git a/src/plugins/hook-runner-global.test.ts b/src/plugins/hook-runner-global.test.ts index 3142b4ba257f..03cb2ef52bf8 100644 --- a/src/plugins/hook-runner-global.test.ts +++ b/src/plugins/hook-runner-global.test.ts @@ -1,13 +1,13 @@ /** Verifies global hook runner sequencing, mutation, and error behavior. */ import { afterEach, describe, expect, it, vi } from "vitest"; -import { createMockPluginRegistry } from "./hooks.test-helpers.js"; +import { createMockPluginRegistry } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import { pinActivePluginChannelRegistry, releasePinnedPluginChannelRegistry, setActivePluginRegistry, } from "./runtime.js"; -import { createPluginRecord } from "./status.test-helpers.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; async function importHookRunnerGlobalModule() { return import("./hook-runner-global.js"); diff --git a/src/plugins/hooks.before-agent-finalize.test.ts b/src/plugins/hooks.before-agent-finalize.test.ts index 1ff50711a36a..3a3027fa6c80 100644 --- a/src/plugins/hooks.before-agent-finalize.test.ts +++ b/src/plugins/hooks.before-agent-finalize.test.ts @@ -1,7 +1,7 @@ // Covers plugin hooks that run before agent finalization. import { describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; +import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-fixtures.js"; const EVENT = { runId: "run-1", diff --git a/src/plugins/hooks.before-agent-reply.test.ts b/src/plugins/hooks.before-agent-reply.test.ts index ffb0bb221616..ec9642d9ce21 100644 --- a/src/plugins/hooks.before-agent-reply.test.ts +++ b/src/plugins/hooks.before-agent-reply.test.ts @@ -1,7 +1,7 @@ // Covers plugin hooks that run before agent replies are emitted. import { describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; +import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-fixtures.js"; const EVENT = { cleanedBody: "hello world" }; diff --git a/src/plugins/hooks.before-agent-start.test.ts b/src/plugins/hooks.before-agent-start.test.ts index b6de78e63772..45d0e4b80f7c 100644 --- a/src/plugins/hooks.before-agent-start.test.ts +++ b/src/plugins/hooks.before-agent-start.test.ts @@ -7,7 +7,7 @@ */ import { beforeEach, describe, expect, it } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addStaticTestHooks, addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; +import { addStaticTestHooks, addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookBeforeAgentStartResult, PluginHookRegistration } from "./types.js"; diff --git a/src/plugins/hooks.before-install.test.ts b/src/plugins/hooks.before-install.test.ts index 0f4c02fd991c..311cc81eeb83 100644 --- a/src/plugins/hooks.before-install.test.ts +++ b/src/plugins/hooks.before-install.test.ts @@ -1,7 +1,7 @@ // Covers hook behavior before plugin install state exists. import { beforeEach, describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addTestHook } from "./hooks.test-helpers.js"; +import { addTestHook } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookBeforeInstallContext, diff --git a/src/plugins/hooks.before-tool-call.test.ts b/src/plugins/hooks.before-tool-call.test.ts index e6f8294e0069..c5720efc54e9 100644 --- a/src/plugins/hooks.before-tool-call.test.ts +++ b/src/plugins/hooks.before-tool-call.test.ts @@ -1,7 +1,7 @@ /** Tests before-tool-call hook ordering, mutation, and cancellation behavior. */ import { beforeEach, describe, expect, it } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addStaticTestHooks } from "./hooks.test-helpers.js"; +import { addStaticTestHooks } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookToolContext } from "./types.js"; import type { PluginHookBeforeToolCallResult } from "./types.js"; diff --git a/src/plugins/hooks.compaction-timeout.test.ts b/src/plugins/hooks.compaction-timeout.test.ts index f1de81614eaf..5d6ac843584d 100644 --- a/src/plugins/hooks.compaction-timeout.test.ts +++ b/src/plugins/hooks.compaction-timeout.test.ts @@ -10,7 +10,7 @@ */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; +import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookRegistration } from "./types.js"; diff --git a/src/plugins/hooks.correlation.test.ts b/src/plugins/hooks.correlation.test.ts index 2612ea994c09..72c2f7fc85cf 100644 --- a/src/plugins/hooks.correlation.test.ts +++ b/src/plugins/hooks.correlation.test.ts @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; +import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookRegistration } from "./types.js"; diff --git a/src/plugins/hooks.model-override-wiring.test.ts b/src/plugins/hooks.model-override-wiring.test.ts index 6b227192b3c9..cce34d793da8 100644 --- a/src/plugins/hooks.model-override-wiring.test.ts +++ b/src/plugins/hooks.model-override-wiring.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { joinPresentTextSegments } from "../shared/text/join-segments.js"; import { createHookRunner } from "./hooks.js"; -import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; +import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookAgentContext, diff --git a/src/plugins/hooks.phase-hooks.test.ts b/src/plugins/hooks.phase-hooks.test.ts index 47fac9be6083..b71e3d3aaff9 100644 --- a/src/plugins/hooks.phase-hooks.test.ts +++ b/src/plugins/hooks.phase-hooks.test.ts @@ -1,7 +1,7 @@ /** Tests phase-scoped plugin hooks and hook registration ordering. */ import { beforeEach, describe, expect, it } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addStaticTestHooks } from "./hooks.test-helpers.js"; +import { addStaticTestHooks } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookBeforeModelResolveResult, diff --git a/src/plugins/hooks.security.test.ts b/src/plugins/hooks.security.test.ts index 85429f8fb4b0..4f0a45a3297e 100644 --- a/src/plugins/hooks.security.test.ts +++ b/src/plugins/hooks.security.test.ts @@ -1,7 +1,7 @@ // Verifies plugin hook security constraints and rejections. import { beforeEach, describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; -import { addStaticTestHooks } from "./hooks.test-helpers.js"; +import { addStaticTestHooks } from "./hooks.test-fixtures.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js"; import type { PluginHookBeforeToolCallResult, PluginHookMessageSendingResult } from "./types.js"; diff --git a/src/plugins/hooks.sync-only.test.ts b/src/plugins/hooks.sync-only.test.ts deleted file mode 100644 index 59706885eb91..000000000000 --- a/src/plugins/hooks.sync-only.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Verifies sync-only plugin hook execution constraints. -import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; -import { describe, expect, it, vi } from "vitest"; -import { createHookRunner, type HookRunnerLogger } from "./hooks.js"; -import { createMockPluginRegistry } from "./hooks.test-helpers.js"; - -function createToolResultMessage(text: string): AgentMessage { - return { - role: "toolResult", - toolCallId: "call_1", - content: [{ type: "text", text }], - isError: false, - } as AgentMessage; -} - -function createLogger(): HookRunnerLogger & { - warn: ReturnType; - error: ReturnType; -} { - const warn = vi.fn<(message: string) => void>(); - const error = vi.fn<(message: string) => void>(); - return { - warn, - error, - }; -} - -describe("sync-only plugin hooks", () => { - it("warns and ignores accidental async tool_result_persist handlers", () => { - const logger = createLogger(); - const originalMessage = createToolResultMessage("original"); - const replacementMessage = createToolResultMessage("replacement"); - const runner = createHookRunner( - createMockPluginRegistry([ - { - hookName: "tool_result_persist", - pluginId: "async-tool-result", - handler: async () => ({ message: replacementMessage }), - }, - ]), - { logger }, - ); - - const result = runner.runToolResultPersist( - { message: originalMessage }, - { agentId: "agent-1", sessionKey: "session-1" }, - ); - - expect(result).toEqual({ message: originalMessage }); - expect(logger.warn.mock.calls).toEqual([ - [ - "[hooks] tool_result_persist handler from async-tool-result returned a Promise; this hook is synchronous and the result was ignored.", - ], - ]); - expect(logger.error).not.toHaveBeenCalled(); - }); - - it("warns and ignores accidental async before_message_write handlers", () => { - const logger = createLogger(); - const originalMessage = createToolResultMessage("original"); - const runner = createHookRunner( - createMockPluginRegistry([ - { - hookName: "before_message_write", - pluginId: "async-before-write", - handler: async () => ({ block: true }), - }, - ]), - { logger }, - ); - - const result = runner.runBeforeMessageWrite( - { message: originalMessage, sessionKey: "session-1", agentId: "agent-1" }, - { agentId: "agent-1", sessionKey: "session-1" }, - ); - - expect(result).toBeUndefined(); - expect(logger.warn.mock.calls).toEqual([ - [ - "[hooks] before_message_write handler from async-before-write returned a Promise; this hook is synchronous and the result was ignored.", - ], - ]); - expect(logger.error).not.toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/hooks.test-fixtures.ts b/src/plugins/hooks.test-fixtures.ts new file mode 100644 index 000000000000..c5ae1de404c2 --- /dev/null +++ b/src/plugins/hooks.test-fixtures.ts @@ -0,0 +1,63 @@ +/** Test-only helpers for exercising plugin hook behavior. */ +import { createHookRunner } from "./hooks.js"; +import { addTestHook, createMockPluginRegistry } from "./hooks.test-helpers.js"; +import type { PluginRegistry } from "./registry.js"; +import type { PluginHookAgentContext, PluginHookRegistration } from "./types.js"; + +export { addTestHook, createMockPluginRegistry }; +export type { + PluginHookBeforeDispatchResult, + PluginHookReplyDispatchResult, +} from "./hook-types.js"; +export type PluginTargetedInboundClaimOutcome = Awaited< + ReturnType["runInboundClaimForPluginOutcome"]> +>; + +export const TEST_PLUGIN_AGENT_CTX: PluginHookAgentContext = { + runId: "test-run-id", + agentId: "test-agent", + sessionKey: "test-session", + sessionId: "test-session-id", + workspaceDir: "/tmp/openclaw-test", + messageProvider: "test", +}; + +export function addStaticTestHooks( + registry: PluginRegistry, + params: { + hookName: PluginHookRegistration["hookName"]; + hooks: ReadonlyArray<{ + pluginId: string; + result: TResult; + priority?: number; + handler?: () => TResult | Promise; + }>; + }, +) { + for (const { pluginId, result, priority, handler } of params.hooks) { + addTestHook({ + registry, + pluginId, + hookName: params.hookName, + handler: (handler ?? (() => result)) as PluginHookRegistration["handler"], + ...(priority !== undefined ? { priority } : {}), + }); + } +} + +export function createHookRunnerWithRegistry( + hooks: Array<{ + hookName: string; + handler: (...args: unknown[]) => unknown; + pluginId?: string; + priority?: number; + timeoutMs?: number; + }>, + options?: Parameters[1], +) { + const registry = createMockPluginRegistry(hooks); + return { + registry, + runner: createHookRunner(registry, options), + }; +} diff --git a/src/plugins/hooks.test-helpers.ts b/src/plugins/hooks.test-helpers.ts index 488db85125b3..4a9d88c994a0 100644 --- a/src/plugins/hooks.test-helpers.ts +++ b/src/plugins/hooks.test-helpers.ts @@ -1,10 +1,9 @@ // Provides shared helpers for plugin hook tests. import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; -import { createHookRunner } from "./hooks.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRegistry } from "./registry.js"; import { createPluginRecord } from "./status.test-helpers.js"; -import type { PluginHookAgentContext, PluginHookRegistration } from "./types.js"; +import type { PluginHookRegistration } from "./types.js"; export function createMockPluginRegistry( hooks: Array<{ @@ -40,16 +39,6 @@ export function createMockPluginRegistry( })) as PluginRegistry["typedHooks"], }; } - -export const TEST_PLUGIN_AGENT_CTX: PluginHookAgentContext = { - runId: "test-run-id", - agentId: "test-agent", - sessionKey: "test-session", - sessionId: "test-session-id", - workspaceDir: "/tmp/openclaw-test", - messageProvider: "test", -}; - export function addTestHook(params: { registry: PluginRegistry; pluginId: string; @@ -67,65 +56,3 @@ export function addTestHook(params: { source: "test", } as PluginHookRegistration); } - -function addTestHooks( - registry: PluginRegistry, - hooks: ReadonlyArray<{ - pluginId: string; - hookName: PluginHookRegistration["hookName"]; - handler: PluginHookRegistration["handler"]; - priority?: number; - timeoutMs?: number; - }>, -) { - for (const hook of hooks) { - addTestHook({ - registry, - pluginId: hook.pluginId, - hookName: hook.hookName, - handler: hook.handler, - ...(hook.priority !== undefined ? { priority: hook.priority } : {}), - ...(hook.timeoutMs !== undefined ? { timeoutMs: hook.timeoutMs } : {}), - }); - } -} - -export function addStaticTestHooks( - registry: PluginRegistry, - params: { - hookName: PluginHookRegistration["hookName"]; - hooks: ReadonlyArray<{ - pluginId: string; - result: TResult; - priority?: number; - handler?: () => TResult | Promise; - }>; - }, -) { - addTestHooks( - registry, - params.hooks.map(({ pluginId, result, priority, handler }) => ({ - pluginId, - hookName: params.hookName, - handler: (handler ?? (() => result)) as PluginHookRegistration["handler"], - ...(priority !== undefined ? { priority } : {}), - })), - ); -} - -export function createHookRunnerWithRegistry( - hooks: Array<{ - hookName: string; - handler: (...args: unknown[]) => unknown; - pluginId?: string; - priority?: number; - timeoutMs?: number; - }>, - options?: Parameters[1], -) { - const registry = createMockPluginRegistry(hooks); - return { - registry, - runner: createHookRunner(registry, options), - }; -} diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index ab3c1bced424..fb12f006b953 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -61,9 +61,6 @@ import type { PluginHookCronReconciledContext, PluginHookCronReconciledEvent, PluginHookCronChangedEvent, - PluginHookGatewayCronDeliveryStatus, - PluginHookGatewayCronJobState, - PluginHookGatewayCronRunStatus, PluginHookGatewayContext, PluginHookGatewayStartEvent, PluginHookGatewayStopEvent, @@ -98,87 +95,14 @@ import type { } from "./hook-types.js"; // Re-export types for consumers -export type { - PluginHookAgentContext, - PluginHookBeforeAgentReplyEvent, - PluginHookBeforeAgentReplyResult, - PluginHookBeforeAgentStartEvent, - PluginHookBeforeAgentStartResult, - PluginHookBeforeDispatchContext, - PluginHookBeforeDispatchEvent, - PluginHookBeforeDispatchResult, - PluginHookReplyPayloadSendingContext, - PluginHookReplyPayloadSendingEvent, - PluginHookReplyPayloadSendingResult, - PluginHookReplyPayload, - PluginHookReplyDispatchContext, - PluginHookReplyDispatchEvent, - PluginHookReplyDispatchResult, - PluginHookBeforeModelResolveEvent, - PluginHookBeforeModelResolveResult, - PluginHookBeforePromptBuildEvent, - PluginHookBeforePromptBuildResult, - PluginHookModelCallEndedEvent, - PluginHookModelCallStartedEvent, - PluginHookLlmInputEvent, - PluginHookLlmOutputEvent, - PluginHookBeforeAgentFinalizeEvent, - PluginHookBeforeAgentFinalizeResult, - PluginHookAgentEndEvent, - PluginHookBeforeCompactionEvent, - PluginHookBeforeResetEvent, - PluginHookInboundClaimContext, - PluginHookInboundClaimEvent, - PluginHookInboundClaimResult, - PluginHookAfterCompactionEvent, - PluginHookMessageContext, - PluginHookMessageReceivedEvent, - PluginHookMessageSendingEvent, - PluginHookMessageSendingResult, - PluginHookMessageSentEvent, - PluginHookToolContext, - PluginHookBeforeToolCallEvent, - PluginHookBeforeToolCallResult, - PluginHookBeforeAgentRunEvent, - PluginHookCronReconciledContext, - PluginHookCronReconciledEvent, - PluginHookAfterToolCallEvent, - PluginHookToolResultPersistContext, - PluginHookToolResultPersistEvent, - PluginHookToolResultPersistResult, - PluginHookBeforeMessageWriteEvent, - PluginHookBeforeMessageWriteResult, - PluginHookSessionContext, - PluginHookSessionStartEvent, - PluginHookSessionEndEvent, - PluginHookSubagentContext, - PluginHookSubagentDeliveryTargetEvent, - PluginHookSubagentDeliveryTargetResult, - PluginHookSubagentSpawningEvent, - PluginHookSubagentSpawningResult, - PluginHookSubagentSpawnedEvent, - PluginHookSubagentEndedEvent, - PluginHookCronChangedEvent, - PluginHookGatewayCronDeliveryStatus, - PluginHookGatewayCronJobState, - PluginHookGatewayCronRunStatus, - PluginHookGatewayContext, - PluginHookGatewayStartEvent, - PluginHookGatewayStopEvent, - PluginHookBeforeInstallContext, - PluginHookBeforeInstallEvent, - PluginHookBeforeInstallResult, - PluginHookResolveExecEnvContext, - PluginHookResolveExecEnvEvent, -}; -export type HookRunnerLogger = { +type HookRunnerLogger = { debug?: (message: string) => void; warn: (message: string) => void; error: (message: string) => void; }; -export type HookFailurePolicy = "fail-open" | "fail-closed"; +type HookFailurePolicy = "fail-open" | "fail-closed"; export type VoidHookRunOptions = { unrefTimeout?: boolean; }; @@ -188,7 +112,7 @@ type BeforeAgentFinalizeResultWithRetryCandidates = PluginHookBeforeAgentFinaliz retryCandidates?: BeforeAgentFinalizeRetry[]; }; -export type HookRunnerOptions = { +type HookRunnerOptions = { logger?: HookRunnerLogger; /** If true, errors in hooks will be caught and logged instead of thrown */ catchErrors?: boolean; @@ -258,7 +182,7 @@ type ModifyingHookPolicy = { onTerminal?: (params: { hookName: K; pluginId: string; result: TResult }) => void; }; -export type PluginTargetedInboundClaimOutcome = +type PluginTargetedInboundClaimOutcome = | { status: "handled"; result: PluginHookInboundClaimResult; diff --git a/src/plugins/host-hook-attachments.ts b/src/plugins/host-hook-attachments.ts index 6e3b41057ef1..0a9759fac269 100644 --- a/src/plugins/host-hook-attachments.ts +++ b/src/plugins/host-hook-attachments.ts @@ -26,7 +26,7 @@ import type { PluginOrigin } from "./plugin-origin.types.js"; const DEFAULT_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024; /** Filesystem adapter used by attachment MIME probes and tests. */ -export const attachmentProbeFs = { +const attachmentProbeFs = { open: (...args: Parameters) => fsPromises.open(...args), }; const MAX_ATTACHMENT_FILES = 10; @@ -89,7 +89,7 @@ async function readMimeSniffBuffer( } /** Resolves channel-specific attachment delivery options from caption format and hints. */ -export function resolveAttachmentDelivery(params: { +function resolveAttachmentDelivery(params: { channel: string; captionFormat?: PluginSessionAttachmentCaptionFormat; channelHints?: PluginAttachmentChannelHints; @@ -213,7 +213,7 @@ function normalizeOptionalThreadId(value: unknown): string | number | undefined } /** Resolves the thread id used when delivering a plugin session attachment. */ -export function resolveSessionAttachmentThreadId(params: { +function resolveSessionAttachmentThreadId(params: { deliveryThreadId?: unknown; explicitThreadId?: unknown; fallbackThreadId?: unknown; diff --git a/src/plugins/host-hook-cleanup.ts b/src/plugins/host-hook-cleanup.ts index 31ae53841a86..9086f885f8af 100644 --- a/src/plugins/host-hook-cleanup.ts +++ b/src/plugins/host-hook-cleanup.ts @@ -1,10 +1,7 @@ /** Runs plugin cleanup callbacks and clears host-side plugin session/runtime state. */ import { normalizeOptionalAgentRuntimeId } from "../agents/agent-runtime-id.js"; import { getRuntimeConfig } from "../config/config.js"; -import { - cleanupPluginHostSessionStore, - clearPluginOwnedSessionState, -} from "../config/sessions/session-accessor.js"; +import { cleanupPluginHostSessionStore } from "../config/sessions/session-accessor.js"; import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js"; import { resolveAllAgentSessionStoreTargetsSync, @@ -22,18 +19,16 @@ import type { PluginRegistry } from "./registry-types.js"; import { getActivePluginRegistry } from "./runtime.js"; import { normalizeSessionEntrySlotKey } from "./session-entry-slot-keys.js"; -export { clearPluginOwnedSessionState }; - /** Failure captured while running plugin cleanup hooks. */ /** Failure captured while running one plugin cleanup callback. */ -export type PluginHostCleanupFailure = { +type PluginHostCleanupFailure = { pluginId: string; hookId: string; error: unknown; }; /** Aggregate cleanup result for plugin host state. */ -export type PluginHostCleanupResult = { +type PluginHostCleanupResult = { cleanupCount: number; failures: PluginHostCleanupFailure[]; }; diff --git a/src/plugins/host-hook-json.ts b/src/plugins/host-hook-json.ts index 8177e959238f..100c2fb1cea8 100644 --- a/src/plugins/host-hook-json.ts +++ b/src/plugins/host-hook-json.ts @@ -1,5 +1,5 @@ /** JSON primitive values accepted across plugin host-hook boundaries. */ -export type PluginJsonPrimitive = string | number | boolean | null; +type PluginJsonPrimitive = string | number | boolean | null; /** Bounded JSON value shape accepted from plugin hooks. */ export type PluginJsonValue = @@ -8,7 +8,7 @@ export type PluginJsonValue = | { [key: string]: PluginJsonValue }; /** Resource limits for untrusted plugin JSON payload validation. */ -export type PluginJsonValueLimits = { +type PluginJsonValueLimits = { maxDepth: number; maxNodes: number; maxObjectKeys: number; diff --git a/src/plugins/host-hook-runtime.test-fixtures.ts b/src/plugins/host-hook-runtime.test-fixtures.ts new file mode 100644 index 000000000000..4da386b0815a --- /dev/null +++ b/src/plugins/host-hook-runtime.test-fixtures.ts @@ -0,0 +1,42 @@ +/** Test-only inspection of process-global plugin host runtime state. */ +import { resolveGlobalSingleton } from "../shared/global-singleton.js"; +import type { + PluginSessionSchedulerJobHandle, + PluginSessionSchedulerJobRegistration, +} from "./host-hooks.js"; + +export const PLUGIN_TERMINAL_EVENT_CLEANUP_WAIT_MS = 5_000; + +type SchedulerJobRecord = { + job: PluginSessionSchedulerJobRegistration; +}; + +type PluginHostRuntimeState = { + schedulerJobsByPlugin: Map>; +}; + +export function listPluginSessionSchedulerJobs( + pluginId?: string, +): PluginSessionSchedulerJobHandle[] { + const state = resolveGlobalSingleton( + Symbol.for("openclaw.pluginHostRuntimeState"), + () => ({ schedulerJobsByPlugin: new Map() }), + ); + const records: PluginSessionSchedulerJobHandle[] = []; + const pluginIds = pluginId ? [pluginId] : [...state.schedulerJobsByPlugin.keys()]; + for (const currentPluginId of pluginIds) { + const jobs = state.schedulerJobsByPlugin.get(currentPluginId); + if (!jobs) { + continue; + } + for (const record of jobs.values()) { + records.push({ + id: record.job.id, + pluginId: currentPluginId, + sessionKey: record.job.sessionKey, + kind: record.job.kind, + }); + } + } + return records.toSorted((left, right) => left.id.localeCompare(right.id)); +} diff --git a/src/plugins/host-hook-runtime.ts b/src/plugins/host-hook-runtime.ts index 63850df9a052..6f952503cfb5 100644 --- a/src/plugins/host-hook-runtime.ts +++ b/src/plugins/host-hook-runtime.ts @@ -41,7 +41,7 @@ type PluginHostRuntimeState = { const PLUGIN_HOST_RUNTIME_STATE_KEY = Symbol.for("openclaw.pluginHostRuntimeState"); const CLOSED_RUN_IDS_MAX = 512; -export const PLUGIN_TERMINAL_EVENT_CLEANUP_WAIT_MS = 5_000; +const PLUGIN_TERMINAL_EVENT_CLEANUP_WAIT_MS = 5_000; const log = createSubsystemLogger("plugins/host-hooks"); function getPluginHostRuntimeState(): PluginHostRuntimeState { @@ -619,26 +619,3 @@ export function clearPluginHostRuntimeState(params?: { pluginId?: string; runId? state.terminalEventCleanupExpiredRunIds.clear(); } } - -export function listPluginSessionSchedulerJobs( - pluginId?: string, -): PluginSessionSchedulerJobHandle[] { - const state = getPluginHostRuntimeState(); - const records: PluginSessionSchedulerJobHandle[] = []; - const pluginIds = pluginId ? [pluginId] : [...state.schedulerJobsByPlugin.keys()]; - for (const currentPluginId of pluginIds) { - const jobs = state.schedulerJobsByPlugin.get(currentPluginId); - if (!jobs) { - continue; - } - for (const record of jobs.values()) { - records.push({ - id: record.job.id, - pluginId: currentPluginId, - sessionKey: record.job.sessionKey, - kind: record.job.kind, - }); - } - } - return records.toSorted((left, right) => left.id.localeCompare(right.id)); -} diff --git a/src/plugins/host-hook-scheduled-turns.ts b/src/plugins/host-hook-scheduled-turns.ts index 616eee91ed49..2eea53b764ef 100644 --- a/src/plugins/host-hook-scheduled-turns.ts +++ b/src/plugins/host-hook-scheduled-turns.ts @@ -139,7 +139,7 @@ function resolvePluginSessionTurnTag(value: unknown): { return { tag, invalid: false }; } -export function buildPluginSchedulerCronName(params: { +function buildPluginSchedulerCronName(params: { pluginId: string; sessionKey: string; tag?: string; diff --git a/src/plugins/host-hook-state.ts b/src/plugins/host-hook-state.ts index 0ab18219c68d..6d69108d71ab 100644 --- a/src/plugins/host-hook-state.ts +++ b/src/plugins/host-hook-state.ts @@ -8,7 +8,6 @@ import { } from "../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -export { clearPluginOwnedSessionState } from "./host-hook-cleanup.js"; import { buildPluginAgentTurnPrepareContext, isPluginJsonValue, @@ -185,7 +184,7 @@ export async function enqueuePluginNextTurnInjection(params: { return { ...updated.result, sessionKey: updated.canonicalKey }; } -export async function drainPluginNextTurnInjections(params: { +async function drainPluginNextTurnInjections(params: { cfg: OpenClawConfig; sessionKey?: string; now?: number; diff --git a/src/plugins/host-hooks.ts b/src/plugins/host-hooks.ts index 4524c2c99daa..68fd036420f6 100644 --- a/src/plugins/host-hooks.ts +++ b/src/plugins/host-hooks.ts @@ -14,7 +14,7 @@ import type { } from "./host-hook-turn-types.js"; export { isPluginJsonValue } from "./host-hook-json.js"; -export type { PluginJsonPrimitive, PluginJsonValue } from "./host-hook-json.js"; +export type { PluginJsonValue } from "./host-hook-json.js"; export type { PluginAgentTurnPrepareEvent, PluginAgentTurnPrepareResult, @@ -22,14 +22,13 @@ export type { PluginHeartbeatPromptContributionResult, PluginNextTurnInjection, PluginNextTurnInjectionEnqueueResult, - PluginNextTurnInjectionPlacement, PluginNextTurnInjectionRecord, } from "./host-hook-turn-types.js"; /** Reason passed to plugin cleanup callbacks when host-owned state changes. */ export type PluginHostCleanupReason = "disable" | "reset" | "delete" | "restart"; -export type PluginSessionExtensionProjectionContext = { +type PluginSessionExtensionProjectionContext = { sessionKey: string; sessionId?: string; state: PluginJsonValue | undefined; @@ -66,7 +65,7 @@ export type PluginSessionExtensionProjection = { value: PluginJsonValue; }; -export type PluginToolPolicyDecision = +type PluginToolPolicyDecision = | PluginHookBeforeToolCallResult | { allow?: boolean; @@ -90,7 +89,7 @@ export type PluginToolMetadataRegistration = { tags?: string[]; }; -export type PluginControlUiTabGroup = "control" | "agent"; +type PluginControlUiTabGroup = "control" | "agent"; export type PluginControlUiDescriptor = { id: string; @@ -218,7 +217,7 @@ export type PluginSessionSchedulerJobHandle = { kind: string; }; -export type PluginSessionAttachmentFile = { +type PluginSessionAttachmentFile = { path: string; }; diff --git a/src/plugins/host-tool-param-parsers.ts b/src/plugins/host-tool-param-parsers.ts index f69013f5bfae..74085143a848 100644 --- a/src/plugins/host-tool-param-parsers.ts +++ b/src/plugins/host-tool-param-parsers.ts @@ -14,12 +14,12 @@ import { * rewrites params. Fields are optional and additive: a missing field means * derivation produced nothing usable, never that it failed loudly. */ -export type HostToolDerivedParams = { +type HostToolDerivedParams = { /** Best-effort destination path hints the tool may read or write, when discoverable. */ derivedPaths?: readonly string[]; }; -export type HostToolDerivationOptions = ApplyPatchPathExtractionOptions; +type HostToolDerivationOptions = ApplyPatchPathExtractionOptions; /** * Per-tool host-owned param derivers. Keep this map small and focused — every diff --git a/src/plugins/http-registry.test.ts b/src/plugins/http-registry.test.ts index fc4edaee84d0..1d3420b14e29 100644 --- a/src/plugins/http-registry.test.ts +++ b/src/plugins/http-registry.test.ts @@ -11,7 +11,7 @@ import { setActivePluginRegistry, } from "./runtime.js"; import type { PluginRuntime } from "./runtime/types.js"; -import { createPluginRecord } from "./status.test-helpers.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; function expectRouteRegistrationDenied(params: { replaceExisting: boolean; diff --git a/src/plugins/install-channel-specs.ts b/src/plugins/install-channel-specs.ts index caa89a1b95c9..86459aefa047 100644 --- a/src/plugins/install-channel-specs.ts +++ b/src/plugins/install-channel-specs.ts @@ -3,7 +3,7 @@ import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js"; import { isExactSemverVersion, parseRegistryNpmSpec } from "../infra/npm-registry-spec.js"; import type { UpdateChannel } from "../infra/update-channels.js"; -export type ChannelInstallSpecs = { +type ChannelInstallSpecs = { installSpec: string; recordSpec: string; fallbackSpec?: string; diff --git a/src/plugins/install-policy-context.ts b/src/plugins/install-policy-context.ts index 976c9fb07c9e..53deae3021a0 100644 --- a/src/plugins/install-policy-context.ts +++ b/src/plugins/install-policy-context.ts @@ -16,7 +16,7 @@ import type { * Keep all payload shaping here so partner feedback lands in one place instead * of drifting across individual install codepaths. */ -export type BeforeInstallHookPayloadParams = { +type BeforeInstallHookPayloadParams = { targetType: PluginInstallTargetType; targetName: string; origin?: string; diff --git a/src/plugins/install-security-scan.ts b/src/plugins/install-security-scan.ts index ec21e14ef9bb..f3e860687ac5 100644 --- a/src/plugins/install-security-scan.ts +++ b/src/plugins/install-security-scan.ts @@ -21,7 +21,7 @@ export type InstallSecurityScanResult = { }; /** Plugin install request kinds that share install policy without skill install semantics. */ -export type PluginInstallRequestKind = Exclude; +type PluginInstallRequestKind = Exclude; /** Skill install metadata shape passed into shared install policy evaluation. */ export type SkillInstallSpecMetadata = { @@ -41,7 +41,7 @@ export type SkillInstallSpecMetadata = { }; /** Package executable metadata used to scope dependency and entrypoint scans. */ -export type PackageExecutableScanMetadata = { +type PackageExecutableScanMetadata = { runtimeExtensions?: readonly string[]; runtimeSetupEntry?: string; setupEntry?: string; diff --git a/src/plugins/install-source-info.ts b/src/plugins/install-source-info.ts index 4703910d6f68..ea08a611648d 100644 --- a/src/plugins/install-source-info.ts +++ b/src/plugins/install-source-info.ts @@ -5,7 +5,7 @@ import { parseRegistryNpmSpec, type ParsedRegistryNpmSpec } from "../infra/npm-r import type { PluginPackageInstall } from "./manifest.js"; /** Warning emitted while describing plugin package install source metadata. */ -export type PluginInstallSourceWarning = +type PluginInstallSourceWarning = | "invalid-clawhub-spec" | "invalid-npm-spec" | "invalid-default-choice" @@ -17,14 +17,14 @@ export type PluginInstallSourceWarning = | "npm-spec-package-name-mismatch"; /** Pinning state for npm plugin install metadata. */ -export type PluginInstallNpmPinState = +type PluginInstallNpmPinState = | "exact-with-integrity" | "exact-without-integrity" | "floating-with-integrity" | "floating-without-integrity"; /** Parsed npm install source metadata for a plugin package. */ -export type PluginInstallNpmSourceInfo = { +type PluginInstallNpmSourceInfo = { spec: string; packageName: string; expectedPackageName?: string; @@ -36,12 +36,12 @@ export type PluginInstallNpmSourceInfo = { }; /** Parsed local install source metadata for a plugin package. */ -export type PluginInstallLocalSourceInfo = { +type PluginInstallLocalSourceInfo = { path: string; }; /** Parsed ClawHub install source metadata for a plugin package. */ -export type PluginInstallClawHubSourceInfo = { +type PluginInstallClawHubSourceInfo = { spec: string; packageName: string; version?: string; @@ -58,7 +58,7 @@ export type PluginInstallSourceInfo = { }; /** Options for describing expected plugin install source metadata. */ -export type DescribePluginInstallSourceOptions = { +type DescribePluginInstallSourceOptions = { expectedPackageName?: string | null; }; diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index b8ef47a67842..7edcb7e68680 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -21,10 +21,10 @@ import { import * as installSecurityScan from "./install-security-scan.js"; import { installPluginFromArchive, - installPluginFromDir, installPluginFromInstalledPackageDir, installPluginFromNpmPackArchive, installPluginFromNpmSpec, + installPluginFromPath, PLUGIN_INSTALL_ERROR_CODE, resolvePluginInstallDir, } from "./install.js"; @@ -36,6 +36,14 @@ import { createDualFormatInstallFixtureFactory, } from "./test-helpers/install-fixtures.js"; +type InstallPluginFromDirParams = Omit[0], "path"> & { + dirPath: string; +}; + +async function installPluginFromDir({ dirPath, ...params }: InstallPluginFromDirParams) { + return await installPluginFromPath({ path: dirPath, ...params }); +} + vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: vi.fn(), })); @@ -1932,49 +1940,6 @@ describe("installPluginFromArchive", () => { expectWarningExcludes(warnings, "dangerous code pattern"); }); - it("forwards policy config and source metadata to bundle scans", async () => { - const scanSpy = vi.spyOn(installSecurityScan, "scanBundleInstallSource"); - const { pluginDir, extensionsDir } = setupBundleInstallFixture({ - bundleFormat: "codex", - name: "Policy Source Bundle", - }); - const config: OpenClawConfig = { - security: { - installPolicy: { - enabled: false, - }, - }, - }; - const source = { - kind: "clawhub", - authority: "openclaw", - mutable: false, - network: true, - } as const; - - try { - const result = await installPluginFromDir({ - dirPath: pluginDir, - extensionsDir, - config, - installPolicyRequest: { - kind: "plugin-archive", - requestedSpecifier: "clawhub:policy-source-bundle", - source, - }, - }); - - expect(result.ok).toBe(true); - const scanParams = scanSpy.mock.calls.at(-1)?.[0]; - expect(scanParams?.config).toBe(config); - expect(scanParams?.requestKind).toBe("plugin-archive"); - expect(scanParams?.requestedSpecifier).toBe("clawhub:policy-source-bundle"); - expect(scanParams?.source).toEqual(source); - } finally { - scanSpy.mockRestore(); - } - }); - it("blocks bundle installs with denied vendored dependency names", async () => { const { pluginDir, extensionsDir } = setupBundleInstallFixture({ bundleFormat: "codex", diff --git a/src/plugins/install.ts b/src/plugins/install.ts index d2e76b3bdb63..e4d7ed64b8fb 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -150,7 +150,7 @@ export const PLUGIN_INSTALL_ERROR_CODE = { UNSUPPORTED_PLAIN_FILE_PLUGIN: "unsupported_plain_file_plugin", } as const; -export type PluginInstallErrorCode = +type PluginInstallErrorCode = (typeof PLUGIN_INSTALL_ERROR_CODE)[keyof typeof PLUGIN_INSTALL_ERROR_CODE]; export type InstallPluginResult = @@ -251,7 +251,7 @@ async function readOptionalPackageManifest(params: { } } -export type PluginNpmIntegrityDriftParams = { +type PluginNpmIntegrityDriftParams = { spec: string; expectedIntegrity: string; actualIntegrity: string; @@ -2827,7 +2827,7 @@ export async function installPluginFromArchive( return result; } -export async function installPluginFromDir( +async function installPluginFromDir( params: { dirPath: string; } & PackageInstallCommonParams, diff --git a/src/plugins/installed-plugin-index-record-cache.ts b/src/plugins/installed-plugin-index-record-cache.ts index 76416e791dc7..307906264e99 100644 --- a/src/plugins/installed-plugin-index-record-cache.ts +++ b/src/plugins/installed-plugin-index-record-cache.ts @@ -2,7 +2,7 @@ import type { PluginInstallRecord } from "../config/types.plugins.js"; /** Cached installed plugin records for one store/recovery key. */ -export type InstallRecordsCacheEntry = { +type InstallRecordsCacheEntry = { records: Record; }; diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index 24419f64916f..fdbb6b170deb 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -40,7 +40,7 @@ export { } from "./installed-plugin-index-store-path.js"; /** Freshness state for the persisted installed plugin index. */ -export type InstalledPluginIndexStoreState = "missing" | "fresh" | "stale"; +type InstalledPluginIndexStoreState = "missing" | "fresh" | "stale"; export type InstalledPluginIndexStoreInspection = { state: InstalledPluginIndexStoreState; diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 74fbff2cad84..e88e7ad1c4ee 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -11,10 +11,7 @@ import { } from "./installed-plugin-index-records.js"; import { diffInstalledPluginIndexInvalidationReasons, - getInstalledPluginRecord, isInstalledPluginEnabled, - listEnabledInstalledPluginRecords, - listInstalledPluginRecords, loadInstalledPluginIndex, refreshInstalledPluginIndex, } from "./installed-plugin-index.js"; @@ -579,63 +576,6 @@ describe("installed plugin index", () => { }, ); - it("exposes cold registry records for existing plugins without plugin runtimes", () => { - const fixture = createRichPluginFixture(); - const index = loadInstalledPluginIndex({ - candidates: [fixture.candidate], - env: hermeticEnv(), - }); - - expect(listInstalledPluginRecords(index).map((plugin) => plugin.pluginId)).toEqual(["demo"]); - expect(listEnabledInstalledPluginRecords(index).map((plugin) => plugin.pluginId)).toEqual([ - "demo", - ]); - const record = getInstalledPluginRecord(index, "demo"); - expectRecordFields(requireRecord(record, "installed plugin record"), { - pluginId: "demo", - enabled: true, - }); - expect(record?.installRecord).toBeUndefined(); - expect(isInstalledPluginEnabled(index, "demo")).toBe(true); - }); - - it("keeps disabled plugins in inventory while excluding them from cold owner resolution", () => { - const fixture = createRichPluginFixture(); - const index = loadInstalledPluginIndex({ - candidates: [fixture.candidate], - config: { - plugins: { - entries: { - demo: { - enabled: false, - }, - }, - }, - }, - env: hermeticEnv(), - }); - - expect(listInstalledPluginRecords(index).map((plugin) => plugin.pluginId)).toEqual(["demo"]); - const config = { - plugins: { - entries: { - demo: { - enabled: false, - }, - }, - }, - }; - expect(listEnabledInstalledPluginRecords(index, config)).toStrictEqual([]); - expectRecordFields( - requireRecord(getInstalledPluginRecord(index, "demo"), "installed plugin record"), - { - pluginId: "demo", - enabled: false, - }, - ); - expect(isInstalledPluginEnabled(index, "demo", config)).toBe(false); - }); - it("keeps an index-disabled plugin disabled when config only enables another plugin", () => { const enabledFixture = createRichPluginFixture({ id: "enabled-demo" }); const disabledFixture = createRichPluginFixture({ id: "disabled-demo" }); @@ -669,43 +609,6 @@ describe("installed plugin index", () => { ).toBe(false); }); - it("uses runtime plugin id normalization for legacy enablement aliases", () => { - const rootDir = makeTempDir(); - writeRuntimeEntry(rootDir); - writePluginManifest(rootDir, { - id: "openai", - configSchema: { type: "object" }, - providers: ["openai"], - }); - - const config = { - plugins: { - entries: { - openai: { - enabled: false, - }, - }, - }, - }; - const index = loadInstalledPluginIndex({ - candidates: [ - createPluginCandidate({ - rootDir, - idHint: "openai", - origin: "bundled", - }), - ], - config, - env: hermeticEnv(), - }); - - expectRecordFields(requireRecord(index.plugins[0], "installed plugin record"), { - pluginId: "openai", - enabled: false, - }); - expect(listEnabledInstalledPluginRecords(index, config)).toStrictEqual([]); - }); - it("records explicit install records separately from package install intent", () => { const fixture = createRichPluginFixture(); diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 790b8406cb54..c1a23fcc8a39 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -33,17 +33,12 @@ export type { InstalledPluginIndexRecord, InstalledPluginIndexRefreshReason, InstalledPluginInstallRecordInfo, - InstalledPluginPackageChannelInfo, - InstalledPluginStartupInfo, LoadInstalledPluginIndexParams, RefreshInstalledPluginIndexParams, } from "./installed-plugin-index-types.js"; export { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index-install-records.js"; export { diffInstalledPluginIndexInvalidationReasons } from "./installed-plugin-index-invalidation.js"; -export { - CONFIG_PATH_ACTIVATION_COMPAT_CODE, - hasMissingConfigPathActivationMetadata, -} from "./installed-plugin-index-config-path-scope.js"; +export { hasMissingConfigPathActivationMetadata } from "./installed-plugin-index-config-path-scope.js"; export { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; function buildInstalledPluginIndex( @@ -106,22 +101,6 @@ export function refreshInstalledPluginIndex( return buildInstalledPluginIndex({ ...params, refreshReason: params.reason }).index; } -export function listInstalledPluginRecords( - index: InstalledPluginIndex, -): readonly InstalledPluginIndexRecord[] { - return index.plugins; -} - -export function listEnabledInstalledPluginRecords( - index: InstalledPluginIndex, - config?: OpenClawConfig, -): readonly InstalledPluginIndexRecord[] { - if (!config) { - return index.plugins.filter((plugin) => plugin.enabled); - } - return index.plugins.filter((plugin) => isInstalledPluginEnabled(index, plugin.pluginId, config)); -} - export function getInstalledPluginRecord( index: InstalledPluginIndex, pluginId: string, diff --git a/src/plugins/interactive-registry.test-fixtures.ts b/src/plugins/interactive-registry.test-fixtures.ts new file mode 100644 index 000000000000..0ce959541dac --- /dev/null +++ b/src/plugins/interactive-registry.test-fixtures.ts @@ -0,0 +1,6 @@ +/** Test-only reset for registry-owned interactive handler snapshots. */ +import { clearPluginInteractiveHandlerRegistrationsState } from "./interactive-state.js"; + +export function clearPluginInteractiveHandlerRegistrations(): void { + clearPluginInteractiveHandlerRegistrationsState(); +} diff --git a/src/plugins/interactive-registry.ts b/src/plugins/interactive-registry.ts index fd23f07b060d..e2c1e5aa7186 100644 --- a/src/plugins/interactive-registry.ts +++ b/src/plugins/interactive-registry.ts @@ -110,7 +110,7 @@ export function clearPluginInteractiveHandlers(): void { } /** Clears stored plugin interactive handler registrations. */ -export function clearPluginInteractiveHandlerRegistrations(): void { +function clearPluginInteractiveHandlerRegistrations(): void { clearPluginInteractiveHandlerRegistrationsState(); } diff --git a/src/plugins/legacy-npm-declaration.ts b/src/plugins/legacy-npm-declaration.ts index f8133097b84c..3e2fd82d85e9 100644 --- a/src/plugins/legacy-npm-declaration.ts +++ b/src/plugins/legacy-npm-declaration.ts @@ -9,7 +9,7 @@ import { validatePluginId } from "./install-paths.js"; const LEGACY_NPM_DECLARATION_FILE = "openclaw.extension.json"; /** Parsed legacy npm declaration stored beside an installed plugin. */ -export type LegacyNpmPluginDeclaration = { +type LegacyNpmPluginDeclaration = { pluginId: string; npmSpec: string; source: string; diff --git a/src/plugins/loader-cache-instances.ts b/src/plugins/loader-cache-instances.ts new file mode 100644 index 000000000000..65aeca3be838 --- /dev/null +++ b/src/plugins/loader-cache-instances.ts @@ -0,0 +1,15 @@ +import { PluginLoaderCacheState } from "./loader-cache-state.js"; +import type { PluginProcessGlobalState } from "./plugin-registration-transaction.js"; +import type { PluginRegistry } from "./registry-types.js"; + +export type CachedPluginState = { + registry: PluginRegistry; + processGlobalState: PluginProcessGlobalState; +}; + +const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128; + +export const pluginLoaderCacheInstances = { + scoped: new PluginLoaderCacheState(MAX_PLUGIN_REGISTRY_CACHE_ENTRIES), + fullWorkspace: new PluginLoaderCacheState(MAX_PLUGIN_REGISTRY_CACHE_ENTRIES), +}; diff --git a/src/plugins/loader-cache-state.test.ts b/src/plugins/loader-cache-state.test.ts index a3baca4b8a2c..6ccce703899e 100644 --- a/src/plugins/loader-cache-state.test.ts +++ b/src/plugins/loader-cache-state.test.ts @@ -1,6 +1,6 @@ /** Tests plugin loader cache state keys, invalidation, and reset behavior. */ import { describe, expect, it } from "vitest"; -import { PluginLoaderCacheState, PluginLoadReentryError } from "./loader-cache-state.js"; +import { PluginLoaderCacheState } from "./loader-cache-state.js"; describe("PluginLoaderCacheState", () => { it("evicts the least recently used registry cache entry", () => { @@ -18,17 +18,6 @@ describe("PluginLoaderCacheState", () => { expect(cache.get("c")).toBe("charlie"); }); - it("tracks in-flight loads and reports reentry by cache key", () => { - const cache = new PluginLoaderCacheState(2); - - cache.beginLoad("demo"); - expect(cache.isLoadInFlight("demo")).toBe(true); - expect(() => cache.beginLoad("demo")).toThrow(PluginLoadReentryError); - - cache.finishLoad("demo"); - expect(cache.isLoadInFlight("demo")).toBe(false); - }); - it("clears registry, in-flight, and warning state together", () => { const cache = new PluginLoaderCacheState(2); diff --git a/src/plugins/loader-cache-state.ts b/src/plugins/loader-cache-state.ts index 197b14330dfe..bd176af8fcdb 100644 --- a/src/plugins/loader-cache-state.ts +++ b/src/plugins/loader-cache-state.ts @@ -2,7 +2,7 @@ import { PluginLruCache } from "./plugin-cache-primitives.js"; /** Error thrown when one plugin registry cache key attempts nested loading. */ -export class PluginLoadReentryError extends Error { +class PluginLoadReentryError extends Error { readonly cacheKey: string; constructor(cacheKey: string) { diff --git a/src/plugins/loader-channel-setup.ts b/src/plugins/loader-channel-setup.ts index 425be53b41ad..66bf96b930a0 100644 --- a/src/plugins/loader-channel-setup.ts +++ b/src/plugins/loader-channel-setup.ts @@ -51,7 +51,7 @@ export function mergeSetupRuntimeChannelPlugin( } as ChannelPlugin; } -export type BundledRuntimeChannelRegistration = { +type BundledRuntimeChannelRegistration = { id?: string; loadChannelPlugin?: () => ChannelPlugin; loadChannelSecrets?: () => ChannelPlugin["secrets"] | undefined; diff --git a/src/plugins/loader-provenance.ts b/src/plugins/loader-provenance.ts index 159f2e25bdd7..8392bb747e88 100644 --- a/src/plugins/loader-provenance.ts +++ b/src/plugins/loader-provenance.ts @@ -22,7 +22,7 @@ type InstallTrackingRule = { }; /** Provenance lookup for trusted plugin load paths and install records. */ -export type PluginProvenanceIndex = { +type PluginProvenanceIndex = { loadPathMatcher: PathMatcher; installRules: Map; }; diff --git a/src/plugins/loader.prefer-over.test.ts b/src/plugins/loader.prefer-over.test.ts index c191736352f1..9737f9b6c0f3 100644 --- a/src/plugins/loader.prefer-over.test.ts +++ b/src/plugins/loader.prefer-over.test.ts @@ -4,7 +4,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; -import { clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.test-fixtures.js"; import { resetPluginRuntimeStateForTest } from "./runtime.js"; const tempDirs: string[] = []; diff --git a/src/plugins/loader.runtime-registry.test.ts b/src/plugins/loader.runtime-registry.test.ts index c807e41d057c..fd3111522089 100644 --- a/src/plugins/loader.runtime-registry.test.ts +++ b/src/plugins/loader.runtime-registry.test.ts @@ -1,13 +1,6 @@ // Verifies plugin loader runtime registry behavior. -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { getCompactionProvider, registerCompactionProvider } from "./compaction-provider.js"; -import { getEmbeddingProvider, registerEmbeddingProvider } from "./embedding-providers.js"; import { - testing, - clearPluginLoaderCache, clearPluginRegistryLoadCache, loadOpenClawPlugins, resolveRuntimePluginRegistry, @@ -17,69 +10,14 @@ import { getMemoryEmbeddingProvider, registerMemoryEmbeddingProvider, } from "./memory-embedding-providers.js"; -import { - buildMemoryPromptSection, - getMemoryRuntime, - listMemoryCorpusSupplements, - registerMemoryCapability, - registerMemoryCorpusSupplement, - registerMemoryPromptSupplement, - resolveMemoryFlushPlan, -} from "./memory-state.js"; -import type { PluginRecord } from "./registry-types.js"; +import { buildMemoryPromptSection, registerMemoryCapability } from "./memory-state.js"; import { createEmptyPluginRegistry } from "./registry.js"; import { setActivePluginRegistry } from "./runtime.js"; -import type { CreatePluginRuntimeOptions } from "./runtime/index.js"; afterEach(() => { resetPluginLoaderTestStateForTest(); }); -function createLoadedPluginRecord(id: string): PluginRecord { - return { - id, - name: id, - source: "test", - origin: "workspace", - enabled: true, - status: "loaded", - toolNames: [], - hookNames: [], - channelIds: [], - cliBackendIds: [], - providerIds: [], - embeddingProviderIds: [], - speechProviderIds: [], - realtimeTranscriptionProviderIds: [], - realtimeVoiceProviderIds: [], - mediaUnderstandingProviderIds: [], - transcriptSourceProviderIds: [], - imageGenerationProviderIds: [], - videoGenerationProviderIds: [], - musicGenerationProviderIds: [], - webFetchProviderIds: [], - webSearchProviderIds: [], - migrationProviderIds: [], - memoryEmbeddingProviderIds: [], - agentHarnessIds: [], - cliCommands: [], - services: [], - gatewayDiscoveryServiceIds: [], - commands: [], - httpRoutes: 0, - hookCount: 0, - configSchema: false, - }; -} - -function requireMemoryRuntime() { - const runtime = getMemoryRuntime(); - if (!runtime) { - throw new Error("expected memory runtime registration"); - } - return runtime; -} - function requireMemoryEmbeddingProvider(providerId: string) { const provider = getMemoryEmbeddingProvider(providerId); if (!provider) { @@ -88,730 +26,13 @@ function requireMemoryEmbeddingProvider(providerId: string) { return provider; } -function makeOpenClawDevSourceRoot(): string { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-loader-dev-source-")); - fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" }), "utf-8"); - fs.mkdirSync(path.join(root, "src"), { recursive: true }); - fs.mkdirSync(path.join(root, "extensions"), { recursive: true }); - return root; -} - -describe("getCompatibleActivePluginRegistry", () => { - it("reuses the active registry only when the load context cache key matches", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect(testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - workspaceDir: "/tmp/workspace-b", - }), - ).toBeUndefined(); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - onlyPluginIds: ["demo"], - }), - ).toBeUndefined(); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - onlyPluginIds: [], - }), - ).toBeUndefined(); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - runtimeOptions: undefined, - }), - ).toBe(registry); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - runtimeOptions: { - subagent: {} as CreatePluginRuntimeOptions["subagent"], - }, - }), - ).toBeUndefined(); - }); - - it("does not treat a default-mode active registry as compatible with gateway binding", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "default"); - - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }), - ).toBeUndefined(); - }); - - it("reuses an active full registry for compatible tool-discovery loads", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "default"); - - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - activate: false, - toolDiscovery: true, - }), - ).toBe(registry); - }); - - it("reuses an active wider registry for compatible scoped runtime loads", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push(createLoadedPluginRecord("demo"), createLoadedPluginRecord("other")); - const loadOptions = { - config: { - plugins: { - allow: ["demo", "other"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - onlyPluginIds: ["demo"], - }), - ).toBe(registry); - }); - - it("does not reuse a wider registry for scoped loads when the load context changes", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push(createLoadedPluginRecord("demo"), createLoadedPluginRecord("other")); - const loadOptions = { - config: { - plugins: { - allow: ["demo", "other"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - workspaceDir: "/tmp/workspace-b", - onlyPluginIds: ["demo"], - }), - ).toBeUndefined(); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/changed.js"] }, - }, - }, - onlyPluginIds: ["demo"], - }), - ).toBeUndefined(); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - onlyPluginIds: ["missing"], - }), - ).toBeUndefined(); - }); - - it("does not reuse a default-mode active registry for gateway-bindable tool discovery", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey, "default"); - - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - activate: false, - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - toolDiscovery: true, - }), - ).toBeUndefined(); - }); - - it("does not embed activation secrets in the loader cache key", () => { - const { cacheKey } = testing.resolvePluginLoadCacheContext({ - config: { - plugins: { - allow: ["telegram"], - }, - }, - activationSourceConfig: { - plugins: { - allow: ["telegram"], - }, - channels: { - telegram: { - enabled: true, - botToken: "secret-token", - }, - }, - }, - autoEnabledReasons: { - telegram: ["telegram configured"], - }, - }); - - expect(cacheKey).not.toContain("secret-token"); - expect(cacheKey).not.toContain("botToken"); - expect(cacheKey).not.toContain("telegram configured"); - }); - - it("separates dev source root precedence in the loader cache key", () => { - const devSourceRoot = makeOpenClawDevSourceRoot(); - try { - const baseOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - env: { ...process.env, OPENCLAW_DEV_SOURCE_ROOT: undefined }, - }; - - const base = testing.resolvePluginLoadCacheContext(baseOptions).cacheKey; - const dev = testing.resolvePluginLoadCacheContext({ - ...baseOptions, - env: { ...process.env, OPENCLAW_DEV_SOURCE_ROOT: devSourceRoot }, - }).cacheKey; - - expect(dev).not.toBe(base); - } finally { - fs.rmSync(devSourceRoot, { recursive: true, force: true }); - } - }); - - it("separates raw env substitution mode in the loader cache key", () => { - const baseOptions = { - config: { - plugins: { - allow: ["demo"], - entries: { - demo: { config: { apiKey: "${DEMO_KEY}" } }, - }, - }, - }, - }; - - const plain = testing.resolvePluginLoadCacheContext(baseOptions).cacheKey; - const resolving = testing.resolvePluginLoadCacheContext({ - ...baseOptions, - resolveRawConfigEnvVars: true, - }).cacheKey; - - expect(resolving).not.toBe(plain); - }); - - it("does not embed raw resolved plugin config env values in the loader cache key", () => { - const { cacheKey } = testing.resolvePluginLoadCacheContext({ - config: { - plugins: { - allow: ["demo"], - entries: { - demo: { config: { apiKey: "${DEMO_KEY}" } }, - }, - }, - }, - env: { ...process.env, DEMO_KEY: "resolved-demo-secret" }, - resolveRawConfigEnvVars: true, - }); - - expect(cacheKey).not.toContain("resolved-demo-secret"); - expect(cacheKey).not.toContain("apiKey"); - }); - - it("falls back to the current active runtime when no compatibility-shaping inputs are supplied", () => { - const registry = createEmptyPluginRegistry(); - setActivePluginRegistry(registry, "startup-registry"); - - expect(testing.getCompatibleActivePluginRegistry()).toBe(registry); - }); - - it("does not reuse the active registry when core gateway method names differ", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - coreGatewayHandlers: { - "sessions.get": () => undefined, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey); - - expect(testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry); - expect( - testing.getCompatibleActivePluginRegistry({ - ...loadOptions, - coreGatewayHandlers: { - "sessions.get": () => undefined, - "sessions.list": () => undefined, - }, - }), - ).toBeUndefined(); - }); - - it("reuses a scoped gateway-bindable registry for a matching default-mode tool scope", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - const startupOptions = { - config: { - plugins: { - allow: ["acpx", "telegram"], - }, - }, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config: startupOptions.config, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }), - ).toBe(registry); - }); - - it("reuses a scoped gateway-bindable registry for a matching snapshot-mode tool scope", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - const startupOptions = { - config: { - plugins: { - allow: ["acpx", "telegram"], - }, - }, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config: startupOptions.config, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - activate: false, - }), - ).toBe(registry); - }); - - it("does not reuse a scoped registry when the requested tool scope needs another plugin", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - const startupOptions = { - config: { - plugins: { - allow: ["acpx", "telegram", "tavily"], - }, - }, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config: startupOptions.config, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram", "tavily"], - }), - ).toBeUndefined(); - }); - - it("does not treat an unscoped request as compatible with the scoped startup registry", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - const startupOptions = { - config: { - plugins: { - allow: ["acpx", "telegram", "tavily"], - }, - }, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config: startupOptions.config, - workspaceDir: "/tmp/workspace-a", - }), - ).toBeUndefined(); - }); - - it("does not reuse a scoped gateway-bindable registry for an explicit subagent request", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - const startupOptions = { - config: { - plugins: { - allow: ["acpx", "telegram"], - }, - }, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config: startupOptions.config, - workspaceDir: "/tmp/workspace-a", - runtimeOptions: { - subagent: {} as CreatePluginRuntimeOptions["subagent"], - }, - }), - ).toBeUndefined(); - }); - - it("reuses a scoped startup registry when only the request omits gateway methods", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - registry.coreGatewayMethodNames = ["sessions.get", "sessions.list"]; - const config = { - plugins: { - allow: ["acpx", "telegram"], - }, - }; - const startupOptions = { - config, - activationSourceConfig: config, - autoEnabledReasons: {}, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - coreGatewayMethodNames: ["sessions.get", "sessions.list"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - }), - ).toBe(registry); - }); - - it("reuses a scoped gateway startup registry when dispatch omits built artifact preference", () => { - const registry = createEmptyPluginRegistry(); - registry.plugins.push( - { id: "acpx" } as (typeof registry.plugins)[number], - { id: "telegram" } as (typeof registry.plugins)[number], - ); - registry.coreGatewayMethodNames = ["sessions.get", "sessions.list"]; - const config = { - plugins: { - allow: ["acpx", "telegram"], - }, - }; - const startupOptions = { - config, - activationSourceConfig: config, - autoEnabledReasons: {}, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - coreGatewayMethodNames: ["sessions.get", "sessions.list"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - preferBuiltPluginArtifacts: true, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions); - setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); - - expect( - testing.getCompatibleActivePluginRegistry({ - config, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }), - ).toBe(registry); - - expect( - testing.getCompatibleActivePluginRegistry({ - config: { - plugins: { - allow: ["acpx", "telegram"], - load: { paths: ["/tmp/changed.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - onlyPluginIds: ["acpx", "telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - }), - ).toBeUndefined(); - }); -}); - describe("resolveRuntimePluginRegistry", () => { - it("reuses the compatible active registry before attempting a fresh load", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - }, - }, - workspaceDir: "/tmp/workspace-a", - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey); - - expect(resolveRuntimePluginRegistry(loadOptions)).toBe(registry); - }); - it("falls back to the current active runtime when no explicit load context is provided", () => { const registry = createEmptyPluginRegistry(); setActivePluginRegistry(registry, "startup-registry"); expect(resolveRuntimePluginRegistry()).toBe(registry); }); - - it("does not treat an explicit empty plugin scope as the active runtime", () => { - const registry = createEmptyPluginRegistry(); - const loadOptions = { - config: { - plugins: { - allow: ["demo"], - load: { paths: ["/tmp/demo.js"] }, - }, - }, - workspaceDir: "/tmp/workspace-a", - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey); - - const scopedEmpty = resolveRuntimePluginRegistry({ ...loadOptions, onlyPluginIds: [] }); - expect(scopedEmpty).not.toBe(registry); - expect(scopedEmpty?.plugins).toStrictEqual([]); - }); - - it("keeps the full workspace registry warm when scoped cron registries churn", () => { - testing.setMaxPluginRegistryCacheEntriesForTest(2); - try { - const loadOptions = { - config: { - plugins: { - allow: ["alpha", "bravo", "charlie"], - }, - }, - workspaceDir: "/tmp/workspace-a", - }; - const fullRegistry = loadOpenClawPlugins(loadOptions); - - loadOpenClawPlugins({ ...loadOptions, onlyPluginIds: ["alpha"] }); - loadOpenClawPlugins({ ...loadOptions, onlyPluginIds: ["bravo"] }); - - expect(resolveRuntimePluginRegistry(loadOptions)).toBe(fullRegistry); - } finally { - testing.setMaxPluginRegistryCacheEntriesForTest(); - } - }); -}); - -describe("clearPluginLoaderCache", () => { - it("resets registered memory plugin registries", () => { - registerEmbeddingProvider({ - id: "stale-embedding", - create: async () => ({ provider: null }), - }); - registerMemoryEmbeddingProvider({ - id: "stale", - create: async () => ({ provider: null }), - }); - registerMemoryCorpusSupplement("memory-wiki", { - search: async () => [], - get: async () => null, - }); - registerMemoryPromptSupplement("memory-wiki", () => ["stale wiki supplement"]); - registerMemoryCapability("memory-core", { - promptBuilder: () => ["stale memory section"], - flushPlanResolver: () => ({ - softThresholdTokens: 1, - forceFlushTranscriptBytes: 2, - reserveTokensFloor: 3, - prompt: "stale", - systemPrompt: "stale", - relativePath: "memory/stale.md", - }), - runtime: { - async getMemorySearchManager() { - return { manager: null }; - }, - resolveMemoryBackendConfig() { - return { backend: "builtin" as const }; - }, - }, - }); - expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([ - "stale memory section", - "stale wiki supplement", - ]); - expect(listMemoryCorpusSupplements()).toHaveLength(1); - expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/stale.md"); - expect( - requireMemoryRuntime().resolveMemoryBackendConfig({ cfg: {} as never, agentId: "main" }), - ).toEqual({ backend: "builtin" }); - expect(getEmbeddingProvider("stale-embedding")?.id).toBe("stale-embedding"); - expect(requireMemoryEmbeddingProvider("stale").id).toBe("stale"); - - clearPluginLoaderCache(); - - expect(getEmbeddingProvider("stale-embedding")).toBeUndefined(); - expect(buildMemoryPromptSection({ availableTools: new Set() })).toStrictEqual([]); - expect(listMemoryCorpusSupplements()).toStrictEqual([]); - expect(resolveMemoryFlushPlan({})).toBeNull(); - expect(getMemoryRuntime()).toBeUndefined(); - expect(getMemoryEmbeddingProvider("stale")).toBeUndefined(); - }); -}); - -describe("loadOpenClawPlugins active runtime clearing", () => { - it("clears plugin-owned global providers before activating a new registry", () => { - registerEmbeddingProvider({ - id: "stale-embedding", - create: async () => ({ provider: null }), - }); - registerCompactionProvider({ - id: "stale-compaction", - label: "Stale Compaction", - summarize: async () => "stale", - }); - registerMemoryEmbeddingProvider({ - id: "stale-memory", - create: async () => ({ provider: null }), - }); - - loadOpenClawPlugins({ onlyPluginIds: [] }); - - expect(getEmbeddingProvider("stale-embedding")).toBeUndefined(); - expect(getCompactionProvider("stale-compaction")).toBeUndefined(); - expect(getMemoryEmbeddingProvider("stale-memory")).toBeUndefined(); - }); }); describe("clearPluginRegistryLoadCache", () => { diff --git a/src/plugins/loader.test-fixtures.ts b/src/plugins/loader.test-fixtures.ts index b615fb3a908f..df5263ebb0c8 100644 --- a/src/plugins/loader.test-fixtures.ts +++ b/src/plugins/loader.test-fixtures.ts @@ -4,9 +4,12 @@ import os from "node:os"; import path from "node:path"; import { resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js"; import { withEnv } from "../test-utils/env.js"; -import { clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js"; +import { pluginLoaderCacheInstances } from "./loader-cache-instances.js"; +import { clearActivatedPluginRuntimeState, loadOpenClawPlugins } from "./loader.js"; import { resetPluginRuntimeStateForTest } from "./runtime.js"; +export { loadOpenClawPlugins }; + export type TempPlugin = { dir: string; file: string; id: string }; export type PluginLoadConfig = NonNullable[0]>["config"]; export type PluginRegistry = ReturnType; @@ -154,6 +157,13 @@ export function resetPluginLoaderTestStateForTest() { } } +/** Clears loader state for test isolation without exposing a production-only reset export. */ +export function clearPluginLoaderCache(): void { + pluginLoaderCacheInstances.scoped.clear(); + pluginLoaderCacheInstances.fullWorkspace.clear(); + clearActivatedPluginRuntimeState(); +} + export function cleanupPluginLoaderFixturesForTest() { try { fs.rmSync(fixtureRoot, { recursive: true, force: true }); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index a318908fc8ab..d3b4dfc4b9bb 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -24,6 +24,7 @@ import { resetDiagnosticEventsForTest, waitForDiagnosticEventsDrained, } from "../infra/diagnostic-events.js"; +import { toSafeImportPath } from "../shared/import-specifier.js"; import { clearDetachedTaskLifecycleRuntimeRegistration, getDetachedTaskLifecycleRuntimeRegistration, @@ -31,13 +32,12 @@ import { type DetachedTaskLifecycleRuntime, } from "../tasks/detached-task-runtime-state.js"; import { withEnv } from "../test-utils/env.js"; -import { buildPluginApi } from "./api-builder.js"; import { clearPluginCommands } from "./command-registry-state.js"; import { getPluginCommandSpecs } from "./command-specs.js"; import { getCompactionProvider } from "./compaction-provider.js"; import { - getEmbeddingProvider, - listEmbeddingProviders, + getRegisteredEmbeddingProvider, + listRegisteredEmbeddingProviders, registerEmbeddingProvider, } from "./embedding-providers.js"; import { @@ -48,25 +48,23 @@ import { import { createHookRunner } from "./hooks.js"; import { writePersistedInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js"; import { - clearPluginInteractiveHandlerRegistrations, clearPluginInteractiveHandlers, resolvePluginInteractiveNamespaceMatch, } from "./interactive-registry.js"; +import { clearPluginInteractiveHandlerRegistrations } from "./interactive-registry.test-fixtures.js"; import { claimPluginInteractiveCallbackDedupe, commitPluginInteractiveCallbackDedupe, } from "./interactive-state.js"; import { warnWhenAllowlistIsOpen } from "./loader-provenance.js"; import { - testing, - clearPluginLoaderCache, loadOpenClawPluginCliRegistry, loadOpenClawPlugins, type PluginLoadOptions, - PluginLoadReentryError, resolveRuntimePluginRegistry, } from "./loader.js"; import { + clearPluginLoaderCache, cleanupPluginLoaderFixturesForTest, EMPTY_PLUGIN_SCHEMA, makeTempDir, @@ -95,8 +93,7 @@ import { registerMemoryCorpusSupplement, registerMemoryPromptSupplement, resolveMemoryFlushPlan, -} from "./memory-state.js"; -import { ensureOpenClawPluginSdkAlias } from "./plugin-sdk-dist-alias.js"; +} from "./memory-state.test-fixtures.js"; import { createEmptyPluginRegistry } from "./registry.js"; import { getActivePluginRegistry, @@ -111,6 +108,11 @@ import { ensurePluginRegistryLoaded, } from "./runtime/runtime-registry-loader.js"; import type { PluginSdkResolutionPreference } from "./sdk-alias.js"; + +const getEmbeddingProvider = (id: string) => getRegisteredEmbeddingProvider(id)?.adapter; +const listEmbeddingProviders = () => + listRegisteredEmbeddingProviders().map((entry) => entry.adapter); + let cachedBundledTelegramDir = ""; let cachedBundledMemoryDir = ""; @@ -1460,133 +1462,6 @@ describe("loadOpenClawPlugins", () => { expect(record?.rootDir).toBe(fs.realpathSync.native(plugin.dir)); }); - it("refreshes bundled plugin-sdk aliases without deleting the shared alias directory", () => { - const distRoot = makeTempDir(); - const pluginSdkDir = path.join(distRoot, "plugin-sdk"); - const aliasDir = path.join(distRoot, "extensions", "node_modules", "openclaw", "plugin-sdk"); - mkdirSafe(pluginSdkDir); - mkdirSafe(aliasDir); - fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const value = 1;\n", "utf8"); - fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 1;\n", "utf8"); - fs.writeFileSync(path.join(aliasDir, "sentinel.txt"), "keep\n", "utf8"); - - ensureOpenClawPluginSdkAlias(distRoot); - fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 2;\n", "utf8"); - ensureOpenClawPluginSdkAlias(distRoot); - - expect(fs.existsSync(path.join(aliasDir, "sentinel.txt"))).toBe(true); - expect(fs.readFileSync(path.join(aliasDir, "core.js"), "utf8")).toContain("core.js"); - }); - - it("keeps private local-only plugin-sdk artifacts out of package dist aliases", () => { - const packageRoot = makeTempDir(); - const distRoot = path.join(packageRoot, "dist"); - const pluginSdkDir = path.join(distRoot, "plugin-sdk"); - const aliasRoot = path.join(distRoot, "extensions", "node_modules", "openclaw"); - const aliasDir = path.join(aliasRoot, "plugin-sdk"); - mkdirSafe(pluginSdkDir); - mkdirSafe(aliasDir); - fs.writeFileSync( - path.join(packageRoot, "package.json"), - JSON.stringify( - { - name: "openclaw", - version: "2026.4.22", - type: "module", - exports: { - "./plugin-sdk": "./dist/plugin-sdk/index.js", - "./plugin-sdk/string-coerce-runtime": "./dist/plugin-sdk/string-coerce-runtime.js", - }, - }, - null, - 2, - ), - "utf8", - ); - fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const root = true;\n", "utf8"); - fs.writeFileSync( - path.join(pluginSdkDir, "string-coerce-runtime.js"), - "export const publicRuntime = true;\n", - "utf8", - ); - fs.writeFileSync( - path.join(pluginSdkDir, "ssrf-runtime-internal.js"), - "export const internal = true;\n", - "utf8", - ); - fs.writeFileSync( - path.join(aliasDir, "ssrf-runtime-internal.js"), - "export const staleInternal = true;\n", - "utf8", - ); - - ensureOpenClawPluginSdkAlias(distRoot); - - const aliasPackage = JSON.parse( - fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"), - ) as { exports?: Record }; - expect(aliasPackage.exports).toEqual({ - "./plugin-sdk": "./plugin-sdk/index.js", - "./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js", - }); - expect(fs.existsSync(path.join(aliasDir, "index.js"))).toBe(true); - expect(fs.existsSync(path.join(aliasDir, "string-coerce-runtime.js"))).toBe(true); - expect(fs.existsSync(path.join(aliasDir, "ssrf-runtime-internal.js"))).toBe(false); - }); - - it("keeps private local-only plugin-sdk artifacts out of legacy package dist aliases", () => { - const packageRoot = makeTempDir(); - const distRoot = path.join(packageRoot, "dist"); - const pluginSdkDir = path.join(distRoot, "plugin-sdk"); - const aliasRoot = path.join(distRoot, "extensions", "node_modules", "openclaw"); - const aliasDir = path.join(aliasRoot, "plugin-sdk"); - mkdirSafe(pluginSdkDir); - mkdirSafe(aliasDir); - fs.writeFileSync( - path.join(packageRoot, "package.json"), - JSON.stringify({ name: "openclaw", version: "2026.4.22", type: "module" }, null, 2), - "utf8", - ); - fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const root = true;\n", "utf8"); - fs.writeFileSync( - path.join(pluginSdkDir, "string-coerce-runtime.js"), - "export const publicRuntime = true;\n", - "utf8", - ); - fs.writeFileSync( - path.join(pluginSdkDir, "ssrf-runtime-internal.js"), - "export const internal = true;\n", - "utf8", - ); - fs.writeFileSync( - path.join(aliasDir, "ssrf-runtime-internal.js"), - "export const staleInternal = true;\n", - "utf8", - ); - - ensureOpenClawPluginSdkAlias(distRoot); - - const aliasPackage = JSON.parse( - fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"), - ) as { exports?: Record }; - expect(aliasPackage.exports).toEqual({ - "./plugin-sdk": "./plugin-sdk/index.js", - "./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js", - }); - expect(fs.existsSync(path.join(aliasDir, "index.js"))).toBe(true); - expect(fs.existsSync(path.join(aliasDir, "string-coerce-runtime.js"))).toBe(true); - expect(fs.existsSync(path.join(aliasDir, "ssrf-runtime-internal.js"))).toBe(false); - }); - - it("keeps private QA plugin-sdk filenames out of bundled source markers", () => { - const source = fs.readFileSync(new URL("./plugin-sdk-dist-alias.ts", import.meta.url), "utf8"); - - expect(source).not.toContain("qa-channel.js"); - expect(source).not.toContain("qa-channel-protocol.js"); - expect(source).not.toContain("qa-lab.js"); - expect(source).not.toContain("qa-runtime.js"); - }); - it("disables bundled plugins by default", () => { const bundledDir = makeTempDir(); writePlugin({ @@ -1625,7 +1500,25 @@ describe("loadOpenClawPlugins", () => { "export const normalizeLowercaseStringOrEmpty = (value) => String(value).toLowerCase();\n", "utf-8", ); - ensureOpenClawPluginSdkAlias(path.join(packageRoot, "dist")); + const aliasRoot = path.join(bundledDir, "node_modules", "openclaw"); + const aliasPluginSdkDir = path.join(aliasRoot, "plugin-sdk"); + fs.mkdirSync(aliasPluginSdkDir, { recursive: true }); + fs.writeFileSync( + path.join(aliasRoot, "package.json"), + JSON.stringify({ + name: "openclaw", + type: "module", + exports: { + "./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js", + }, + }), + "utf-8", + ); + fs.writeFileSync( + path.join(aliasPluginSdkDir, "string-coerce-runtime.js"), + "export * from '../../../../plugin-sdk/string-coerce-runtime.js';\n", + "utf-8", + ); fs.writeFileSync( path.join(pluginRoot, "index.js"), [ @@ -2175,98 +2068,6 @@ describe("loadOpenClawPlugins", () => { expect(Object.keys(registry.gatewayHandlers)).not.toContain("async-register.ping"); }, }, - { - label: - "keeps sendSessionAttachment callable after register closes while blocking registration-only APIs", - run: () => { - const registerGatewayMethod = vi.fn(); - const registerSessionExtension = vi.fn(); - const sendSessionAttachment = vi.fn(async () => ({ - ok: true as const, - channel: "proofchat", - deliveredTo: "12345", - count: 1, - })); - const emitAgentEvent = vi.fn(() => ({ - emitted: true as const, - stream: "late-attachment-plugin.workflow", - })); - const api = buildPluginApi({ - id: "late-attachment-plugin", - name: "Late Attachment Plugin", - source: "/tmp/late-attachment-plugin/index.cjs", - registrationMode: "full", - config: {}, - runtime: {} as never, - logger: { - info() {}, - warn() {}, - error() {}, - debug() {}, - }, - resolvePath: (input) => input, - handlers: { - emitAgentEvent, - registerGatewayMethod, - registerSessionExtension, - sendSessionAttachment, - }, - }); - let capturedApi: typeof api | undefined; - - testing.runPluginRegisterSync((guardedApi) => { - capturedApi = guardedApi; - // Host-hook delivery remains callable after registration closes; only registration-only APIs lock. - guardedApi.registerGatewayMethod("proofchat.ping", vi.fn() as never); - }, api); - - expect(registerGatewayMethod).toHaveBeenCalledTimes(1); - expect( - capturedApi?.registerGatewayMethod("proofchat.late-ping", vi.fn() as never), - ).toBeUndefined(); - expect(registerGatewayMethod).toHaveBeenCalledTimes(1); - - const attachmentParams = { - sessionKey: "agent:main:main", - files: [{ path: "./proof-report.txt" }], - text: "attachment ready", - }; - const lateResult = capturedApi?.sendSessionAttachment(attachmentParams); - const lateWorkflowResult = - capturedApi?.session?.workflow.sendSessionAttachment(attachmentParams); - const eventParams = { - runId: "run-late", - stream: "late-attachment-plugin.workflow", - data: { phase: "done" }, - }; - const lateEventResult = capturedApi?.emitAgentEvent(eventParams); - const lateNamespacedEventResult = capturedApi?.agent?.events.emitAgentEvent(eventParams); - capturedApi?.session?.state.registerSessionExtension({ - namespace: "late", - description: "late extension should stay blocked", - }); - - expect(lateResult).toBe(sendSessionAttachment.mock.results[0]?.value); - expect(lateWorkflowResult).toBe(sendSessionAttachment.mock.results[1]?.value); - expect(sendSessionAttachment).toHaveBeenCalledWith({ - sessionKey: "agent:main:main", - files: [{ path: "./proof-report.txt" }], - text: "attachment ready", - }); - expect(sendSessionAttachment).toHaveBeenCalledTimes(2); - expect(lateEventResult).toEqual({ - emitted: true, - stream: "late-attachment-plugin.workflow", - }); - expect(lateNamespacedEventResult).toEqual({ - emitted: true, - stream: "late-attachment-plugin.workflow", - }); - expect(emitAgentEvent).toHaveBeenCalledTimes(2); - expect(emitAgentEvent).toHaveBeenCalledWith(eventParams); - expect(registerSessionExtension).not.toHaveBeenCalled(); - }, - }, { label: "limits imports to the requested plugin ids", run: () => { @@ -2524,7 +2325,7 @@ module.exports = { id: "throws-after-import", register() {} };`, const reentryError = Reflect.get(globalThis, marker) as | { name?: unknown; message?: unknown } | undefined; - expect(reentryError?.name).toBe(PluginLoadReentryError.name); + expect(reentryError?.name).toBe("PluginLoadReentryError"); expect(String(reentryError?.message)).toContain("plugin load reentry detected"); const record = registry.plugins.find((entry) => entry.id === "reentrant-snapshot"); expect(record?.status).toBe("error"); @@ -4816,53 +4617,6 @@ module.exports = { id: "throws-after-import", register() {} };`, expectCacheMissThenHit(setup()); }); - it("evicts least recently used registries when the loader cache exceeds its cap", () => { - useNoBundledPlugins(); - const plugin = writePlugin({ - id: "cache-eviction", - filename: "cache-eviction.cjs", - body: `module.exports = { id: "cache-eviction", register() {} };`, - }); - const previousCacheCap = testing.maxPluginRegistryCacheEntries; - testing.setMaxPluginRegistryCacheEntriesForTest(4); - const stateDirs = Array.from({ length: testing.maxPluginRegistryCacheEntries + 1 }, () => - makeTempDir(), - ); - - const loadWithStateDir = (stateDir: string) => - loadOpenClawPlugins({ - env: { - ...process.env, - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", - }, - config: { - plugins: { - allow: ["cache-eviction"], - load: { - paths: [plugin.file], - }, - }, - }, - }); - - try { - const first = loadWithStateDir(stateDirs[0] ?? makeTempDir()); - const second = loadWithStateDir(stateDirs[1] ?? makeTempDir()); - - expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first); - - for (const stateDir of stateDirs.slice(2)) { - loadWithStateDir(stateDir); - } - - expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first); - expect(loadWithStateDir(stateDirs[1] ?? makeTempDir())).not.toBe(second); - } finally { - testing.setMaxPluginRegistryCacheEntriesForTest(previousCacheCap); - } - }); - it("normalizes bundled plugin env overrides against the provided env", () => { const bundledDir = makeTempDir(); const homeDir = path.dirname(bundledDir); @@ -7352,26 +7106,6 @@ module.exports = { ).toBe("loaded"); }); - it("prefers setupEntry for configured channel loads during startup when opted in", () => { - expect( - testing.shouldLoadChannelPluginInSetupRuntime({ - manifestChannels: ["setup-runtime-preferred-test"], - setupSource: "./setup-entry.cjs", - startupDeferConfiguredChannelFullLoadUntilAfterListen: true, - cfg: { - channels: { - "setup-runtime-preferred-test": { - enabled: true, - token: "configured", - }, - }, - }, - env: {}, - preferSetupRuntimeForChannelPlugins: true, - }), - ).toBe(true); - }); - it("prefers built bundled plugin artifacts over source TS when requested", () => { const repoRoot = makeTempDir(); const sourceDir = path.join(repoRoot, "extensions", "startup-artifact-test"); @@ -9938,19 +9672,19 @@ export const runtimeValue = helperValue;`, it("converts Windows absolute import specifiers to file URLs only for module loading", () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); try { - expect(testing.toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe( + expect(toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe( "file:///C:/Users/alice/plugin/index.mjs", ); - expect(testing.toSafeImportPath("C:\\Users\\alice\\plugin folder\\x#y.mjs")).toBe( + expect(toSafeImportPath("C:\\Users\\alice\\plugin folder\\x#y.mjs")).toBe( "file:///C:/Users/alice/plugin%20folder/x%23y.mjs", ); - expect(testing.toSafeImportPath("\\\\server\\share\\plugin\\index.mjs")).toBe( + expect(toSafeImportPath("\\\\server\\share\\plugin\\index.mjs")).toBe( "file://server/share/plugin/index.mjs", ); - expect(testing.toSafeImportPath("file:///C:/Users/alice/plugin/index.mjs")).toBe( + expect(toSafeImportPath("file:///C:/Users/alice/plugin/index.mjs")).toBe( "file:///C:/Users/alice/plugin/index.mjs", ); - expect(testing.toSafeImportPath("./relative/index.mjs")).toBe("./relative/index.mjs"); + expect(toSafeImportPath("./relative/index.mjs")).toBe("./relative/index.mjs"); } finally { platformSpy.mockRestore(); } diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 243fb6bec648..b3217ced2e2d 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -51,7 +51,7 @@ import { initializeGlobalHookRunner } from "./hook-runner-global.js"; import { collectPluginManifestCompatCodes } from "./installed-plugin-index-record-builder.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js"; import { clearPluginInteractiveHandlers } from "./interactive-registry.js"; -import { PluginLoaderCacheState } from "./loader-cache-state.js"; +import { pluginLoaderCacheInstances, type CachedPluginState } from "./loader-cache-instances.js"; import { channelPluginIdBelongsToManifest, loadBundledRuntimeChannelPlugin, @@ -100,7 +100,6 @@ import { } from "./plugin-module-loader-cache.js"; import { createPluginRegistrationTransaction, - type PluginProcessGlobalState, restorePluginProcessGlobalState, snapshotPluginProcessGlobalState, } from "./plugin-registration-transaction.js"; @@ -114,7 +113,6 @@ import { normalizePluginIdScope, serializePluginIdScope, } from "./plugin-scope.js"; -import { ensureOpenClawPluginSdkAlias } from "./plugin-sdk-dist-alias.js"; import { installOpenClawPluginSdkNativeResolver } from "./plugin-sdk-native-resolver.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRegistryParams } from "./registry-types.js"; @@ -131,18 +129,9 @@ import type { PluginRuntime } from "./runtime/types.js"; import { validateJsonSchemaValue } from "./schema-validator.js"; import { buildPluginLoaderAliasMap, - buildPluginLoaderJitiOptions, - listPluginSdkAliasCandidates, - listPluginSdkExportedSubpaths, type PluginRuntimeModuleResolution, type PluginSdkResolutionPreference, - resolveExtensionApiAlias, - resolvePluginSdkAliasCandidateOrder, - resolvePluginSdkAliasFile, - resolvePluginRuntimeModulePath, resolvePluginRuntimeModulePathWithDiagnostics, - resolvePluginSdkScopedAliasMap, - shouldPreferNativeModuleLoad, } from "./sdk-alias.js"; import { hasKind, kindsEqual } from "./slots.js"; import { encodeStartupTraceSegment } from "./startup-trace-segment.js"; @@ -154,9 +143,6 @@ import type { PluginRegistrationMode, } from "./types.js"; -export type PluginLoadResult = PluginRegistry; -export { PluginLoadReentryError } from "./loader-cache-state.js"; - export type PluginLoadOptions = { config?: OpenClawConfig; activationSourceConfig?: OpenClawConfig; @@ -340,18 +326,8 @@ class PluginLoadFailureError extends Error { } } -type CachedPluginState = { - registry: PluginRegistry; - processGlobalState: PluginProcessGlobalState; -}; - -const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128; -const pluginLoaderCacheState = new PluginLoaderCacheState( - MAX_PLUGIN_REGISTRY_CACHE_ENTRIES, -); -const fullWorkspacePluginLoaderCacheState = new PluginLoaderCacheState( - MAX_PLUGIN_REGISTRY_CACHE_ENTRIES, -); +const { scoped: pluginLoaderCacheState, fullWorkspace: fullWorkspacePluginLoaderCacheState } = + pluginLoaderCacheInstances; const LAZY_RUNTIME_REFLECTION_KEYS = [ "version", "gateway", @@ -389,13 +365,6 @@ function createPluginCandidatesFromManifestRegistry( ...(record.packageManifest !== undefined ? { packageManifest: record.packageManifest } : {}), })); } - -export function clearPluginLoaderCache(): void { - pluginLoaderCacheState.clear(); - fullWorkspacePluginLoaderCacheState.clear(); - clearActivatedPluginRuntimeState(); -} - export function clearActivatedPluginRuntimeState(): void { clearAgentHarnesses(); clearPluginCommands(); @@ -520,34 +489,6 @@ function formatPluginRuntimeModuleResolutionError(params: { ...(resolution.error ? [`resolverError=${resolution.error}`] : []), ].join("; "); } - -export const testing = { - buildPluginLoaderJitiOptions, - buildPluginLoaderAliasMap, - listPluginSdkAliasCandidates, - listPluginSdkExportedSubpaths, - resolveExtensionApiAlias, - resolvePluginSdkScopedAliasMap, - resolvePluginSdkAliasCandidateOrder, - resolvePluginSdkAliasFile, - resolvePluginRuntimeModulePath, - ensureOpenClawPluginSdkAlias, - shouldLoadChannelPluginInSetupRuntime, - shouldPreferNativeModuleLoad, - toSafeImportPath, - createGuardedPluginRegistrationApi, - runPluginRegisterSync, - getCompatibleActivePluginRegistry, - resolvePluginLoadCacheContext, - get maxPluginRegistryCacheEntries() { - return pluginLoaderCacheState.maxEntries; - }, - setMaxPluginRegistryCacheEntriesForTest(value?: number) { - pluginLoaderCacheState.setMaxEntriesForTest(value); - fullWorkspacePluginLoaderCacheState.setMaxEntriesForTest(value); - }, -}; - function getPluginRegistryCache(onlyPluginIds?: string[]) { return onlyPluginIds ? pluginLoaderCacheState : fullWorkspacePluginLoaderCacheState; } @@ -3054,4 +2995,3 @@ function resolveCliMetadataEntrySource(rootDir: string): string | null { } return null; } -export { testing as __testing }; diff --git a/src/plugins/management-service.test.ts b/src/plugins/management-service.test.ts index 20dfc8c5cde7..427ab2221833 100644 --- a/src/plugins/management-service.test.ts +++ b/src/plugins/management-service.test.ts @@ -86,7 +86,6 @@ const { clearManagedPluginOfficialCatalogCache, installManagedPlugin, listManagedPlugins, - overlayBundledOfficialPluginCatalogMetadata, setManagedPluginEnabled, uninstallManagedPlugin, } = await import("./management-service.js"); @@ -170,17 +169,6 @@ const hostedDiffsEntry = { }, }; -const bundledDiffsEntry = { - name: "@openclaw/diffs", - version: "1.0.0", - description: "Bundled description", - openclaw: { - plugin: { id: "diffs", label: "Bundled Diffs" }, - catalog: { featured: true, order: 40 }, - install: { clawhubSpec: "clawhub:@openclaw/diffs", defaultChoice: "npm" }, - }, -}; - // Mirrors the current default ClawHub feed shape: package identity lives in a // source candidate while runtime/editorial metadata remains local. const hostedFeedDiffsEntry = { @@ -224,95 +212,80 @@ describe("plugin management service", () => { }); }); - it("overlays bundled runtime identity and curation while keeping hosted install metadata", () => { - const [merged] = overlayBundledOfficialPluginCatalogMetadata( - [hostedDiffsEntry] as never, - [bundledDiffsEntry] as never, - ); - - expect(merged).toMatchObject({ - name: "@openclaw/diffs", - version: "2.0.0", - description: "Hosted description", - openclaw: { - plugin: { id: "diffs", label: "Bundled Diffs" }, - install: { clawhubSpec: "clawhub:@openclaw/diffs", defaultChoice: "clawhub" }, - catalog: { featured: true, order: 40 }, - }, - }); - }); - - it("normalizes the current package-shaped hosted feed against exact local identity", async () => { - const [merged] = overlayBundledOfficialPluginCatalogMetadata( - [hostedFeedDiffsEntry] as never, - [bundledDiffsEntry] as never, - ); + it("overlays bundled curation through the hosted catalog load path", async () => { mocks.metadata.mockReturnValue(emptyMetadataSnapshot()); - - const catalog = await listManagedPlugins({ - config: {}, - env: {}, - officialCatalog: { entries: merged ? [merged] : [] }, + mocks.officialCatalog.mockResolvedValue({ + source: "hosted", + entries: [hostedDiffsEntry], + feed: { schemaVersion: 1, id: "test", generatedAt: "now", sequence: 1, entries: [] }, + metadata: { url: "https://clawhub.ai/feed", status: 200, checksum: "hash" }, }); - expect(merged).toMatchObject({ - id: "@openclaw/diffs", - title: "Diffs", - name: "@openclaw/diffs", - description: "Bundled description", - install: hostedFeedDiffsEntry.install, - openclaw: { - plugin: { id: "diffs", label: "Bundled Diffs" }, - catalog: { featured: true, order: 40 }, - }, - }); + const catalog = await listManagedPlugins({ config: {}, env: {} }); + expect(catalog.plugins).toEqual([ expect.objectContaining({ id: "diffs", - name: "Bundled Diffs", + name: "Diffs", + description: "Hosted description", + version: "2.0.0", + featured: true, + order: 40, + install: { source: "clawhub", packageName: "@openclaw/diffs" }, + }), + ]); + }); + + it("normalizes package-shaped hosted rows and deduplicates their runtime id", async () => { + mocks.metadata.mockReturnValue(emptyMetadataSnapshot()); + mocks.officialCatalog.mockResolvedValue({ + source: "hosted", + entries: [hostedFeedDiffsEntry], + feed: { schemaVersion: 1, id: "test", generatedAt: "now", sequence: 1, entries: [] }, + metadata: { url: "https://clawhub.ai/feed", status: 200, checksum: "hash" }, + }); + + const available = await listManagedPlugins({ config: {}, env: {} }); + expect(available.plugins).toEqual([ + expect.objectContaining({ + id: "diffs", + name: "Diffs", installed: false, featured: true, order: 40, install: { source: "official", pluginId: "diffs" }, }), ]); - }); - it("deduplicates a package-shaped hosted row against its installed runtime id", async () => { - const [merged] = overlayBundledOfficialPluginCatalogMetadata( - [hostedFeedDiffsEntry] as never, - [bundledDiffsEntry] as never, - ); mocks.metadata.mockReturnValue( metadataSnapshot({ enabled: true, id: "diffs", name: "Diffs", origin: "global" }), ); - - const catalog = await listManagedPlugins({ - config: {}, - env: {}, - officialCatalog: { entries: merged ? [merged] : [] }, - }); - - expect(catalog.plugins).toHaveLength(1); - expect(catalog.plugins[0]).toMatchObject({ id: "diffs", installed: true, enabled: true }); + const installed = await listManagedPlugins({ config: {}, env: {} }); + expect(installed.plugins).toHaveLength(1); + expect(installed.plugins[0]).toMatchObject({ id: "diffs", installed: true, enabled: true }); }); - it("does not transfer endorsement to a hosted entry that only reuses the plugin id", () => { - const impostor = { - ...hostedDiffsEntry, - name: "community/impostor", - openclaw: { - ...hostedDiffsEntry.openclaw, - install: { clawhubSpec: "clawhub:community/impostor", defaultChoice: "clawhub" }, - }, - }; + it("does not transfer bundled endorsement to a package identity impostor", async () => { + mocks.metadata.mockReturnValue(emptyMetadataSnapshot()); + mocks.officialCatalog.mockResolvedValue({ + source: "hosted", + entries: [ + { + ...hostedDiffsEntry, + name: "community/impostor", + openclaw: { + ...hostedDiffsEntry.openclaw, + install: { clawhubSpec: "clawhub:community/impostor", defaultChoice: "clawhub" }, + }, + }, + ], + feed: { schemaVersion: 1, id: "test", generatedAt: "now", sequence: 1, entries: [] }, + metadata: { url: "https://clawhub.ai/feed", status: 200, checksum: "hash" }, + }); - const [merged] = overlayBundledOfficialPluginCatalogMetadata( - [impostor] as never, - [bundledDiffsEntry] as never, - ); + const catalog = await listManagedPlugins({ config: {}, env: {} }); - expect(merged?.openclaw?.catalog).toBeUndefined(); + expect(catalog.plugins).toEqual([]); }); it("normalizes hosted catalog hints before building the public DTO", async () => { @@ -816,44 +789,6 @@ describe("plugin management service", () => { }); }); - it("suppresses hosted package-name-fallback entries once their package is installed", async () => { - // Hosted curated row without a declared runtime id: its catalog id falls - // back to the package name, which never matches the installed runtime id. - const hostedRow = { - id: "@openclaw/bluebubbles", - title: "BlueBubbles", - state: "available", - publisher: { id: "openclaw", trust: "official" }, - openclaw: { catalog: { featured: true, order: 90 } }, - install: { - candidates: [{ sourceRef: "public-clawhub", package: "@openclaw/bluebubbles" }], - }, - }; - mocks.metadata.mockReturnValue( - metadataSnapshot({ - enabled: true, - id: "bluebubbles", - name: "BlueBubbles", - origin: "global", - installRecord: { source: "clawhub", installPath: "/tmp/extensions/bluebubbles" }, - }), - ); - - const catalog = await listManagedPlugins({ - config: {}, - env: {}, - officialCatalog: { - entries: overlayBundledOfficialPluginCatalogMetadata( - // Route through the hosted-feed normalizer used by loadOfficialCatalog. - [hostedRow] as never, - [], - ), - }, - }); - - expect(catalog.plugins.map((plugin) => plugin.id)).toEqual(["bluebubbles"]); - }); - it("marks external installs removable and bundled plugins non-removable", async () => { mocks.metadata.mockReturnValue( metadataSnapshot({ diff --git a/src/plugins/management-service.ts b/src/plugins/management-service.ts index 2d8de9b2384f..e7b6ef9b885c 100644 --- a/src/plugins/management-service.ts +++ b/src/plugins/management-service.ts @@ -58,7 +58,7 @@ import { planPluginUninstall, } from "./uninstall.js"; -export type ManagedPluginCatalogEntry = { +type ManagedPluginCatalogEntry = { id: string; name: string; packageName?: string; @@ -77,13 +77,13 @@ export type ManagedPluginCatalogEntry = { removable?: boolean; }; -export type ManagedPluginCatalog = { +type ManagedPluginCatalog = { plugins: ManagedPluginCatalogEntry[]; diagnostics: unknown[]; mutationAllowed: boolean; }; -export type ManagedPluginInstallRequest = +type ManagedPluginInstallRequest = | { source: "clawhub"; packageName: string; @@ -189,7 +189,7 @@ function matchesBundledCatalogIdentity(params: { } /** Overlay local runtime identity and editorial hints after an exact package/source match. */ -export function overlayBundledOfficialPluginCatalogMetadata( +function overlayBundledOfficialPluginCatalogMetadata( entries: readonly OfficialExternalPluginCatalogEntry[], bundledEntries: readonly OfficialExternalPluginCatalogEntry[] = listOfficialExternalPluginCatalogEntries(), ): OfficialExternalPluginCatalogEntry[] { @@ -258,9 +258,7 @@ function resolveCatalogInstallAction(params: { } /** Coarse manifest-derived grouping so catalog UIs can shelve a large inventory. */ -export function derivePluginCategory( - manifest: PluginManifestRecord | undefined, -): string | undefined { +function derivePluginCategory(manifest: PluginManifestRecord | undefined): string | undefined { if (!manifest) { return undefined; } diff --git a/src/plugins/manifest-command-aliases.ts b/src/plugins/manifest-command-aliases.ts index bd5c14df8736..90e7fe3a2ec6 100644 --- a/src/plugins/manifest-command-aliases.ts +++ b/src/plugins/manifest-command-aliases.ts @@ -5,7 +5,7 @@ import { } from "@openclaw/normalization-core/string-coerce"; import { isRecord } from "../utils.js"; -export type PluginManifestCommandAliasKind = "runtime-slash"; +type PluginManifestCommandAliasKind = "runtime-slash"; /** One command alias declared by a plugin manifest. */ export type PluginManifestCommandAlias = { diff --git a/src/plugins/manifest-contract-runtime.ts b/src/plugins/manifest-contract-runtime.ts index 1cd6e686982a..24165602a6d8 100644 --- a/src/plugins/manifest-contract-runtime.ts +++ b/src/plugins/manifest-contract-runtime.ts @@ -8,7 +8,7 @@ import { import type { PluginManifestContractListKey } from "./manifest-registry.js"; import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; -export type ManifestContractRuntimePluginResolution = { +type ManifestContractRuntimePluginResolution = { pluginIds: string[]; bundledCompatPluginIds: string[]; }; diff --git a/src/plugins/manifest-contribution-ids.ts b/src/plugins/manifest-contribution-ids.ts index dbe95b86a90a..075b622f517f 100644 --- a/src/plugins/manifest-contribution-ids.ts +++ b/src/plugins/manifest-contribution-ids.ts @@ -8,16 +8,14 @@ import { } from "./plugin-registry.js"; /** Parameters for listing manifest contribution ids from a registry snapshot. */ -export type ListManifestContributionIdsParams = LoadPluginRegistryParams & { +type ListManifestContributionIdsParams = LoadPluginRegistryParams & { contribution: PluginRegistryContributionKey; index?: PluginRegistrySnapshot; includeDisabled?: boolean; }; /** Lists ids contributed by plugin manifests for one contribution kind. */ -export function listManifestContributionIds( - params: ListManifestContributionIdsParams, -): readonly string[] { +function listManifestContributionIds(params: ListManifestContributionIdsParams): readonly string[] { const env = params.env ?? process.env; const index = params.index ?? @@ -47,13 +45,3 @@ export function listManifestChannelContributionIds( contribution: "channels", }); } - -/** Lists provider ids contributed by plugin manifests. */ -export function listManifestProviderContributionIds( - params: Omit = {}, -): readonly string[] { - return listManifestContributionIds({ - ...params, - contribution: "providers", - }); -} diff --git a/src/plugins/manifest-model-id-normalization.test.ts b/src/plugins/manifest-model-id-normalization.test.ts index c4e8b2c75207..016dd68f5eaa 100644 --- a/src/plugins/manifest-model-id-normalization.test.ts +++ b/src/plugins/manifest-model-id-normalization.test.ts @@ -3,22 +3,13 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; -import { - clearCurrentPluginMetadataSnapshot, - resolvePluginMetadataControlPlaneFingerprint, - setCurrentPluginMetadataSnapshot, -} from "./current-plugin-metadata-snapshot.js"; -import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; +import { clearCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; import { writePersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js"; -import type { InstalledPluginIndex } from "./installed-plugin-index.js"; import { listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan.js"; import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js"; import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js"; -import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; -import { createEmptyPluginRegistry } from "./registry-empty.js"; -import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js"; +import { resetPluginRuntimeStateForTest } from "./runtime.js"; const tempDirs: string[] = []; const testEnvSnapshot = captureEnv([ @@ -96,68 +87,6 @@ function writeNormalizerManifest(params: { pluginDir: string; prefix: string }): ); } -function createCurrentSnapshot(params: { - manifestHash: string; - prefix: string; - workspaceDir?: string; - config?: OpenClawConfig; -}): PluginMetadataSnapshot { - const config = params.config ?? {}; - const policyHash = resolveInstalledPluginIndexPolicyHash(config); - const index: InstalledPluginIndex = { - version: 1, - hostContractVersion: "test-host", - compatRegistryVersion: "test-compat", - migrationVersion: 1, - policyHash, - generatedAtMs: 0, - installRecords: {}, - plugins: [ - { - pluginId: "normalizer", - manifestPath: `/tmp/normalizer-${params.manifestHash}/openclaw.plugin.json`, - manifestHash: params.manifestHash, - source: `/tmp/normalizer-${params.manifestHash}/index.ts`, - rootDir: `/tmp/normalizer-${params.manifestHash}`, - origin: "global", - enabled: true, - startup: { - sidecar: false, - memory: false, - deferConfiguredChannelFullLoadUntilAfterListen: false, - agentHarnesses: [], - }, - compat: [], - }, - ], - diagnostics: [], - }; - return { - policyHash, - configFingerprint: resolvePluginMetadataControlPlaneFingerprint(config, { - env: process.env, - index, - policyHash, - workspaceDir: params.workspaceDir, - }), - workspaceDir: params.workspaceDir, - index, - registryDiagnostics: [], - plugins: [ - { - id: "normalizer", - modelIdNormalization: { - providers: { - demo: { - prefixWhenBare: params.prefix, - }, - }, - }, - }, - ], - } as unknown as PluginMetadataSnapshot; -} - function normalizeDemoModel(modelId = "demo-model"): string | undefined { return normalizeProviderModelIdWithManifest({ provider: "demo", @@ -165,17 +94,6 @@ function normalizeDemoModel(modelId = "demo-model"): string | undefined { }); } -function normalizeDemoModelWithEnv( - env: NodeJS.ProcessEnv, - modelId = "demo-model", -): string | undefined { - return normalizeProviderModelIdWithManifest({ - provider: "demo", - env, - context: { provider: "demo", modelId }, - }); -} - describe("manifest model id normalization", () => { beforeEach(() => { resetPluginRuntimeStateForTest(); @@ -192,112 +110,6 @@ describe("manifest model id normalization", () => { } }); - it("refreshes cached policies when the current metadata snapshot changes", () => { - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "alpha", - prefix: "alpha", - }), - { config: {}, env: process.env }, - ); - - expect(normalizeDemoModel()).toBe("alpha/demo-model"); - expect(normalizeDemoModel("second-model")).toBe("alpha/second-model"); - - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "bravo", - prefix: "bravo", - }), - { config: {}, env: process.env }, - ); - - expect(normalizeDemoModel()).toBe("bravo/demo-model"); - }); - - it("uses workspace-scoped current metadata through the active plugin runtime", () => { - setActivePluginRegistry( - createEmptyPluginRegistry(), - "workspace-a", - "gateway-bindable", - "/workspace/a", - ); - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "alpha", - prefix: "alpha", - workspaceDir: "/workspace/a", - }), - { config: {}, env: process.env }, - ); - - expect(normalizeDemoModel()).toBe("alpha/demo-model"); - expect(normalizeDemoModel("second-model")).toBe("alpha/second-model"); - - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "bravo", - prefix: "bravo", - workspaceDir: "/workspace/a", - }), - { config: {}, env: process.env }, - ); - - expect(normalizeDemoModel()).toBe("bravo/demo-model"); - }); - - it("reuses workspace-scoped current metadata for unscoped normalization", () => { - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "alpha", - prefix: "alpha", - workspaceDir: "/workspace/a", - }), - { config: {}, env: process.env }, - ); - - expect(normalizeDemoModel()).toBe("alpha/demo-model"); - }); - - it("reuses current metadata when callers omit config", () => { - const config: OpenClawConfig = { plugins: { allow: ["normalizer"] } }; - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "alpha", - prefix: "alpha", - config, - }), - { config, env: process.env }, - ); - - expect(normalizeDemoModel()).toBe("alpha/demo-model"); - }); - - it("validates explicit env before reusing current metadata", () => { - setCurrentPluginMetadataSnapshot( - createCurrentSnapshot({ - manifestHash: "alpha", - prefix: "alpha", - }), - { config: {}, env: process.env }, - ); - - const stateDir = makeTempDir(); - const pluginDir = path.join(stateDir, "extensions", "normalizer"); - writeInstallIndex({ stateDir, pluginDir }); - writeNormalizerManifest({ pluginDir, prefix: "bravo" }); - - const env = { - ...process.env, - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_HOME: undefined, - OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", - OPENCLAW_BUNDLED_PLUGINS_DIR: undefined, - }; - - expect(normalizeDemoModelWithEnv(env)).toBe("bravo/demo-model"); - }); - it("keeps process metadata stable across manifest edits and reflects state-dir changes", () => { const stateDirA = makeTempDir(); const pluginDirA = path.join(stateDirA, "extensions", "normalizer"); diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index b338a55b2ba8..7eee96d17ee5 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -6,10 +6,7 @@ import { collectChannelSchemaMetadata } from "../config/channel-config-metadata. import type { PluginInstallRecord } from "../config/types.plugins.js"; import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.js"; import type { PluginCandidate } from "./discovery.js"; -import { - __testing as manifestRegistryTesting, - loadPluginManifestRegistry, -} from "./manifest-registry.js"; +import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { OpenClawPackageManifest } from "./manifest.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; @@ -2358,26 +2355,6 @@ describe("loadPluginManifestRegistry", () => { }); }); - it("preserves host-trusted plugin contracts from catalog overlays", () => { - const contracts = manifestRegistryTesting.mergeManifestContracts( - { - agentToolResultMiddleware: ["openclaw"], - usageProviders: ["openai"], - }, - { - agentToolResultMiddleware: ["codex"], - trustedToolPolicies: ["workflow-budget"], - usageProviders: ["openrouter"], - }, - ); - - expect(contracts).toEqual({ - agentToolResultMiddleware: ["openclaw", "codex"], - trustedToolPolicies: ["workflow-budget"], - usageProviders: ["openai", "openrouter"], - }); - }); - it("preserves channel env metadata from plugin manifests", () => { const dir = makeTempDir(); writeManifest(dir, { diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index fdebbdaf1732..ac3cd06a316f 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -1220,8 +1220,3 @@ export function loadPluginManifestRegistry( const registry = { plugins: records, diagnostics: dedupePluginDiagnostics(diagnostics) }; return registry; } - -export const testing = { - mergeManifestContracts, -}; -export { testing as __testing }; diff --git a/src/plugins/manifest-tool-availability.ts b/src/plugins/manifest-tool-availability.ts index 287b341a277c..697258effe37 100644 --- a/src/plugins/manifest-tool-availability.ts +++ b/src/plugins/manifest-tool-availability.ts @@ -11,8 +11,8 @@ import type { } from "./manifest.js"; type ToolMetadata = NonNullable[string]; -export type ManifestConfigAvailabilitySignal = PluginManifestCapabilityProviderConfigSignal; -export type ManifestAuthAvailabilitySignal = PluginManifestCapabilityProviderAuthSignal; +type ManifestConfigAvailabilitySignal = PluginManifestCapabilityProviderConfigSignal; +type ManifestAuthAvailabilitySignal = PluginManifestCapabilityProviderAuthSignal; function readPath(root: unknown, path: string | undefined): unknown { if (!path?.trim()) { diff --git a/src/plugins/manifest.json5-tolerance.test.ts b/src/plugins/manifest.json5-tolerance.test.ts index f2f345691e92..869f4b36f332 100644 --- a/src/plugins/manifest.json5-tolerance.test.ts +++ b/src/plugins/manifest.json5-tolerance.test.ts @@ -3,11 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import JSON5 from "json5"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - clearPluginManifestLoadCache, - loadPluginManifest, - MAX_PLUGIN_MANIFEST_BYTES, -} from "./manifest.js"; +import { loadPluginManifest } from "./manifest.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; const tempDirs: string[] = []; @@ -18,7 +14,6 @@ function makeTempDir() { afterEach(() => { vi.restoreAllMocks(); - clearPluginManifestLoadCache(); cleanupTrackedTempDirs(tempDirs); }); @@ -239,24 +234,4 @@ describe("loadPluginManifest JSON5 tolerance", () => { expect(result.error).toContain("plugin manifest must be an object"); } }); - - it("rejects oversized manifests before parsing", () => { - const dir = makeTempDir(); - fs.writeFileSync( - path.join(dir, "openclaw.plugin.json"), - JSON.stringify({ - id: "too-large", - configSchema: { type: "object" }, - padding: "x".repeat(MAX_PLUGIN_MANIFEST_BYTES), - }), - "utf-8", - ); - - const result = loadPluginManifest(dir, false); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toContain("unsafe plugin manifest path"); - } - }); }); diff --git a/src/plugins/manifest.reserved-id.test.ts b/src/plugins/manifest.reserved-id.test.ts index a5e9cfbd5d08..28974f918890 100644 --- a/src/plugins/manifest.reserved-id.test.ts +++ b/src/plugins/manifest.reserved-id.test.ts @@ -3,12 +3,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { clearPluginManifestLoadCache, loadPluginManifest } from "./manifest.js"; +import { loadPluginManifest } from "./manifest.js"; const tempDirs: string[] = []; afterEach(async () => { - clearPluginManifestLoadCache(); await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 90b4291bbd9f..edce57fcbbe7 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -25,7 +25,7 @@ import type { PluginKind } from "./plugin-kind.types.js"; /** Canonical plugin manifest filename inside plugin roots. */ export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const; -export const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024; +const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024; const MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES = 512; const MAX_SECRET_PROVIDER_EXEC_ARGS = 128; const MAX_SECRET_PROVIDER_EXEC_ARG_BYTES = 1024; @@ -45,12 +45,6 @@ type PluginManifestLoadCacheEntry = { const pluginManifestLoadCache = new PluginLruCache( MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES, ); - -/** Clears process-local manifest parse cache for tests and explicit refresh flows. */ -export function clearPluginManifestLoadCache(): void { - pluginManifestLoadCache.clear(); -} - export type PluginManifestChannelConfig = { schema: JsonSchemaObject; uiHints?: Record; @@ -99,7 +93,7 @@ export type PluginManifestModelPricing = { providers?: Record; }; -export type PluginManifestModelIdPrefixRule = { +type PluginManifestModelIdPrefixRule = { modelPrefix: string; prefix: string; }; @@ -133,7 +127,7 @@ export type PluginManifestProviderEndpoint = { googleVertexRegionHostSuffix?: string; }; -export type PluginManifestProviderRequestProvider = { +type PluginManifestProviderRequestProvider = { family?: string; compatibilityFamily?: "moonshot"; openAICompletions?: { @@ -189,9 +183,9 @@ export type PluginManifestActivation = { onCapabilities?: PluginManifestActivationCapability[]; }; -export type PluginManifestDefaultPlatform = NodeJS.Platform; +type PluginManifestDefaultPlatform = NodeJS.Platform; -export type PluginManifestSetupProvider = { +type PluginManifestSetupProvider = { /** Provider id surfaced during setup/onboarding. */ id: string; /** Setup/auth methods that this provider supports. */ @@ -206,7 +200,7 @@ export type PluginManifestSetupProvider = { authEvidence?: PluginManifestSetupProviderAuthEvidence[]; }; -export type PluginManifestSetupProviderAuthEvidence = { +type PluginManifestSetupProviderAuthEvidence = { /** Generic local file evidence gated by required environment metadata. */ type: "local-file-with-env"; /** Optional env var containing an explicit credential file path. */ @@ -244,9 +238,9 @@ export type PluginManifestQaRunner = { description?: string; }; -export type PluginManifestConfigLiteral = string | number | boolean | null; +type PluginManifestConfigLiteral = string | number | boolean | null; -export type PluginManifestDangerousConfigFlag = { +type PluginManifestDangerousConfigFlag = { /** * Dot-separated config path relative to `plugins.entries..config`. * Supports `*` wildcards for map/array segments. @@ -256,7 +250,7 @@ export type PluginManifestDangerousConfigFlag = { equals: PluginManifestConfigLiteral; }; -export type PluginManifestSecretInputPath = { +type PluginManifestSecretInputPath = { /** * Dot-separated config path relative to `plugins.entries..config`. * Supports `*` wildcards for map/array segments. @@ -266,7 +260,7 @@ export type PluginManifestSecretInputPath = { expected?: "string"; }; -export type PluginManifestSecretInputContracts = { +type PluginManifestSecretInputContracts = { /** * Override bundled-plugin default enablement when deciding whether this * SecretRef surface is active. Use this when the plugin is bundled but the @@ -445,7 +439,7 @@ export type PluginManifestContracts = { tools?: string[]; }; -export type PluginManifestMediaUnderstandingCapability = "image" | "audio" | "video"; +type PluginManifestMediaUnderstandingCapability = "image" | "audio" | "video"; export type PluginManifestMediaUnderstandingProviderMetadata = { capabilities?: PluginManifestMediaUnderstandingCapability[]; @@ -463,7 +457,7 @@ export type PluginManifestMediaUnderstandingProviderMetadata = { >; }; -export type PluginManifestProviderBaseUrlGuard = { +type PluginManifestProviderBaseUrlGuard = { provider: string; defaultBaseUrl?: string; allowedBaseUrls: string[]; @@ -474,7 +468,7 @@ export type PluginManifestCapabilityProviderAuthSignal = { providerBaseUrl?: PluginManifestProviderBaseUrlGuard; }; -export type PluginManifestCapabilityProviderModeConfigSignal = { +type PluginManifestCapabilityProviderModeConfigSignal = { path?: string; default?: string; allowed?: string[]; @@ -504,7 +498,7 @@ export type PluginManifestToolMetadata = PluginManifestCapabilityProviderMetadat replaySafe?: boolean; }; -export type PluginManifestProviderAuthChoice = { +type PluginManifestProviderAuthChoice = { /** Provider id owned by this manifest entry. */ provider: string; /** Provider auth method id that this choice should dispatch to. */ @@ -545,12 +539,9 @@ export type PluginManifestProviderAuthChoice = { onboardingScopes?: PluginManifestOnboardingScope[]; }; -export type PluginManifestOnboardingScope = - | "text-inference" - | "image-generation" - | "music-generation"; +type PluginManifestOnboardingScope = "text-inference" | "image-generation" | "music-generation"; -export type PluginManifestLoadResult = +type PluginManifestLoadResult = | { ok: true; manifest: PluginManifest; manifestPath: string } | { ok: false; error: string; manifestPath: string }; @@ -1981,7 +1972,7 @@ export type PluginPackageChannelDoctorCapabilities = { warnOnEmptyGroupSenderAllowlist?: boolean; }; -export type PluginPackageChannelCliOption = { +type PluginPackageChannelCliOption = { flags: string; description: string; defaultValue?: boolean | string; @@ -1998,7 +1989,7 @@ export type PluginPackageInstall = { requiredPlatformPackages?: string[]; }; -export type OpenClawPackageStartup = { +type OpenClawPackageStartup = { /** * Opt-in for channel plugins whose `setupEntry` fully covers the gateway * startup surface needed before the server starts listening. @@ -2006,13 +1997,13 @@ export type OpenClawPackageStartup = { deferConfiguredChannelFullLoadUntilAfterListen?: boolean; }; -export type OpenClawPackageSetupFeatures = { +type OpenClawPackageSetupFeatures = { configPromotion?: boolean; legacyStateMigrations?: boolean; legacySessionSurfaces?: boolean; }; -export type OpenClawPackageCompat = { +type OpenClawPackageCompat = { pluginApi?: string; }; @@ -2050,7 +2041,7 @@ export type PackageExtensionResolution = | { status: "empty"; entries: [] } | { status: "invalid"; entries: []; error: string }; -export type ManifestKey = typeof MANIFEST_KEY; +type ManifestKey = typeof MANIFEST_KEY; export type PackageManifest = { name?: string; diff --git a/src/plugins/marketplace.ts b/src/plugins/marketplace.ts index baf0763221c1..d594d2ffa65d 100644 --- a/src/plugins/marketplace.ts +++ b/src/plugins/marketplace.ts @@ -46,14 +46,14 @@ type MarketplaceEntrySource = | { kind: "git-subdir"; url: string; path: string; ref?: string } | { kind: "url"; url: string }; -export type MarketplacePluginEntry = { +type MarketplacePluginEntry = { name: string; version?: string; description?: string; source: MarketplaceEntrySource; }; -export type MarketplaceManifest = { +type MarketplaceManifest = { name?: string; version?: string; plugins: MarketplacePluginEntry[]; @@ -80,7 +80,7 @@ type KnownMarketplaceRecord = { source?: unknown; }; -export type MarketplacePluginListResult = +type MarketplacePluginListResult = | { ok: true; manifest: MarketplaceManifest; @@ -91,7 +91,7 @@ export type MarketplacePluginListResult = error: string; }; -export type MarketplaceInstallResult = +type MarketplaceInstallResult = | ({ ok: true; marketplaceName?: string; @@ -102,7 +102,7 @@ export type MarketplaceInstallResult = } & Extract) | Extract; -export type MarketplaceShortcutResolution = +type MarketplaceShortcutResolution = | { ok: true; plugin: string; diff --git a/src/plugins/memory-embedding-providers.test.ts b/src/plugins/memory-embedding-providers.test.ts index 8a95315fab60..72e25713c913 100644 --- a/src/plugins/memory-embedding-providers.test.ts +++ b/src/plugins/memory-embedding-providers.test.ts @@ -8,7 +8,6 @@ import { listRegisteredMemoryEmbeddingProviders, registerMemoryEmbeddingProvider, restoreRegisteredMemoryEmbeddingProviders, - restoreMemoryEmbeddingProviders, type MemoryEmbeddingProviderAdapter, } from "./memory-embedding-providers.js"; @@ -115,17 +114,6 @@ describe("memory embedding provider registry", () => { }); }); - it("restores a previous snapshot", () => { - const alpha = createAdapter("alpha"); - const beta = createAdapter("beta"); - registerMemoryEmbeddingProvider(alpha); - - restoreMemoryEmbeddingProviders([beta]); - - expectCurrentMemoryEmbeddingProvider("alpha", undefined); - expectCurrentMemoryEmbeddingProvider("beta", beta); - }); - it.each([ { name: "tracks owner plugin ids in registered snapshots", diff --git a/src/plugins/memory-embedding-providers.ts b/src/plugins/memory-embedding-providers.ts index d9bb9c009b1f..a0f54af8e7ba 100644 --- a/src/plugins/memory-embedding-providers.ts +++ b/src/plugins/memory-embedding-providers.ts @@ -174,15 +174,6 @@ export function listRegisteredMemoryEmbeddingProviders(): RegisteredMemoryEmbedd export function listMemoryEmbeddingProviders(): MemoryEmbeddingProviderAdapter[] { return listRegisteredMemoryEmbeddingProviders().map((entry) => entry.adapter); } - -/** Replaces registered memory embedding providers with adapter-only state. */ -export function restoreMemoryEmbeddingProviders(adapters: MemoryEmbeddingProviderAdapter[]): void { - getMemoryEmbeddingProviders().clear(); - for (const adapter of adapters) { - registerMemoryEmbeddingProvider(adapter); - } -} - /** Replaces registered memory embedding providers while preserving metadata. */ export function restoreRegisteredMemoryEmbeddingProviders( entries: RegisteredMemoryEmbeddingProvider[], diff --git a/src/plugins/memory-state.test-fixtures.ts b/src/plugins/memory-state.test-fixtures.ts new file mode 100644 index 000000000000..2c53ea4730e1 --- /dev/null +++ b/src/plugins/memory-state.test-fixtures.ts @@ -0,0 +1,23 @@ +/** Test-only compatibility fixtures for plugin memory state. */ +import { + registerMemoryFlushPlanResolverForPlugin, + registerMemoryPromptSectionForPlugin, + registerMemoryRuntimeForPlugin, + type MemoryFlushPlanResolver, + type MemoryPluginRuntime, + type MemoryPromptSectionBuilder, +} from "./memory-state.js"; + +export * from "./memory-state.js"; + +export function registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void { + registerMemoryPromptSectionForPlugin("test-memory", builder); +} + +export function registerMemoryFlushPlanResolver(resolver: MemoryFlushPlanResolver): void { + registerMemoryFlushPlanResolverForPlugin("test-memory", resolver); +} + +export function registerMemoryRuntime(runtime: MemoryPluginRuntime): void { + registerMemoryRuntimeForPlugin("test-memory", runtime); +} diff --git a/src/plugins/memory-state.test.ts b/src/plugins/memory-state.test.ts index 9e9d218c8ca3..e24662aed23e 100644 --- a/src/plugins/memory-state.test.ts +++ b/src/plugins/memory-state.test.ts @@ -5,20 +5,17 @@ import { clearMemoryPluginState, getMemoryCapabilityRegistration, getMemoryRuntime, - hasMemoryRuntime, listMemoryCorpusSupplements, listMemoryPromptSupplements, listActiveMemoryPublicArtifacts, registerMemoryCapability, registerMemoryCorpusSupplement, - registerMemoryFlushPlanResolver, registerMemoryPromptSupplement, registerMemoryPromptSection, - registerMemoryRuntime, resolveMemoryFlushPlan, restoreMemoryPluginState, type MemoryPluginPublicArtifact, -} from "./memory-state.js"; +} from "./memory-state.test-fixtures.js"; function createMemoryRuntime() { return { @@ -97,68 +94,6 @@ describe("memory plugin state", () => { ]); }); - it("adapts deprecated split registration to the unified memory capability", () => { - const runtime = createMemoryRuntime(); - const promptBuilder = () => ["legacy prompt"]; - const flushPlanResolver = () => createMemoryFlushPlan("memory/legacy.md"); - - registerMemoryPromptSection(promptBuilder); - registerMemoryFlushPlanResolver(flushPlanResolver); - registerMemoryRuntime(runtime); - - expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual(["legacy prompt"]); - expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/legacy.md"); - expect(getMemoryRuntime()).toBe(runtime); - expect(getMemoryCapabilityRegistration()).toStrictEqual({ - pluginId: "legacy-memory-v1", - capability: { - promptBuilder, - flushPlanResolver, - runtime, - }, - }); - }); - - it("prefers the registered memory capability over earlier legacy split state", async () => { - const runtime = createMemoryRuntime(); - const promptBuilder = () => ["capability prompt"]; - const flushPlanResolver = () => createMemoryFlushPlan("memory/capability.md"); - - registerMemoryPromptSection(() => ["legacy prompt"]); - registerMemoryFlushPlanResolver(() => createMemoryFlushPlan("memory/legacy.md")); - registerMemoryRuntime({ - async getMemorySearchManager() { - return { manager: null, error: "legacy" }; - }, - resolveMemoryBackendConfig() { - return { backend: "builtin" as const }; - }, - }); - registerMemoryCapability("memory-core", { - promptBuilder, - flushPlanResolver, - runtime, - }); - - expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual(["capability prompt"]); - expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/capability.md"); - await expect( - getMemoryRuntime()?.getMemorySearchManager({ - cfg: {} as never, - agentId: "main", - }), - ).resolves.toEqual({ manager: null, error: "missing" }); - expect(hasMemoryRuntime()).toBe(true); - expect(getMemoryCapabilityRegistration()).toStrictEqual({ - pluginId: "memory-core", - capability: { - promptBuilder, - flushPlanResolver, - runtime, - }, - }); - }); - it("lists active public memory artifacts in deterministic order", async () => { registerMemoryCapability("memory-core", { publicArtifacts: { @@ -400,33 +335,6 @@ describe("memory plugin state", () => { ).resolves.toEqual([{ corpus: "wiki", path: "sources/alpha.md", score: 1, snippet: "x" }]); }); - it("uses the registered flush plan resolver", () => { - registerMemoryFlushPlanResolver(() => ({ - softThresholdTokens: 1, - forceFlushTranscriptBytes: 2, - reserveTokensFloor: 3, - prompt: "prompt", - systemPrompt: "system", - relativePath: "memory/test.md", - })); - - expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/test.md"); - }); - - it("stores the registered memory runtime", async () => { - const runtime = createMemoryRuntime(); - - registerMemoryRuntime(runtime); - - expect(getMemoryRuntime()).toBe(runtime); - await expect( - getMemoryRuntime()?.getMemorySearchManager({ - cfg: {} as never, - agentId: "main", - }), - ).resolves.toEqual({ manager: null, error: "missing" }); - }); - it("restoreMemoryPluginState swaps both prompt and flush state", () => { const runtime = createMemoryRuntime(); registerMemoryState({ diff --git a/src/plugins/memory-state.ts b/src/plugins/memory-state.ts index 52d0bed6500b..f7047f2d240f 100644 --- a/src/plugins/memory-state.ts +++ b/src/plugins/memory-state.ts @@ -70,7 +70,7 @@ export type MemoryCorpusSupplementRegistration = { supplement: MemoryCorpusSupplement; }; -export type MemoryPromptSupplementRegistration = { +type MemoryPromptSupplementRegistration = { pluginId: string; builder: MemoryPromptSectionBuilder; }; @@ -92,11 +92,11 @@ export type MemoryFlushPlanResolver = (params: { export type RegisteredMemorySearchManager = MemorySearchManager; -export type MemoryRuntimeQmdConfig = { +type MemoryRuntimeQmdConfig = { command?: string; }; -export type MemoryRuntimeBackendConfig = +type MemoryRuntimeBackendConfig = | { backend: "builtin"; } @@ -137,7 +137,7 @@ export type MemoryPluginRuntime = { closeAllMemorySearchManagers?(): Promise; }; -export type MemoryPluginPublicArtifactContentType = "markdown" | "json" | "text"; +type MemoryPluginPublicArtifactContentType = "markdown" | "json" | "text"; export type MemoryPluginPublicArtifact = { kind: string; @@ -159,13 +159,11 @@ export type MemoryPluginCapability = { publicArtifacts?: MemoryPluginPublicArtifactsProvider; }; -export type MemoryPluginCapabilityRegistration = { +type MemoryPluginCapabilityRegistration = { pluginId: string; capability: MemoryPluginCapability; }; -const LEGACY_MEMORY_COMPAT_PLUGIN_ID = "legacy-memory-v1"; - type MemoryPluginState = { capability?: MemoryPluginCapabilityRegistration; corpusSupplements: MemoryCorpusSupplementRegistration[]; @@ -229,12 +227,6 @@ export function getMemoryCapabilityRegistration(): MemoryPluginCapabilityRegistr export function listMemoryCorpusSupplements(): MemoryCorpusSupplementRegistration[] { return [...memoryPluginState.corpusSupplements]; } - -/** @deprecated Use registerMemoryCapability(pluginId, { promptBuilder }) instead. */ -export function registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void { - registerMemoryPromptSectionForPlugin(LEGACY_MEMORY_COMPAT_PLUGIN_ID, builder); -} - export function registerMemoryPromptSectionForPlugin( pluginId: string, builder: MemoryPromptSectionBuilder, @@ -280,12 +272,6 @@ function normalizeMemoryPromptLines(value: unknown): string[] { export function listMemoryPromptSupplements(): MemoryPromptSupplementRegistration[] { return [...memoryPluginState.promptSupplements]; } - -/** @deprecated Use registerMemoryCapability(pluginId, { flushPlanResolver }) instead. */ -export function registerMemoryFlushPlanResolver(resolver: MemoryFlushPlanResolver): void { - registerMemoryFlushPlanResolverForPlugin(LEGACY_MEMORY_COMPAT_PLUGIN_ID, resolver); -} - export function registerMemoryFlushPlanResolverForPlugin( pluginId: string, resolver: MemoryFlushPlanResolver, @@ -299,12 +285,6 @@ export function resolveMemoryFlushPlan(params: { }): MemoryFlushPlan | null { return memoryPluginState.capability?.capability.flushPlanResolver?.(params) ?? null; } - -/** @deprecated Use registerMemoryCapability(pluginId, { runtime }) instead. */ -export function registerMemoryRuntime(runtime: MemoryPluginRuntime): void { - registerMemoryRuntimeForPlugin(LEGACY_MEMORY_COMPAT_PLUGIN_ID, runtime); -} - export function registerMemoryRuntimeForPlugin( pluginId: string, runtime: MemoryPluginRuntime, diff --git a/src/plugins/min-host-version.test.ts b/src/plugins/min-host-version.test.ts index 9db9adf278f9..48f474fe607f 100644 --- a/src/plugins/min-host-version.test.ts +++ b/src/plugins/min-host-version.test.ts @@ -1,10 +1,6 @@ // Verifies plugin minimum host version compatibility checks. import { describe, expect, it } from "vitest"; -import { - checkMinHostVersion, - MIN_HOST_VERSION_FORMAT, - parseMinHostVersionRequirement, -} from "./min-host-version.js"; +import { checkMinHostVersion, parseMinHostVersionRequirement } from "./min-host-version.js"; const MIN_HOST_REQUIREMENT = { raw: ">=2026.3.22", @@ -39,18 +35,6 @@ function expectHostCheckResult(params: { ).toEqual(params.expected); } -function expectInvalidHostCheck(minHostVersion: string | number) { - expectHostCheckResult({ - currentVersion: "2026.3.22", - minHostVersion, - expected: { - ok: false, - kind: "invalid", - error: MIN_HOST_VERSION_FORMAT, - }, - }); -} - describe("min-host-version", () => { it("accepts empty metadata", () => { expect(parseMinHostVersionRequirement(undefined)).toBeNull(); @@ -86,13 +70,6 @@ describe("min-host-version", () => { }); }); - it.each(["2026.3.22", 123, ">=2026.3.22 garbage"] as const)( - "rejects invalid floor syntax and host checks: %p", - (minHostVersion) => { - expectInvalidHostCheck(minHostVersion); - }, - ); - it.each([ { name: "reports unknown host versions distinctly", diff --git a/src/plugins/min-host-version.ts b/src/plugins/min-host-version.ts index dff5032b2f78..bc866287adc6 100644 --- a/src/plugins/min-host-version.ts +++ b/src/plugins/min-host-version.ts @@ -2,14 +2,14 @@ import { isAtLeast, parseSemver } from "../infra/runtime-guard.js"; /** Validation message for plugin minHostVersion manifest fields. */ -export const MIN_HOST_VERSION_FORMAT = +const MIN_HOST_VERSION_FORMAT = 'openclaw.install.minHostVersion must use a semver floor in the form ">=x.y.z[-prerelease][+build]"'; const SEMVER_LABEL_RE = String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`; const MIN_HOST_VERSION_RE = new RegExp(`^>=(${SEMVER_LABEL_RE})$`); const LEGACY_MIN_HOST_VERSION_RE = /^(\d+)\.(\d+)\.(\d+)$/; /** Parsed plugin minimum host version requirement. */ -export type MinHostVersionRequirement = { +type MinHostVersionRequirement = { raw: string; minimumLabel: string; }; @@ -17,7 +17,7 @@ export type MinHostVersionRequirement = { import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; /** Result of checking a plugin minHostVersion against the current host. */ -export type MinHostVersionCheckResult = +type MinHostVersionCheckResult = | { ok: true; requirement: MinHostVersionRequirement | null } | { ok: false; kind: "invalid"; error: string } | { ok: false; kind: "unknown_host_version"; requirement: MinHostVersionRequirement } diff --git a/src/plugins/official-external-plugin-catalog-envelope.test.ts b/src/plugins/official-external-plugin-catalog-envelope.test.ts index 0c742dbe9535..70639559f5d9 100644 --- a/src/plugins/official-external-plugin-catalog-envelope.test.ts +++ b/src/plugins/official-external-plugin-catalog-envelope.test.ts @@ -1,15 +1,16 @@ -import crypto from "node:crypto"; +import crypto, { type KeyObject } from "node:crypto"; import { describe, expect, it } from "vitest"; -import { publicKeyRawBase64UrlFromEd25519Pem } from "../infra/ed25519-signature.js"; -import { - OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE, - createOfficialExternalPluginCatalogEnvelopePayload, - createOfficialExternalPluginCatalogEnvelopeSigningInput, - verifyOfficialExternalPluginCatalogSignedEnvelope, - type OfficialExternalPluginCatalogSignedEnvelope, -} from "./official-external-plugin-catalog-envelope.js"; +import { verifyOfficialExternalPluginCatalogSignedEnvelope } from "./official-external-plugin-catalog-envelope.js"; import type { OfficialExternalPluginCatalogFeed } from "./official-external-plugin-catalog.js"; +const PAYLOAD_TYPE = "openclaw.official-external-plugin-catalog-feed.v1"; + +type SigningKey = { + keyId: string; + privateKey: KeyObject; + publicKey: KeyObject; +}; + function fixtureFeed(): OfficialExternalPluginCatalogFeed { return { schemaVersion: 2, @@ -28,344 +29,176 @@ function fixtureFeed(): OfficialExternalPluginCatalogFeed { }; } -function signedEnvelope(params?: { - feed?: OfficialExternalPluginCatalogFeed; - payload?: string; - payloadType?: string; - keyId?: string; - privateKeyPem?: string; -}): { - envelope: OfficialExternalPluginCatalogSignedEnvelope; - publicKeyPem: string; - rawPublicKey: string; -} { - const { publicKey, privateKey } = - params?.privateKeyPem === undefined - ? crypto.generateKeyPairSync("ed25519", { - publicKeyEncoding: { type: "spki", format: "pem" }, - privateKeyEncoding: { type: "pkcs8", format: "pem" }, - }) - : { - publicKey: crypto - .createPublicKey(params.privateKeyPem) - .export({ type: "spki", format: "pem" }) as string, - privateKey: params.privateKeyPem, - }; - const payloadType = params?.payloadType ?? OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE; - const payload = - params?.payload ?? - createOfficialExternalPluginCatalogEnvelopePayload(params?.feed ?? fixtureFeed()); - const payloadBytes = Buffer.from(payload, "base64"); - const signingInput = createOfficialExternalPluginCatalogEnvelopeSigningInput({ - payloadType, +function createSigningKey(keyId: string): SigningKey { + const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519"); + return { keyId, privateKey, publicKey }; +} + +function exportPublicKey(key: SigningKey): string { + return key.publicKey.export({ type: "spki", format: "pem" }); +} + +function signingInput(payloadType: string, payloadBytes: Buffer): Buffer { + const payloadTypeBytes = Buffer.from(payloadType, "utf8"); + return Buffer.concat([ + Buffer.from(`DSSEv1 ${payloadTypeBytes.length} ${payloadType} ${payloadBytes.length} `, "utf8"), payloadBytes, - }); - const signature = crypto - .sign(null, signingInput, crypto.createPrivateKey(privateKey)) - .toString("base64url"); + ]); +} + +function signedEnvelope(params: { + keys: readonly SigningKey[]; + feed?: unknown; + payloadType?: string; + encoding?: "base64" | "base64url"; +}) { + const payloadType = params.payloadType ?? PAYLOAD_TYPE; + const payloadBytes = Buffer.from(JSON.stringify(params.feed ?? fixtureFeed()), "utf8"); + const payload = payloadBytes.toString(params.encoding ?? "base64url"); + const input = signingInput(payloadType, payloadBytes); return { - envelope: { - schemaVersion: 1, - payloadType, - payload, - signatures: [ - { - keyId: params?.keyId ?? "clawhub-root-2026", - algorithm: "ed25519", - signature, - }, - ], - }, - publicKeyPem: publicKey, - rawPublicKey: publicKeyRawBase64UrlFromEd25519Pem(publicKey), + schemaVersion: 1, + payloadType, + payload, + signatures: params.keys.map((key) => ({ + keyId: key.keyId, + algorithm: "ed25519", + signature: crypto.sign(null, input, key.privateKey).toString("base64url"), + })), }; } describe("official external plugin catalog signed envelopes", () => { - it("uses DSSE pre-authentication encoding for signature input", () => { - expect( - createOfficialExternalPluginCatalogEnvelopeSigningInput({ - payloadType: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE, - payloadBytes: Buffer.from("abc", "utf8"), - }).toString("utf8"), - ).toBe("DSSEv1 49 openclaw.official-external-plugin-catalog-feed.v1 3 abc"); - }); + it.each(["base64", "base64url"] as const)( + "verifies decoded payload bytes from %s envelopes", + (encoding) => { + const key = createSigningKey("catalog-root"); + const result = verifyOfficialExternalPluginCatalogSignedEnvelope( + signedEnvelope({ keys: [key], encoding }), + { trustedKeys: [{ keyId: key.keyId, publicKey: exportPublicKey(key) }] }, + ); - it("verifies a signed ClawHub feed envelope with a trusted PEM key", () => { - const { envelope, publicKeyPem } = signedEnvelope(); - - const result = verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }); - - expect(result).toEqual({ - ok: true, - signedBy: "clawhub-root-2026", - feed: fixtureFeed(), - }); - }); - - it("verifies signatures over decoded payload bytes across base64 encodings", () => { - const feedBytes = Buffer.from(JSON.stringify(fixtureFeed()), "utf8"); - const urlSafePayload = feedBytes.toString("base64url"); - const standardPayload = feedBytes.toString("base64"); - const { envelope, publicKeyPem } = signedEnvelope({ payload: urlSafePayload }); + expect(result).toMatchObject({ ok: true, feed: fixtureFeed(), signedBy: key.keyId }); + }, + ); + it("enforces distinct trusted key material for signature thresholds", () => { + const first = createSigningKey("first"); + const second = createSigningKey("second"); const result = verifyOfficialExternalPluginCatalogSignedEnvelope( + signedEnvelope({ keys: [first, second] }), { - ...envelope, - payload: standardPayload, - }, - { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], + trustedKeys: [ + { keyId: first.keyId, publicKey: exportPublicKey(first) }, + { keyId: second.keyId, publicKey: exportPublicKey(second) }, + ], + threshold: 2, }, ); - expect(result).toEqual({ + expect(result).toMatchObject({ ok: true, - signedBy: "clawhub-root-2026", - feed: fixtureFeed(), - }); - }); - - it("verifies a signed ClawHub feed envelope with a trusted raw base64url key", () => { - const { envelope, rawPublicKey } = signedEnvelope(); - - const result = verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: rawPublicKey }], - }); - - expect(result.ok).toBe(true); - }); - - it("enforces the configured trusted signature threshold", () => { - const first = signedEnvelope({ keyId: "clawhub-root-a" }); - const second = signedEnvelope({ keyId: "clawhub-root-b" }); - const firstSignature = first.envelope.signatures?.[0]; - const secondSignature = second.envelope.signatures?.[0]; - if (!firstSignature || !secondSignature) { - throw new Error("expected signatures"); - } - const envelope = { - ...first.envelope, - signatures: [firstSignature, secondSignature], - }; - - expect( - verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { - trustedKeys: [ - { keyId: "clawhub-root-a", publicKey: first.publicKeyPem }, - { keyId: "clawhub-root-b", publicKey: second.publicKeyPem }, - ], - threshold: 2, - }), - ).toMatchObject({ - ok: true, - signedBy: "clawhub-root-a", - signedByKeyIds: ["clawhub-root-a", "clawhub-root-b"], + signedByKeyIds: ["first", "second"], signatureCount: 2, threshold: 2, }); - expect( - verifyOfficialExternalPluginCatalogSignedEnvelope(first.envelope, { - trustedKeys: [ - { keyId: "clawhub-root-a", publicKey: first.publicKeyPem }, - { keyId: "clawhub-root-b", publicKey: second.publicKeyPem }, - ], - threshold: 2, - }), - ).toMatchObject({ - ok: false, - error: "invalid-signature", - }); - }); - - it("does not count duplicate trust key material toward the signature threshold", () => { - const { envelope, publicKeyPem } = signedEnvelope({ keyId: "clawhub-root-a" }); - const firstSignature = envelope.signatures?.[0]; - if (!firstSignature) { - throw new Error("expected signature"); - } - const result = verifyOfficialExternalPluginCatalogSignedEnvelope( - { - ...envelope, - signatures: [ - firstSignature, - { - ...firstSignature, - keyId: "clawhub-root-b", - }, - ], - }, + const duplicateId = { ...first, keyId: "duplicate-material" }; + const duplicateMaterial = verifyOfficialExternalPluginCatalogSignedEnvelope( + signedEnvelope({ keys: [first, duplicateId] }), { trustedKeys: [ - { keyId: "clawhub-root-a", publicKey: publicKeyPem }, - { keyId: "clawhub-root-b", publicKey: publicKeyPem }, + { keyId: first.keyId, publicKey: exportPublicKey(first) }, + { keyId: duplicateId.keyId, publicKey: exportPublicKey(first) }, ], threshold: 2, }, ); - - expect(result).toMatchObject({ - ok: false, - error: "invalid-signature", - }); + expect(duplicateMaterial).toMatchObject({ ok: false, error: "invalid-signature" }); }); it("rejects payload bytes changed after signing", () => { - const { envelope, publicKeyPem } = signedEnvelope(); - const tamperedFeed = { ...fixtureFeed(), sequence: 43 }; - const result = verifyOfficialExternalPluginCatalogSignedEnvelope( - { - ...envelope, - payload: createOfficialExternalPluginCatalogEnvelopePayload(tamperedFeed), - }, - { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }, - ); + const key = createSigningKey("catalog-root"); + const envelope = signedEnvelope({ keys: [key] }); + envelope.payload = Buffer.from( + JSON.stringify({ ...fixtureFeed(), sequence: 43 }), + "utf8", + ).toString("base64url"); - expect(result).toMatchObject({ - ok: false, - error: "invalid-signature", - }); + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { + trustedKeys: [{ keyId: key.keyId, publicKey: exportPublicKey(key) }], + }), + ).toMatchObject({ ok: false, error: "invalid-signature" }); }); - it("rejects signatures made by an untrusted key id", () => { - const { envelope, publicKeyPem } = signedEnvelope({ keyId: "unknown-key" }); + it("distinguishes unknown keys from invalid trusted signatures", () => { + const signer = createSigningKey("signer"); + const other = createSigningKey("other"); + const envelope = signedEnvelope({ keys: [signer] }); - const result = verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }); - - expect(result).toMatchObject({ - ok: false, - error: "missing-trust-key", - }); + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { + trustedKeys: [{ keyId: other.keyId, publicKey: exportPublicKey(other) }], + }), + ).toMatchObject({ ok: false, error: "missing-trust-key" }); + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { + trustedKeys: [{ keyId: signer.keyId, publicKey: exportPublicKey(other) }], + }), + ).toMatchObject({ ok: false, error: "invalid-signature" }); }); - it("rejects signatures made by the wrong trusted key", () => { - const { envelope } = signedEnvelope(); - const { publicKeyPem: wrongPublicKey } = signedEnvelope(); + it("rejects duplicate key ids and excessive signature lists", () => { + const key = createSigningKey("catalog-root"); + const envelope = signedEnvelope({ keys: [key] }); + const signature = envelope.signatures[0]; + const trustedKeys = [{ keyId: key.keyId, publicKey: exportPublicKey(key) }]; - const result = verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: wrongPublicKey }], - }); - - expect(result).toMatchObject({ - ok: false, - error: "invalid-signature", - }); - }); - - it("rejects duplicate key ids before signature verification", () => { - const { envelope, publicKeyPem } = signedEnvelope(); - const [signature] = envelope.signatures ?? []; - expect(signature).toBeDefined(); - - const result = verifyOfficialExternalPluginCatalogSignedEnvelope( - { - ...envelope, - signatures: [signature!, signature!], - }, - { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }, - ); - - expect(result).toMatchObject({ - ok: false, - error: "invalid-envelope", - }); - }); - - it("rejects excessive signature entries before signature verification", () => { - const { envelope, publicKeyPem } = signedEnvelope(); - const [signature] = envelope.signatures ?? []; - expect(signature).toBeDefined(); - - const result = verifyOfficialExternalPluginCatalogSignedEnvelope( - { - ...envelope, - signatures: Array.from({ length: 17 }, (_, index) => ({ - ...signature!, - keyId: `key-${index}`, - })), - }, - { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }, - ); - - expect(result).toMatchObject({ - ok: false, - error: "invalid-envelope", - }); - }); - - it("rejects excessive raw signature entries before filtering malformed entries", () => { - const { envelope, publicKeyPem } = signedEnvelope(); - const [signature] = envelope.signatures ?? []; - expect(signature).toBeDefined(); - - const result = verifyOfficialExternalPluginCatalogSignedEnvelope( - { - ...envelope, - signatures: [ - ...Array.from({ length: 16 }, () => ({ - algorithm: "ed25519", - })), - signature!, - ], - }, - { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }, - ); - - expect(result).toMatchObject({ - ok: false, - error: "invalid-envelope", - }); - }); - - it("rejects unsupported payload types before trusting the payload", () => { - const { envelope, publicKeyPem } = signedEnvelope({ - payloadType: "openclaw.other-feed.v1", - }); - - const result = verifyOfficialExternalPluginCatalogSignedEnvelope(envelope, { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: publicKeyPem }], - }); - - expect(result).toMatchObject({ - ok: false, - error: "unsupported-payload", - }); - }); - - it("rejects malformed envelopes and invalid feed payloads", () => { expect( verifyOfficialExternalPluginCatalogSignedEnvelope( - { schemaVersion: 1, payloadType: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE }, + { ...envelope, signatures: [signature, signature] }, + { trustedKeys }, + ), + ).toMatchObject({ ok: false, error: "invalid-envelope" }); + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope( + { + ...envelope, + signatures: Array.from({ length: 17 }, (_, index) => ({ + ...signature, + keyId: `key-${index}`, + })), + }, + { trustedKeys }, + ), + ).toMatchObject({ ok: false, error: "invalid-envelope" }); + }); + + it("rejects unsupported payload types and signed invalid feeds", () => { + const key = createSigningKey("catalog-root"); + const trustedKeys = [{ keyId: key.keyId, publicKey: exportPublicKey(key) }]; + + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope( + signedEnvelope({ keys: [key], payloadType: "example.unsupported" }), + { trustedKeys }, + ), + ).toMatchObject({ ok: false, error: "unsupported-payload" }); + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope( + signedEnvelope({ keys: [key], feed: { entries: [] } }), + { trustedKeys }, + ), + ).toMatchObject({ ok: false, error: "invalid-payload" }); + }); + + it("rejects malformed envelopes before verification", () => { + expect( + verifyOfficialExternalPluginCatalogSignedEnvelope( + { schemaVersion: 1, payloadType: PAYLOAD_TYPE, payload: "", signatures: [] }, { trustedKeys: [] }, ), - ).toMatchObject({ - ok: false, - error: "invalid-envelope", - }); - - const malformedPayload = signedEnvelope({ - payload: Buffer.from(JSON.stringify({ schemaVersion: 99 }), "utf8").toString("base64url"), - }); - expect( - verifyOfficialExternalPluginCatalogSignedEnvelope(malformedPayload.envelope, { - trustedKeys: [{ keyId: "clawhub-root-2026", publicKey: malformedPayload.publicKeyPem }], - }), - ).toMatchObject({ - ok: false, - error: "invalid-payload", - }); + ).toMatchObject({ ok: false, error: "invalid-envelope" }); }); }); diff --git a/src/plugins/official-external-plugin-catalog-envelope.ts b/src/plugins/official-external-plugin-catalog-envelope.ts index da3c442b8c32..45bcfee62828 100644 --- a/src/plugins/official-external-plugin-catalog-envelope.ts +++ b/src/plugins/official-external-plugin-catalog-envelope.ts @@ -8,29 +8,21 @@ import { type OfficialExternalPluginCatalogFeed, } from "./official-external-plugin-catalog.js"; -export const OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE = +const OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE = "openclaw.official-external-plugin-catalog-feed.v1"; const OFFICIAL_EXTERNAL_PLUGIN_CATALOG_MAX_SIGNATURES = 16; -export type OfficialExternalPluginCatalogEnvelopeSignature = { +type OfficialExternalPluginCatalogEnvelopeSignature = { keyId?: string; algorithm?: string; signature?: string; }; - -export type OfficialExternalPluginCatalogSignedEnvelope = { - schemaVersion?: number; - payloadType?: string; - payload?: string; - signatures?: readonly OfficialExternalPluginCatalogEnvelopeSignature[]; -}; - -export type OfficialExternalPluginCatalogTrustedSigningKey = { +type OfficialExternalPluginCatalogTrustedSigningKey = { keyId: string; publicKey: string; }; -export type OfficialExternalPluginCatalogEnvelopeVerificationResult = +type OfficialExternalPluginCatalogEnvelopeVerificationResult = | { ok: true; feed: OfficialExternalPluginCatalogFeed; @@ -49,14 +41,7 @@ export type OfficialExternalPluginCatalogEnvelopeVerificationResult = | "invalid-signature"; message: string; }; - -export function createOfficialExternalPluginCatalogEnvelopePayload( - feed: OfficialExternalPluginCatalogFeed, -): string { - return Buffer.from(JSON.stringify(feed), "utf8").toString("base64url"); -} - -export function createOfficialExternalPluginCatalogEnvelopeSigningInput(params: { +function createOfficialExternalPluginCatalogEnvelopeSigningInput(params: { payloadType: string; payloadBytes: Buffer; }): Buffer { diff --git a/src/plugins/official-external-plugin-catalog-snapshot-store.ts b/src/plugins/official-external-plugin-catalog-snapshot-store.ts index 5f7a0f74623e..9e8cec6fcf97 100644 --- a/src/plugins/official-external-plugin-catalog-snapshot-store.ts +++ b/src/plugins/official-external-plugin-catalog-snapshot-store.ts @@ -20,7 +20,7 @@ import type { HostedOfficialExternalPluginCatalogTrustState, } from "./official-external-plugin-catalog.js"; -export type HostedOfficialExternalPluginCatalogSnapshotStoreOptions = { +type HostedOfficialExternalPluginCatalogSnapshotStoreOptions = { env?: NodeJS.ProcessEnv; stateDir?: string; stateDatabasePath?: string; diff --git a/src/plugins/official-external-plugin-catalog.test.ts b/src/plugins/official-external-plugin-catalog.test.ts index 086afda87ab0..60e83c62cbee 100644 --- a/src/plugins/official-external-plugin-catalog.test.ts +++ b/src/plugins/official-external-plugin-catalog.test.ts @@ -2,38 +2,26 @@ import crypto from "node:crypto"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; -import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" }; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; -import { - OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE, - createOfficialExternalPluginCatalogEnvelopePayload, - createOfficialExternalPluginCatalogEnvelopeSigningInput, - type OfficialExternalPluginCatalogSignedEnvelope, -} from "./official-external-plugin-catalog-envelope.js"; import { createSqliteHostedOfficialExternalPluginCatalogSnapshotStore } from "./official-external-plugin-catalog-snapshot-store.js"; import { + type HostedOfficialExternalPluginCatalogSnapshot, + type HostedOfficialExternalPluginCatalogSnapshotStore, type OfficialExternalPluginCatalogEntry, - DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL, - createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore, + type OfficialExternalPluginCatalogFeed, getOfficialExternalPluginCatalogEntry, getOfficialExternalPluginCatalogManifest, isOfficialExternalPluginCatalogFeed, - filterOfficialExternalPluginCatalogEntriesBySourceRefs, listOfficialExternalPluginCatalogEntries, loadConfiguredHostedOfficialExternalPluginCatalogEntries, - loadHostedOfficialExternalPluginCatalogEntries, - parseOfficialExternalPluginCatalogEntries, resolveOfficialExternalProviderContractPluginIds, resolveOfficialExternalProviderPluginIds, resolveOfficialExternalProviderPluginIdsForEnv, resolveOfficialExternalWebProviderContractPluginIdsForEnv, resolveOfficialExternalPluginId, resolveOfficialExternalPluginInstall, - resolveOfficialExternalPluginCatalogProfileConfigFromConfig, - validateOfficialExternalPluginCatalogEntrySourceRefs, - type OfficialExternalPluginCatalogFeed, } from "./official-external-plugin-catalog.js"; function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry { @@ -44,71 +32,130 @@ function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry { return entry; } -function expectRequestUrl(input: RequestInfo | URL): string { - return typeof input === "string" ? input : input instanceof URL ? input.href : input.url; +const HOSTED_CATALOG_PAYLOAD_TYPE = "openclaw.official-external-plugin-catalog-feed.v1"; + +type HostedCatalogConfig = NonNullable< + NonNullable< + Parameters[0] + >["marketplaces"] +>; +type ConfiguredHostedCatalogLoadParams = NonNullable< + Parameters[1] +>; +type HostedCatalogLoadParams = ConfiguredHostedCatalogLoadParams & { + catalogConfig?: HostedCatalogConfig; +}; + +function loadHostedCatalog( + params: HostedCatalogLoadParams = {}, +): ReturnType { + const { catalogConfig, ...loadParams } = params; + return loadConfiguredHostedOfficialExternalPluginCatalogEntries( + catalogConfig ? { marketplaces: catalogConfig } : undefined, + loadParams, + ); } -function signedHostedCatalogFeed(params?: { - feed?: OfficialExternalPluginCatalogFeed; - keyId?: string; - privateKeyPem?: string; -}): { - body: string; - publicKeyPem: string; - privateKeyPem: string; -} { - const { publicKey, privateKey } = - params?.privateKeyPem === undefined - ? crypto.generateKeyPairSync("ed25519", { - publicKeyEncoding: { type: "spki", format: "pem" }, - privateKeyEncoding: { type: "pkcs8", format: "pem" }, - }) - : { - publicKey: crypto - .createPublicKey(params.privateKeyPem) - .export({ type: "spki", format: "pem" }) as string, - privateKey: params.privateKeyPem, - }; - const payload = createOfficialExternalPluginCatalogEnvelopePayload( - params?.feed ?? { - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 8, - entries: [ - { - name: "@openclaw/signed-refresh-proof", - kind: "plugin", - openclaw: { - plugin: { id: "signed-refresh-proof" }, - install: { sourceRef: "acme-npm", npmSpec: "@openclaw/signed-refresh-proof" }, - }, - }, - ], +function createInMemoryHostedCatalogSnapshotStore( + initialSnapshots: HostedOfficialExternalPluginCatalogSnapshot[] = [], +): HostedOfficialExternalPluginCatalogSnapshotStore { + const snapshots = new Map(); + for (const snapshot of initialSnapshots) { + snapshots.set(snapshot.metadata.url, snapshot); + } + return { + async read(url) { + return snapshots.get(url) ?? null; }, - ); - const signingInput = createOfficialExternalPluginCatalogEnvelopeSigningInput({ - payloadType: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE, - payloadBytes: Buffer.from(payload, "base64"), - }); - const envelope: OfficialExternalPluginCatalogSignedEnvelope = { + async write(snapshot) { + snapshots.set(snapshot.metadata.url, snapshot); + }, + }; +} + +function hostedCatalogFeed(params: { + sequence: number; + pluginName: string; +}): OfficialExternalPluginCatalogFeed { + const pluginId = params.pluginName.replace(/^@[^/]+\//u, ""); + return { schemaVersion: 1, - payloadType: OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PAYLOAD_TYPE, - payload, - signatures: [ + id: "openclaw-official-external-plugins", + generatedAt: `2026-06-22T00:00:${String(params.sequence).padStart(2, "0")}.000Z`, + sequence: params.sequence, + entries: [ { - keyId: params?.keyId ?? "acme-root", - algorithm: "ed25519", - signature: crypto - .sign(null, signingInput, crypto.createPrivateKey(privateKey)) - .toString("base64url"), + name: params.pluginName, + kind: "plugin", + openclaw: { + plugin: { id: pluginId }, + install: { sourceRef: "acme-npm", npmSpec: params.pluginName }, + }, }, ], }; +} + +function signedHostedCatalogFeed(params: { + feed: OfficialExternalPluginCatalogFeed; + privateKeyPem?: string; +}): { body: string; privateKeyPem: string; publicKeyPem: string } { + const keys = params.privateKeyPem + ? { + privateKeyPem: params.privateKeyPem, + publicKeyPem: crypto + .createPublicKey(params.privateKeyPem) + .export({ type: "spki", format: "pem" }), + } + : (() => { + const generated = crypto.generateKeyPairSync("ed25519", { + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, + }); + return { privateKeyPem: generated.privateKey, publicKeyPem: generated.publicKey }; + })(); + const payloadBytes = Buffer.from(JSON.stringify(params.feed), "utf8"); + const payloadTypeBytes = Buffer.from(HOSTED_CATALOG_PAYLOAD_TYPE, "utf8"); + const signingInput = Buffer.concat([ + Buffer.from( + `DSSEv1 ${payloadTypeBytes.length} ${HOSTED_CATALOG_PAYLOAD_TYPE} ${payloadBytes.length} `, + "utf8", + ), + payloadBytes, + ]); return { - body: JSON.stringify(envelope), - publicKeyPem: publicKey, - privateKeyPem: privateKey, + body: JSON.stringify({ + schemaVersion: 1, + payloadType: HOSTED_CATALOG_PAYLOAD_TYPE, + payload: payloadBytes.toString("base64url"), + signatures: [ + { + keyId: "acme-root", + algorithm: "ed25519", + signature: crypto + .sign(null, signingInput, crypto.createPrivateKey(keys.privateKeyPem)) + .toString("base64url"), + }, + ], + }), + ...keys, + }; +} + +function signedCatalogConfig(publicKeyPem: string): HostedCatalogConfig { + return { + feeds: { + acme: { + url: "https://packages.acme.example/openclaw/feed", + verification: { + mode: "signed", + keys: [{ keyId: "acme-root", publicKey: publicKeyPem }], + }, + }, + }, + sources: { + "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" }, + }, }; } @@ -197,1419 +244,6 @@ describe("official external plugin catalog", () => { ).toBe(true); }); - it("accepts live ClawHub marketplace entries with trusted install candidates", () => { - const [entry] = parseOfficialExternalPluginCatalogEntries({ - schemaVersion: 2, - id: "clawhub-official", - generatedAt: "2026-06-25T01:19:39.629Z", - sequence: 11, - entries: [ - { - type: "plugin", - id: "@expediagroup/expedia-openclaw", - title: "Expedia Travel", - version: "1.0.4", - state: "available", - publisher: { - id: "expediagroup", - trust: "official", - }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@expediagroup/expedia-openclaw", - version: "1.0.4", - integrity: - "sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9", - }, - ], - }, - }, - ], - }); - - if (entry === undefined) { - throw new Error("Expected hosted ClawHub feed entry to parse"); - } - - expect(entry).toMatchObject({ - id: "@expediagroup/expedia-openclaw", - title: "Expedia Travel", - version: "1.0.4", - }); - expect(resolveOfficialExternalPluginId(entry)).toBe("@expediagroup/expedia-openclaw"); - expect(resolveOfficialExternalPluginInstall(entry)).toEqual({ - clawhubSpec: "clawhub:@expediagroup/expedia-openclaw@1.0.4", - defaultChoice: "clawhub", - expectedIntegrity: "sha256-s1XdoEQDvsqri7qwaf0eewV4Ji50WeWYzFsZYVtb2rk=", - }); - }); - - it("does not synthesize trusted installs for unavailable or untrusted hosted entries", () => { - const entries = parseOfficialExternalPluginCatalogEntries({ - schemaVersion: 2, - id: "clawhub-official", - generatedAt: "2026-06-25T01:19:39.629Z", - sequence: 11, - entries: [ - { - type: "plugin", - id: "@example/unavailable", - title: "Unavailable", - version: "1.0.0", - state: "disabled", - publisher: { id: "example", trust: "official" }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@example/unavailable", - version: "1.0.0", - }, - ], - }, - }, - { - type: "plugin", - id: "@example/community", - title: "Community", - version: "1.0.0", - state: "available", - publisher: { id: "example", trust: "community" }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@example/community", - version: "1.0.0", - }, - ], - }, - }, - { - type: "plugin", - id: "@example/missing-state", - title: "Missing State", - version: "1.0.0", - publisher: { id: "example", trust: "official" }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@example/missing-state", - version: "1.0.0", - }, - ], - }, - }, - { - type: "plugin", - id: "@example/missing-trust", - title: "Missing Trust", - version: "1.0.0", - state: "available", - publisher: { id: "example" }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@example/missing-trust", - version: "1.0.0", - }, - ], - }, - }, - { - type: "plugin", - id: "@example/missing-publisher", - title: "Missing Publisher", - version: "1.0.0", - state: "available", - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@example/missing-publisher", - version: "1.0.0", - }, - ], - }, - }, - { - type: "plugin", - id: "@example/private-source", - title: "Private Source", - version: "1.0.0", - state: "available", - publisher: { id: "example", trust: "official" }, - install: { - candidates: [ - { - sourceRef: "private-feed", - package: "@example/private-source", - version: "1.0.0", - }, - ], - }, - }, - ], - }); - - expect(entries).toHaveLength(6); - for (const entry of entries) { - expect(resolveOfficialExternalPluginId(entry)).toBe(entry.id); - expect(resolveOfficialExternalPluginInstall(entry)).toBeNull(); - } - }); - - it("keeps unsupported versioned feed wrappers out of legacy catalog parsing", () => { - expect( - parseOfficialExternalPluginCatalogEntries({ - schemaVersion: 3, - id: "future-feed", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 1, - entries: [{ name: "should-not-load" }], - }), - ).toEqual([]); - expect( - parseOfficialExternalPluginCatalogEntries({ - entries: [{ name: "legacy-catalog-entry" }], - }), - ).toEqual([{ name: "legacy-catalog-entry" }]); - }); - - it("loads a hosted feed with conditional headers and checksum metadata", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 2, - entries: [ - { - name: "@openclaw/hosted-proof", - kind: "plugin", - openclaw: { - plugin: { id: "hosted-proof", label: "Hosted Proof" }, - install: { npmSpec: "@openclaw/hosted-proof", defaultChoice: "npm" }, - }, - }, - ], - }); - const fetchImpl = vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => { - const headers = new Headers(init?.headers); - expect(headers.get("if-none-match")).toBe('"old"'); - expect(headers.get("if-modified-since")).toBe("Mon, 22 Jun 2026 00:00:00 GMT"); - return new Response(body, { - status: 200, - headers: { - etag: '"next"', - "last-modified": "Mon, 22 Jun 2026 01:00:00 GMT", - "content-length": String(new TextEncoder().encode(body).byteLength), - }, - }); - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - fetchImpl, - ifNoneMatch: '"old"', - ifModifiedSince: "Mon, 22 Jun 2026 00:00:00 GMT", - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/hosted-proof"]); - if (result.source === "hosted") { - expect(result.feed.sequence).toBe(2); - expect(result.metadata).toMatchObject({ - status: 200, - etag: '"next"', - lastModified: "Mon, 22 Jun 2026 01:00:00 GMT", - }); - expect(result.metadata.checksum).toMatch(/^sha256:[0-9a-f]{64}$/); - } - }); - - it("keeps live ClawHub metadata-only entries after hosted feed loading", async () => { - const body = JSON.stringify({ - schemaVersion: 2, - id: "clawhub-official", - generatedAt: "2026-06-25T01:19:39.629Z", - sequence: 11, - entries: [ - { - type: "plugin", - id: "@expediagroup/expedia-openclaw", - title: "Expedia Travel", - version: "1.0.4", - state: "available", - publisher: { - id: "expediagroup", - trust: "official", - }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@expediagroup/expedia-openclaw", - version: "1.0.4", - integrity: - "sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9", - }, - ], - }, - }, - ], - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - fetchImpl: vi.fn( - async () => - new Response(body, { - status: 200, - headers: { - "content-length": String(new TextEncoder().encode(body).byteLength), - }, - }), - ), - }); - - expect(result.source).toBe("hosted"); - expect(result.entries).toHaveLength(1); - expect(result.entries[0]).toMatchObject({ - id: "@expediagroup/expedia-openclaw", - title: "Expedia Travel", - version: "1.0.4", - }); - }); - - it("uses the default local feed profile for hosted catalog loading", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 8, - entries: [ - { - name: "@openclaw/default-profile-proof", - kind: "plugin", - openclaw: { plugin: { id: "default-profile-proof" } }, - }, - ], - }); - const fetchImpl = vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe(DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL); - return new Response(body, { status: 200 }); - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "clawhub-public", - catalogConfig: { - sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } }, - }, - fetchImpl, - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/default-profile-proof"]); - }); - - it("accepts the live ClawHub feed source ref by default", async () => { - const body = JSON.stringify({ - schemaVersion: 2, - id: "clawhub-official", - generatedAt: "2026-06-23T09:38:53.000Z", - sequence: 4, - entries: [ - { - type: "plugin", - id: "@openclaw/live-feed-proof", - title: "Live Feed Proof", - version: "1.0.0", - state: "available", - publisher: { id: "openclaw", trust: "official" }, - install: { - candidates: [ - { - sourceRef: "public-clawhub", - package: "@openclaw/live-feed-proof", - version: "1.0.0", - integrity: - "sha256:b355dda04403becaab8bbab069fd1e7b0578262e7459e598cc5b19615b5bdab9", - }, - ], - }, - }, - ], - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - fetchImpl: vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe(DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL); - return new Response(body, { status: 200 }); - }), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.id)).toEqual(["@openclaw/live-feed-proof"]); - expect( - resolveOfficialExternalPluginInstall( - expectDefined(result.entries[0], "result.entries[0] test invariant"), - ), - ).toEqual({ - clawhubSpec: "clawhub:@openclaw/live-feed-proof@1.0.0", - defaultChoice: "clawhub", - expectedIntegrity: "sha256-s1XdoEQDvsqri7qwaf0eewV4Ji50WeWYzFsZYVtb2rk=", - }); - }); - - it("loads hosted catalog profiles from OpenClaw config", async () => { - const config = { - marketplaces: { - feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } }, - sources: { "acme-npm": { type: "npm" as const } }, - }, - }; - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 14, - entries: [ - { - name: "@acme/config-profile-proof", - kind: "plugin", - openclaw: { - plugin: { id: "config-profile-proof" }, - install: { sourceRef: "acme-npm", npmSpec: "@acme/config-profile-proof" }, - }, - }, - ], - }); - - expect(resolveOfficialExternalPluginCatalogProfileConfigFromConfig(config)).toBe( - config.marketplaces, - ); - - const result = await loadConfiguredHostedOfficialExternalPluginCatalogEntries(config, { - feedProfile: "acme", - fetchImpl: vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe("https://packages.acme.example/openclaw/feed"); - return new Response(body, { status: 200 }); - }), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/config-profile-proof"]); - }); - - it("allows named local feed profiles to authorize their configured HTTPS host", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 9, - entries: [ - { - name: "@acme/private-proof", - kind: "plugin", - install: { - candidates: [ - { - sourceRef: "acme-npm", - package: "@acme/private-proof", - version: "1.0.0", - }, - ], - }, - openclaw: { - plugin: { id: "private-proof" }, - install: { sourceRef: "acme-npm", npmSpec: "@acme/private-proof" }, - }, - }, - ], - }); - const fetchImpl = vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe("https://packages.acme.example/openclaw/feed"); - return new Response(body, { status: 200 }); - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig: { - feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } }, - sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } }, - }, - fetchImpl, - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/private-proof"]); - }); - - it("loads and snapshots a signed hosted feed profile after envelope verification", async () => { - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(); - const writeSpy = vi.spyOn(snapshotStore, "write"); - const signed = signedHostedCatalogFeed(); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig: { - feeds: { - acme: { - url: "https://packages.acme.example/openclaw/feed", - verification: { - mode: "signed", - keys: [ - { - keyId: "acme-root", - publicKey: signed.publicKeyPem, - }, - ], - }, - }, - }, - sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } }, - }, - fetchImpl: vi.fn(async () => new Response(signed.body, { status: 200 })), - now: () => new Date("2026-06-22T04:05:06.000Z"), - snapshotStore, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/signed-refresh-proof"]); - if (result.source === "hosted") { - expect(result.trust).toEqual({ - mode: "signed", - signedBy: "acme-root", - signatureCount: 1, - threshold: 1, - verifiedAt: "2026-06-22T04:05:06.000Z", - }); - } - expect(writeSpy).toHaveBeenCalledTimes(1); - await expect( - snapshotStore.read("https://packages.acme.example/openclaw/feed"), - ).resolves.toMatchObject({ - body: signed.body, - trust: { - mode: "signed", - signedBy: "acme-root", - signatureCount: 1, - threshold: 1, - verifiedAt: "2026-06-22T04:05:06.000Z", - }, - }); - }); - - it("rejects signed hosted feed rollback before replacing snapshots", async () => { - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(); - const newer = signedHostedCatalogFeed({ - feed: { - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:10.000Z", - sequence: 10, - entries: [ - { - name: "@openclaw/signed-refresh-proof-v10", - kind: "plugin", - openclaw: { - plugin: { id: "signed-refresh-proof-v10" }, - install: { sourceRef: "acme-npm", npmSpec: "@openclaw/signed-refresh-proof-v10" }, - }, - }, - ], - }, - }); - const older = signedHostedCatalogFeed({ - privateKeyPem: newer.privateKeyPem, - feed: { - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:09.000Z", - sequence: 9, - entries: [ - { - name: "@openclaw/signed-refresh-proof-v9", - kind: "plugin", - openclaw: { - plugin: { id: "signed-refresh-proof-v9" }, - install: { sourceRef: "acme-npm", npmSpec: "@openclaw/signed-refresh-proof-v9" }, - }, - }, - ], - }, - }); - const catalogConfig = { - feeds: { - acme: { - url: "https://packages.acme.example/openclaw/feed", - verification: { - mode: "signed" as const, - keys: [{ keyId: "acme-root", publicKey: newer.publicKeyPem }], - }, - }, - }, - sources: { - "acme-npm": { type: "npm" as const, registry: "https://packages.acme.example/npm/" }, - }, - }; - - await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig, - fetchImpl: vi.fn(async () => new Response(newer.body, { status: 200 })), - snapshotStore, - }); - const writeSpy = vi.spyOn(snapshotStore, "write"); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig, - fetchImpl: vi.fn(async () => new Response(older.body, { status: 200 })), - snapshotStore, - }); - - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@openclaw/signed-refresh-proof-v10", - ]); - if (result.source === "hosted-snapshot") { - expect(result.error).toContain("signed feed sequence is older"); - expect(result.feed.sequence).toBe(10); - } - expect(writeSpy).not.toHaveBeenCalled(); - }); - - it("keeps signed SQLite snapshot replacement monotonic under concurrent writes", async () => { - const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-signed-snapshot-race-")); - const url = "https://packages.acme.example/openclaw/feed"; - const newer = signedHostedCatalogFeed({ - feed: { - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:10.000Z", - sequence: 10, - entries: [ - { - name: "@openclaw/signed-refresh-proof-v10", - kind: "plugin", - openclaw: { plugin: { id: "signed-refresh-proof-v10" } }, - }, - ], - }, - }); - const older = signedHostedCatalogFeed({ - privateKeyPem: newer.privateKeyPem, - feed: { - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:09.000Z", - sequence: 9, - entries: [ - { - name: "@openclaw/signed-refresh-proof-v9", - kind: "plugin", - openclaw: { plugin: { id: "signed-refresh-proof-v9" } }, - }, - ], - }, - }); - const snapshotStore = createSqliteHostedOfficialExternalPluginCatalogSnapshotStore({ - stateDir: tempDir, - }); - const trust = { - mode: "signed" as const, - signedBy: "acme-root", - signatureCount: 1, - threshold: 1, - verifiedAt: "2026-06-22T04:05:06.000Z", - }; - const snapshotFor = (body: string, sequence: number, generatedAt: string) => ({ - body, - metadata: { - url, - status: 200, - checksum: "sha256:" + crypto.createHash("sha256").update(body).digest("hex"), - }, - savedAt: "2026-06-22T04:05:06.000Z", - trust, - monotonic: { mode: "signed-feed" as const, sequence, generatedAt }, - }); - - try { - await Promise.allSettled([ - snapshotStore.write(snapshotFor(older.body, 9, "2026-06-22T00:00:09.000Z")), - snapshotStore.write(snapshotFor(newer.body, 10, "2026-06-22T00:00:10.000Z")), - ]); - - const snapshot = await snapshotStore.read(url); - expect(snapshot?.body).toBe(newer.body); - expect(snapshot?.trust).toMatchObject(trust); - } finally { - closeOpenClawStateDatabaseForTest(); - rmSync(tempDir, { recursive: true, force: true }); - } - }); - - it("fails closed when a signed feed profile receives unsigned JSON", async () => { - const fetchImpl = vi.fn( - async () => - new Response( - JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 8, - entries: [], - }), - { status: 200 }, - ), - ); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig: { - feeds: { - acme: { - url: "https://packages.acme.example/openclaw/feed", - verification: { - mode: "signed", - keys: [ - { - keyId: "acme-root", - publicKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - }, - ], - }, - }, - }, - }, - fetchImpl, - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - }); - - expect(fetchImpl).toHaveBeenCalledTimes(1); - expect(result.source).toBe("bundled-fallback"); - expect(result.entries).toEqual([]); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("signed envelope is malformed"); - } - }); - - it("preserves signed feed profile verification for direct URL overrides", async () => { - const signed = signedHostedCatalogFeed(); - const fetchImpl = vi.fn( - async () => - new Response( - JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 8, - entries: [], - }), - { status: 200 }, - ), - ); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - feedUrl: "https://clawhub.ai/v1/feeds/plugins", - catalogConfig: { - feeds: { - acme: { - url: "https://packages.acme.example/openclaw/feed", - verification: { - mode: "signed", - keys: [{ keyId: "acme-root", publicKey: signed.publicKeyPem }], - }, - }, - }, - }, - fetchImpl, - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - }); - - expect(fetchImpl).toHaveBeenCalledTimes(1); - expect(result.source).toBe("bundled-fallback"); - expect(result.entries).toEqual([]); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("signed envelope is malformed"); - } - }); - - it("keeps direct hosted feed URL overrides constrained to the public allowlist", async () => { - const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 })); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedUrl: "https://packages.acme.example/openclaw/feed", - fetchImpl, - snapshotStore: null, - }); - - expect(result.source).toBe("bundled-fallback"); - expect(fetchImpl).not.toHaveBeenCalled(); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("hostname is not allowed"); - } - }); - - it("rejects credential-bearing direct hosted feed URL overrides", async () => { - const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 })); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedUrl: "https://user:secret@clawhub.ai/v1/feeds/plugins", - fetchImpl, - snapshotStore: null, - }); - - expect(result.source).toBe("bundled-fallback"); - expect(fetchImpl).not.toHaveBeenCalled(); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("must not include credentials"); - } - }); - - it("rejects query or fragment-bearing direct hosted feed URL overrides", async () => { - for (const feedUrl of [ - "https://clawhub.ai/v1/feeds/plugins?token=secret", - "https://clawhub.ai/v1/feeds/plugins#fragment", - ]) { - const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 })); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedUrl, - fetchImpl, - snapshotStore: null, - }); - - expect(result.source).toBe("bundled-fallback"); - expect(fetchImpl).not.toHaveBeenCalled(); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("must not include query strings or fragments"); - } - } - }); - - it("requires manifest install source refs when the default feed profile URL is overridden", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 13, - entries: [ - { - name: "@acme/default-override-missing-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "default-override-missing-source-ref" }, - install: { npmSpec: "@acme/default-override-missing-source-ref" }, - }, - }, - { - name: "@acme/default-override-known-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "default-override-known-source-ref" }, - install: { sourceRef: "acme-npm", npmSpec: "@acme/default-override-known-source-ref" }, - }, - }, - ], - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - catalogConfig: { - feeds: { "clawhub-public": { url: "https://packages.acme.example/openclaw/feed" } }, - sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } }, - }, - fetchImpl: vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe("https://packages.acme.example/openclaw/feed"); - return new Response(body, { status: 200 }); - }), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@acme/default-override-known-source-ref", - ]); - }); - - it("preserves default feed manifest installs for direct default hosted feed URL refreshes", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 14, - entries: [ - { - name: "@openclaw/direct-default-missing-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "direct-default-missing-source-ref" }, - install: { npmSpec: "@openclaw/direct-default-missing-source-ref" }, - }, - }, - ], - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedUrl: "https://clawhub.ai/v1/feeds/plugins", - fetchImpl: vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe("https://clawhub.ai/v1/feeds/plugins"); - return new Response(body, { status: 200 }); - }), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@openclaw/direct-default-missing-source-ref", - ]); - }); - - it("requires manifest install source refs for non-default direct hosted feed URL overrides", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 15, - entries: [ - { - name: "@acme/direct-url-missing-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "direct-url-missing-source-ref" }, - install: { npmSpec: "@acme/direct-url-missing-source-ref" }, - }, - }, - { - name: "@acme/direct-url-known-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "direct-url-known-source-ref" }, - install: { sourceRef: "acme-npm", npmSpec: "@acme/direct-url-known-source-ref" }, - }, - }, - ], - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedUrl: "https://clawhub.ai/v1/feeds/acme", - catalogConfig: { - sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } }, - }, - fetchImpl: vi.fn(async (url: RequestInfo | URL) => { - expect(expectRequestUrl(url)).toBe("https://clawhub.ai/v1/feeds/acme"); - return new Response(body, { status: 200 }); - }), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@acme/direct-url-known-source-ref", - ]); - }); - - it("requires manifest install source refs for custom local feed profiles", async () => { - const missingManifestSourceRef = { - name: "@acme/missing-manifest-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "missing-manifest-source-ref" }, - install: { npmSpec: "@acme/missing-manifest-source-ref" }, - }, - }; - const knownManifestSourceRef = { - name: "@acme/known-manifest-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "known-manifest-source-ref" }, - install: { - sourceRef: "acme-npm", - npmSpec: "@acme/known-manifest-source-ref", - }, - }, - }; - const implicitNameInstall = { - name: "@acme/implicit-name-install", - kind: "plugin", - openclaw: { plugin: { id: "implicit-name-install" } }, - }; - const topLevelCandidateOnly = { - name: "@acme/top-level-candidate-only", - kind: "plugin", - install: { - candidates: [{ sourceRef: "acme-npm", package: "@acme/top-level-candidate-only" }], - }, - openclaw: { - plugin: { id: "top-level-candidate-only" }, - install: { npmSpec: "@acme/top-level-candidate-only" }, - }, - }; - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 11, - entries: [ - missingManifestSourceRef, - implicitNameInstall, - topLevelCandidateOnly, - knownManifestSourceRef, - ], - }); - - const catalogConfig = { - feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } }, - sources: { "acme-npm": { type: "npm" as const } }, - }; - - expect( - validateOfficialExternalPluginCatalogEntrySourceRefs(missingManifestSourceRef, { - catalogConfig, - requireManifestInstallSourceRef: true, - }), - ).toEqual(["feed install candidate is missing sourceRef"]); - expect( - validateOfficialExternalPluginCatalogEntrySourceRefs(implicitNameInstall, { - catalogConfig, - requireManifestInstallSourceRef: true, - }), - ).toEqual(["feed install candidate is missing sourceRef"]); - expect( - validateOfficialExternalPluginCatalogEntrySourceRefs(topLevelCandidateOnly, { - catalogConfig, - requireManifestInstallSourceRef: true, - }), - ).toEqual(["feed install candidate is missing sourceRef"]); - expect( - validateOfficialExternalPluginCatalogEntrySourceRefs(knownManifestSourceRef, { - catalogConfig, - requireManifestInstallSourceRef: true, - }), - ).toEqual([]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig, - fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/known-manifest-source-ref"]); - }); - - it("filters hosted feed entries that reference unknown local source profiles", async () => { - const knownEntry = { - name: "@openclaw/source-ref-known", - kind: "plugin", - install: { - candidates: [{ sourceRef: "public-clawhub", package: "@openclaw/source-ref-known" }], - }, - openclaw: { plugin: { id: "source-ref-known" } }, - }; - const unknownEntry = { - name: "@openclaw/source-ref-unknown", - kind: "plugin", - install: { - candidates: [{ sourceRef: "attacker-npm", package: "@openclaw/source-ref-unknown" }], - }, - openclaw: { plugin: { id: "source-ref-unknown" } }, - }; - const missingEntry = { - name: "@openclaw/source-ref-missing", - kind: "plugin", - install: { candidates: [{ package: "@openclaw/source-ref-missing" }] }, - openclaw: { plugin: { id: "source-ref-missing" } }, - }; - const manifestInstallWithoutSourceRef = { - name: "@openclaw/source-ref-manifest-missing", - kind: "plugin", - install: { - candidates: [ - { sourceRef: "public-clawhub", package: "@openclaw/source-ref-manifest-missing" }, - ], - }, - openclaw: { - plugin: { id: "source-ref-manifest-missing" }, - install: { npmSpec: "@openclaw/source-ref-manifest-missing" }, - }, - }; - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 10, - entries: [knownEntry, unknownEntry, missingEntry, manifestInstallWithoutSourceRef], - }); - - expect(validateOfficialExternalPluginCatalogEntrySourceRefs(knownEntry)).toEqual([]); - expect(validateOfficialExternalPluginCatalogEntrySourceRefs(unknownEntry)).toEqual([ - 'feed install candidate references unknown sourceRef "attacker-npm"', - ]); - expect(validateOfficialExternalPluginCatalogEntrySourceRefs(missingEntry)).toEqual([ - "feed install candidate is missing sourceRef", - ]); - expect( - validateOfficialExternalPluginCatalogEntrySourceRefs(manifestInstallWithoutSourceRef), - ).toEqual([]); - expect( - filterOfficialExternalPluginCatalogEntriesBySourceRefs([ - knownEntry, - unknownEntry, - missingEntry, - manifestInstallWithoutSourceRef, - ]).map((entry) => entry.name), - ).toEqual(["@openclaw/source-ref-known", "@openclaw/source-ref-manifest-missing"]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), - snapshotStore: null, - }); - - expect(result.source).toBe("hosted"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@openclaw/source-ref-known", - "@openclaw/source-ref-manifest-missing", - ]); - }); - - it("falls back to the bundled catalog when hosted feed validation fails", async () => { - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, - fetchImpl: vi.fn( - async () => - new Response(JSON.stringify({ schemaVersion: 1, id: " ", entries: [] }), { - status: 200, - }), - ), - }); - - expect(result.source).toBe("bundled-fallback"); - expect(result.entries.length).toBe(listOfficialExternalPluginCatalogEntries().length); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("supported schema version"); - expect(result.metadata?.checksum).toMatch(/^sha256:[0-9a-f]{64}$/); - } - }); - - it("falls back to the bundled catalog on HTTP 304 until a snapshot cache exists", async () => { - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, - fetchImpl: vi.fn( - async () => - new Response(null, { - status: 304, - headers: { etag: '"same"', "last-modified": "Mon, 22 Jun 2026 01:00:00 GMT" }, - }), - ), - }); - - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("HTTP 304"); - expect(result.metadata).toMatchObject({ - status: 304, - etag: '"same"', - lastModified: "Mon, 22 Jun 2026 01:00:00 GMT", - }); - } - }); - - it("writes a validated hosted feed snapshot after a successful fetch", async () => { - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(); - const writeSpy = vi.spyOn(snapshotStore, "write"); - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 3, - entries: [ - { - name: "@openclaw/snapshot-write-proof", - kind: "plugin", - openclaw: { plugin: { id: "snapshot-write-proof" } }, - }, - ], - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - now: () => new Date("2026-06-22T01:02:03.000Z"), - fetchImpl: vi.fn( - async () => - new Response(body, { - status: 200, - headers: { etag: '"fresh"' }, - }), - ), - }); - - expect(result.source).toBe("hosted"); - expect(writeSpy).toHaveBeenCalledTimes(1); - const snapshot = await snapshotStore.read( - result.source === "hosted" ? result.metadata.url : "", - ); - expect(snapshot).toMatchObject({ - body, - savedAt: "2026-06-22T01:02:03.000Z", - metadata: { etag: '"fresh"' }, - }); - expect(snapshot?.metadata.checksum).toMatch(/^sha256:[0-9a-f]{64}$/); - }); - - it("fails explicit refreshes when required snapshot persistence fails", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 4, - entries: [ - { - name: "@openclaw/snapshot-write-fail-proof", - kind: "plugin", - openclaw: { plugin: { id: "snapshot-write-fail-proof" } }, - }, - ], - }); - const snapshotStore = { - read: vi.fn(async () => null), - write: vi.fn(async () => { - throw new Error("state database is read-only"); - }), - }; - - await expect( - loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - requireSnapshotWrite: true, - fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), - }), - ).rejects.toThrow("state database is read-only"); - - expect(snapshotStore.write).toHaveBeenCalledTimes(1); - }); - - it("reads the latest accepted snapshot in offline mode without fetching", async () => { - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(); - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 5, - entries: [ - { - name: "@openclaw/offline-snapshot-proof", - kind: "plugin", - openclaw: { plugin: { id: "offline-snapshot-proof" } }, - }, - ], - }); - const seedFetch = vi.fn( - async () => - new Response(body, { - status: 200, - headers: { etag: '"offline"' }, - }), - ); - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - fetchImpl: seedFetch, - }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - - const offlineFetch = vi.fn(async () => new Response(null, { status: 500 })); - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - fetchImpl: offlineFetch, - offline: true, - }); - - expect(offlineFetch).not.toHaveBeenCalled(); - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/offline-snapshot-proof"]); - if (result.source === "hosted-snapshot") { - expect(result.error).toBe("hosted catalog feed offline mode"); - expect(result.metadata.checksum).toBe(seeded.metadata.checksum); - } - }); - - it("re-verifies a signed snapshot before using it in offline mode", async () => { - const signed = signedHostedCatalogFeed(); - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { - body: signed.body, - metadata: { - url: "https://packages.acme.example/openclaw/feed", - status: 200, - checksum: `sha256:${crypto.createHash("sha256").update(signed.body).digest("hex")}`, - }, - savedAt: "2026-06-22T04:05:06.000Z", - trust: { - mode: "signed", - signedBy: "acme-root", - signatureCount: 1, - threshold: 1, - verifiedAt: "2026-06-22T04:05:06.000Z", - }, - }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig: { - feeds: { - acme: { - url: "https://packages.acme.example/openclaw/feed", - verification: { - mode: "signed", - keys: [{ keyId: "acme-root", publicKey: signed.publicKeyPem }], - }, - }, - }, - sources: { "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" } }, - }, - snapshotStore, - offline: true, - fetchImpl: vi.fn(), - }); - - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/signed-refresh-proof"]); - if (result.source === "hosted-snapshot") { - expect(result.trust).toMatchObject({ - mode: "signed", - signedBy: "acme-root", - signatureCount: 1, - threshold: 1, - }); - } - }); - - it("does not use an old unsigned snapshot for a signed feed profile", async () => { - const unsignedBody = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 9, - entries: [ - { - name: "@openclaw/unsigned-snapshot-proof", - kind: "plugin", - openclaw: { plugin: { id: "unsigned-snapshot-proof" } }, - }, - ], - }); - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { - body: unsignedBody, - metadata: { - url: "https://packages.acme.example/openclaw/feed", - status: 200, - checksum: `sha256:${crypto.createHash("sha256").update(unsignedBody).digest("hex")}`, - }, - savedAt: "2026-06-22T04:05:06.000Z", - }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - feedProfile: "acme", - catalogConfig: { - feeds: { - acme: { - url: "https://packages.acme.example/openclaw/feed", - verification: { - mode: "signed", - keys: [ - { - keyId: "acme-root", - publicKey: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - }, - ], - }, - }, - }, - }, - snapshotStore, - offline: true, - fetchImpl: vi.fn(), - }); - - expect(result.source).toBe("bundled-fallback"); - expect(result.entries).toEqual([]); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("snapshot fallback failed"); - expect(result.error).toContain("signed envelope is malformed"); - } - }); - - it("persists hosted feed snapshots in OpenClaw state for HTTP 304 reuse", async () => { - const stateDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-hosted-catalog-")); - try { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 7, - entries: [ - { - name: "@openclaw/sqlite-snapshot-proof", - kind: "plugin", - openclaw: { plugin: { id: "sqlite-snapshot-proof" } }, - }, - ], - }); - - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - stateDir, - now: () => new Date("2026-06-22T02:03:04.000Z"), - fetchImpl: vi.fn( - async () => - new Response(body, { - status: 200, - headers: { - etag: '"sqlite"', - "last-modified": "Mon, 22 Jun 2026 02:00:00 GMT", - }, - }), - ), - }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - closeOpenClawStateDatabaseForTest(); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - stateDir, - fetchImpl: vi.fn(async () => new Response(null, { status: 304 })), - }); - - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@openclaw/sqlite-snapshot-proof", - ]); - if (result.source === "hosted-snapshot") { - expect(result.snapshot.savedAt).toBe("2026-06-22T02:03:04.000Z"); - expect(result.metadata.checksum).toBe(seeded.metadata.checksum); - } - } finally { - closeOpenClawStateDatabaseForTest(); - rmSync(stateDir, { recursive: true, force: true }); - } - }); - it("reads and updates hosted catalog snapshots in the SQLite store", async () => { const stateDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-hosted-store-")); try { @@ -1671,426 +305,259 @@ describe("official external plugin catalog", () => { } }); - it("applies custom source-ref validation to exception snapshot fallback", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 12, - entries: [ - { - name: "@acme/snapshot-missing-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "snapshot-missing-source-ref" }, - install: { npmSpec: "@acme/snapshot-missing-source-ref" }, - }, - }, - { - name: "@acme/snapshot-known-source-ref", - kind: "plugin", - openclaw: { - plugin: { id: "snapshot-known-source-ref" }, - install: { sourceRef: "acme-npm", npmSpec: "@acme/snapshot-known-source-ref" }, - }, - }, - ], + it("verifies signed hosted feeds and rejects rollback before replacing snapshots", async () => { + const newer = signedHostedCatalogFeed({ + feed: hostedCatalogFeed({ sequence: 10, pluginName: "@openclaw/signed-v10" }), }); - const catalogConfig = { - feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } }, - sources: { "acme-npm": { type: "npm" as const } }, - }; - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ + const older = signedHostedCatalogFeed({ + feed: hostedCatalogFeed({ sequence: 9, pluginName: "@openclaw/signed-v9" }), + privateKeyPem: newer.privateKeyPem, + }); + const snapshotStore = createInMemoryHostedCatalogSnapshotStore(); + const catalogConfig = signedCatalogConfig(newer.publicKeyPem); + + const accepted = await loadHostedCatalog({ feedProfile: "acme", catalogConfig, - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), + fetchImpl: vi.fn(async () => new Response(newer.body, { status: 200 })), + now: () => new Date("2026-06-22T00:00:10.000Z"), + snapshotStore, }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" }, - ]); - const result = await loadHostedOfficialExternalPluginCatalogEntries({ + expect(accepted.source).toBe("hosted"); + expect(accepted.entries.map((entry) => entry.name)).toEqual(["@openclaw/signed-v10"]); + if (accepted.source === "hosted") { + expect(accepted.trust).toMatchObject({ + mode: "signed", + signedBy: "acme-root", + signatureCount: 1, + threshold: 1, + }); + } + + const writeSpy = vi.spyOn(snapshotStore, "write"); + const rolledBack = await loadHostedCatalog({ feedProfile: "acme", catalogConfig, + fetchImpl: vi.fn(async () => new Response(older.body, { status: 200 })), + now: () => new Date("2026-06-22T00:00:11.000Z"), snapshotStore, - fetchImpl: vi.fn(async () => new Response("{ nope", { status: 200 })), }); - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/snapshot-known-source-ref"]); + expect(rolledBack.source).toBe("hosted-snapshot"); + expect(rolledBack.entries.map((entry) => entry.name)).toEqual(["@openclaw/signed-v10"]); + if (rolledBack.source === "hosted-snapshot") { + expect(rolledBack.error).toContain("signed feed sequence is older"); + } + expect(writeSpy).not.toHaveBeenCalled(); }); - it("uses the last known good snapshot when the hosted feed returns HTTP 304", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 4, - entries: [ - { - name: "@openclaw/snapshot-proof", - kind: "plugin", - openclaw: { plugin: { id: "snapshot-proof" } }, - }, - ], + it("fails closed for unsigned signed-profile responses and re-verifies offline snapshots", async () => { + const signed = signedHostedCatalogFeed({ + feed: hostedCatalogFeed({ sequence: 8, pluginName: "@openclaw/signed-offline" }), }); - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - fetchImpl: vi.fn( - async () => - new Response(body, { - status: 200, - headers: { etag: '"snapshot-v1"' }, - }), - ), + const catalogConfig = signedCatalogConfig(signed.publicKeyPem); + const unsignedBody = JSON.stringify( + hostedCatalogFeed({ sequence: 8, pluginName: "@openclaw/unsigned" }), + ); + + const unsigned = await loadHostedCatalog({ + feedProfile: "acme", + catalogConfig, + fetchImpl: vi.fn(async () => new Response(unsignedBody, { status: 200 })), + snapshotStore: createInMemoryHostedCatalogSnapshotStore(), }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); + + expect(unsigned.source).toBe("bundled-fallback"); + expect(unsigned.entries).toEqual([]); + if (unsigned.source === "bundled-fallback") { + expect(unsigned.error).toContain("signed envelope is malformed"); } - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ + + const signedSnapshot = createInMemoryHostedCatalogSnapshotStore([ { - body, - metadata: seeded.metadata, - savedAt: "2026-06-22T01:02:03.000Z", - }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - ifNoneMatch: '"snapshot-v1"', - fetchImpl: vi.fn( - async () => - new Response(null, { - status: 304, - headers: { etag: '"snapshot-v1"' }, - }), - ), - }); - - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual(["@openclaw/snapshot-proof"]); - if (result.source === "hosted-snapshot") { - expect(result.error).toContain("HTTP 304"); - expect(result.snapshot.savedAt).toBe("2026-06-22T01:02:03.000Z"); - expect(result.metadata.checksum).toBe(seeded.metadata.checksum); - } - }); - - it("does not use a stale snapshot when HTTP 304 validators do not match", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 4, - entries: [ - { - name: "@openclaw/stale-snapshot-proof", - kind: "plugin", - openclaw: { plugin: { id: "stale-snapshot-proof" } }, - }, - ], - }); - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - fetchImpl: vi.fn( - async () => - new Response(body, { - status: 200, - headers: { etag: '"snapshot-v1"' }, - }), - ), - }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { - body, - metadata: seeded.metadata, - savedAt: "2026-06-22T01:02:03.000Z", - }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - ifNoneMatch: '"snapshot-v2"', - fetchImpl: vi.fn( - async () => - new Response(null, { - status: 304, - headers: { etag: '"snapshot-v2"' }, - }), - ), - }); - - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("snapshot fallback failed"); - expect(result.error).toContain("ETag"); - } - }); - - it("uses a valid snapshot before bundled fallback when hosted validation fails", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 5, - entries: [ - { - name: "@openclaw/snapshot-validation-proof", - kind: "plugin", - openclaw: { plugin: { id: "snapshot-validation-proof" } }, - }, - ], - }); - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), - }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - fetchImpl: vi.fn(async () => new Response("{ nope", { status: 200 })), - }); - - expect(result.source).toBe("hosted-snapshot"); - expect(result.entries.map((entry) => entry.name)).toEqual([ - "@openclaw/snapshot-validation-proof", - ]); - if (result.source === "hosted-snapshot") { - expect(result.error).toContain("JSON"); - } - }); - - it("does not use a stale snapshot when hosted validation fails with unmatched validators", async () => { - const body = JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 5, - entries: [ - { - name: "@openclaw/stale-validation-snapshot-proof", - kind: "plugin", - openclaw: { plugin: { id: "stale-validation-snapshot-proof" } }, - }, - ], - }); - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - fetchImpl: vi.fn( - async () => new Response(body, { status: 200, headers: { etag: '"snapshot-v1"' } }), - ), - }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - ifNoneMatch: '"snapshot-v2"', - fetchImpl: vi.fn(async () => new Response("{ nope", { status: 200 })), - }); - - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("snapshot fallback failed"); - expect(result.error).toContain("ETag"); - } - }); - - it("falls back to bundled entries when the snapshot is invalid", async () => { - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { - body: JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 1, - entries: [], - }), + body: signed.body, metadata: { - url: DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL, + url: "https://packages.acme.example/openclaw/feed", status: 200, - checksum: "sha256:not-current", + checksum: `sha256:${crypto.createHash("sha256").update(signed.body).digest("hex")}`, + }, + savedAt: "2026-06-22T00:00:08.000Z", + trust: { + mode: "signed", + signedBy: "acme-root", + signatureCount: 1, + threshold: 1, + verifiedAt: "2026-06-22T00:00:08.000Z", }, - savedAt: "2026-06-22T01:02:03.000Z", }, ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - fetchImpl: vi.fn(async () => new Response(null, { status: 304 })), + const offline = await loadHostedCatalog({ + feedProfile: "acme", + catalogConfig, + offline: true, + snapshotStore: signedSnapshot, }); + expect(offline.source).toBe("hosted-snapshot"); + expect(offline.entries.map((entry) => entry.name)).toEqual(["@openclaw/signed-offline"]); + }); + + it.each([ + [ + "off-allowlist hosts", + "https://packages.acme.example/openclaw/feed", + "hostname is not allowed", + ], + [ + "credential-bearing URLs", + "https://user:test-auth-token@clawhub.ai/v1/feeds/plugins", + "must not include credentials", + ], + [ + "query-bearing URLs", + "https://clawhub.ai/v1/feeds/plugins?query=test-value", + "must not include query strings or fragments", + ], + [ + "fragment-bearing URLs", + "https://clawhub.ai/v1/feeds/plugins#fragment", + "must not include query strings or fragments", + ], + ])("rejects direct hosted feed overrides with %s", async (_label, feedUrl, expectedError) => { + const fetchImpl = vi.fn(async () => new Response("{}", { status: 200 })); + const result = await loadHostedCatalog({ feedUrl, fetchImpl, snapshotStore: null }); + expect(result.source).toBe("bundled-fallback"); + expect(fetchImpl).not.toHaveBeenCalled(); if (result.source === "bundled-fallback") { - expect(result.error).toContain("snapshot fallback failed"); - expect(result.error).toContain("checksum mismatch"); + expect(result.error).toContain(expectedError); } }); - it("does not use a snapshot that violates the expected checksum", async () => { + it("filters hosted entries that reference unknown source profiles", async () => { const body = JSON.stringify({ schemaVersion: 1, id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 6, + generatedAt: "2026-06-22T00:00:10.000Z", + sequence: 10, entries: [ { - name: "@openclaw/snapshot-pin-proof", + name: "@acme/known-source", kind: "plugin", - openclaw: { plugin: { id: "snapshot-pin-proof" } }, + openclaw: { + plugin: { id: "known-source" }, + install: { sourceRef: "acme-npm", npmSpec: "@acme/known-source" }, + }, + }, + { + name: "@acme/unknown-source", + kind: "plugin", + openclaw: { + plugin: { id: "unknown-source" }, + install: { sourceRef: "attacker-npm", npmSpec: "@acme/unknown-source" }, + }, }, ], }); - const seeded = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore(), - fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), - }); - if (seeded.source !== "hosted") { - throw new Error("expected seeded hosted feed"); - } - const snapshotStore = createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore([ - { body, metadata: seeded.metadata, savedAt: "2026-06-22T01:02:03.000Z" }, - ]); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore, - expectedSha256: "sha256:not-current", + const result = await loadHostedCatalog({ + feedProfile: "acme", + catalogConfig: { + feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } }, + sources: { + "acme-npm": { type: "npm", registry: "https://packages.acme.example/npm/" }, + }, + }, fetchImpl: vi.fn(async () => new Response(body, { status: 200 })), + snapshotStore: null, }); - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("snapshot fallback failed"); - expect(result.error).toContain("expected checksum"); - } + expect(result.source).toBe("hosted"); + expect(result.entries.map((entry) => entry.name)).toEqual(["@acme/known-source"]); }); - it("falls back to the bundled catalog on checksum mismatch and oversized bodies", async () => { - const mismatch = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, - expectedSha256: "sha256:not-current", - fetchImpl: vi.fn( - async () => - new Response( - JSON.stringify({ - schemaVersion: 1, - id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", - sequence: 1, - entries: [], - }), - { status: 200 }, - ), - ), + it("enforces hosted checksum and response-size limits", async () => { + const validBody = JSON.stringify({ + schemaVersion: 1, + id: "openclaw-official-external-plugins", + generatedAt: "2026-06-22T00:00:01.000Z", + sequence: 1, + entries: [], }); + const mismatch = await loadHostedCatalog({ + expectedSha256: "sha256:not-current", + fetchImpl: vi.fn(async () => new Response(validBody, { status: 200 })), + snapshotStore: null, + }); + expect(mismatch.source).toBe("bundled-fallback"); if (mismatch.source === "bundled-fallback") { expect(mismatch.error).toContain("checksum mismatch"); - expect(mismatch.metadata?.checksum).toMatch(/^sha256:[0-9a-f]{64}$/); + expect(mismatch.metadata?.checksum).toMatch(/^sha256:[0-9a-f]{64}$/u); } - const oversized = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, + const oversized = await loadHostedCatalog({ maxBytes: 4, fetchImpl: vi.fn(async () => new Response("12345", { status: 200 })), + snapshotStore: null, }); expect(oversized.source).toBe("bundled-fallback"); if (oversized.source === "bundled-fallback") { expect(oversized.error).toContain("exceeds 4 bytes"); } - }); - it("rejects non-streaming hosted feeds before buffering underreported bodies", async () => { - const oversized = "x".repeat(8192); - const response = new Response(oversized, { + const response = new Response("x".repeat(8192), { status: 200, headers: { "content-length": "1" }, }); Object.defineProperty(response, "body", { value: null }); const arrayBuffer = vi.fn(response.arrayBuffer.bind(response)); Object.defineProperty(response, "arrayBuffer", { value: arrayBuffer }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, + const nonStreaming = await loadHostedCatalog({ maxBytes: 4096, fetchImpl: vi.fn(async () => response), + snapshotStore: null, }); - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("streaming response body unavailable"); + expect(nonStreaming.source).toBe("bundled-fallback"); + if (nonStreaming.source === "bundled-fallback") { + expect(nonStreaming.error).toContain("streaming response body unavailable"); } expect(arrayBuffer).not.toHaveBeenCalled(); }); - it("rejects non-streaming hosted feeds without content-length", async () => { + it("reuses a validated snapshot for matching HTTP 304 validators", async () => { + const snapshotStore = createInMemoryHostedCatalogSnapshotStore(); const body = JSON.stringify({ schemaVersion: 1, id: "openclaw-official-external-plugins", - generatedAt: "2026-06-22T00:00:00.000Z", + generatedAt: "2026-06-22T00:00:01.000Z", sequence: 1, entries: [], }); - const response = new Response(body, { status: 200 }); - Object.defineProperty(response, "body", { value: null }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, - fetchImpl: vi.fn(async () => response), - }); - - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("streaming response body unavailable"); - } - }); - - it("rejects oversized streaming hosted feeds incrementally", async () => { - const chunk = new Uint8Array(512 * 1024).fill("x".charCodeAt(0)); - let emitted = 0; - const body = new ReadableStream({ - pull(controller) { - if (emitted >= 3) { - controller.close(); - return; - } - emitted += 1; - controller.enqueue(chunk); - }, - }); - - const result = await loadHostedOfficialExternalPluginCatalogEntries({ - snapshotStore: null, - maxBytes: 1024 * 1024, + const seeded = await loadHostedCatalog({ fetchImpl: vi.fn( async () => new Response(body, { status: 200, - headers: { "content-type": "application/json" }, + headers: { etag: '"snapshot-v1"' }, }), ), + now: () => new Date("2026-06-22T00:00:01.000Z"), + snapshotStore, }); + expect(seeded.source).toBe("hosted"); - expect(result.source).toBe("bundled-fallback"); - if (result.source === "bundled-fallback") { - expect(result.error).toContain("exceeds 1048576 bytes"); + const reused = await loadHostedCatalog({ + ifNoneMatch: '"snapshot-v1"', + fetchImpl: vi.fn( + async () => new Response(null, { status: 304, headers: { etag: '"snapshot-v1"' } }), + ), + snapshotStore, + }); + expect(reused.source).toBe("hosted-snapshot"); + if (reused.source === "hosted-snapshot") { + expect(reused.snapshot.savedAt).toBe("2026-06-22T00:00:01.000Z"); } }); diff --git a/src/plugins/official-external-plugin-catalog.ts b/src/plugins/official-external-plugin-catalog.ts index 4eedf7cbadab..94c94eba82d6 100644 --- a/src/plugins/official-external-plugin-catalog.ts +++ b/src/plugins/official-external-plugin-catalog.ts @@ -45,7 +45,7 @@ export type OfficialExternalProviderAuthChoice = { onboardingScopes?: readonly ("text-inference" | "image-generation" | "music-generation")[]; }; -export type OfficialExternalProviderCatalogProvider = { +type OfficialExternalProviderCatalogProvider = { id?: string; aliases?: readonly string[]; name?: string; @@ -71,7 +71,7 @@ export type OfficialExternalWebSearchProvider = { }; /** Manifest-like metadata stored in official external catalog entries. */ -export type OfficialExternalPluginCatalogManifest = { +type OfficialExternalPluginCatalogManifest = { plugin?: { id?: string; label?: string; @@ -114,7 +114,7 @@ export type OfficialExternalPluginCatalogEntry = { }; } & Partial>; -export type OfficialExternalPluginCatalogInstallCandidate = { +type OfficialExternalPluginCatalogInstallCandidate = { sourceRef?: string; package?: string; version?: string; @@ -124,7 +124,7 @@ export type OfficialExternalPluginCatalogInstallCandidate = { commit?: string; }; -export type OfficialExternalPluginCatalogSourceProfile = +type OfficialExternalPluginCatalogSourceProfile = | { type: "npm"; registry?: string; @@ -138,12 +138,12 @@ export type OfficialExternalPluginCatalogSourceProfile = baseUrl?: string; }; -export type OfficialExternalPluginCatalogFeedProfile = { +type OfficialExternalPluginCatalogFeedProfile = { url: string; verification?: OfficialExternalPluginCatalogFeedVerification; }; -export type OfficialExternalPluginCatalogFeedVerification = +type OfficialExternalPluginCatalogFeedVerification = | { mode: "unsigned"; } @@ -153,12 +153,12 @@ export type OfficialExternalPluginCatalogFeedVerification = threshold?: number; }; -export type OfficialExternalPluginCatalogFeedSigningKey = { +type OfficialExternalPluginCatalogFeedSigningKey = { keyId: string; publicKey: string; }; -export type OfficialExternalPluginCatalogProfileConfig = { +type OfficialExternalPluginCatalogProfileConfig = { feeds?: Record; sources?: Record; }; @@ -244,11 +244,10 @@ type OfficialExternalProviderContract = | "webFetchProviders"; const SUPPORTED_OFFICIAL_EXTERNAL_CATALOG_FEED_SCHEMA_VERSIONS = new Set([1, 2]); -export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL = - "https://clawhub.ai/v1/feeds/plugins"; -export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE = "clawhub-public"; -export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF = "public-clawhub"; -export const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF = "public-npm"; +const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL = "https://clawhub.ai/v1/feeds/plugins"; +const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_PROFILE = "clawhub-public"; +const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_CLAWHUB_SOURCE_REF = "public-clawhub"; +const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_NPM_SOURCE_REF = "public-npm"; const DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_PROFILE_CONFIG: OfficialExternalPluginCatalogProfileConfig = { feeds: { @@ -294,7 +293,7 @@ export function isOfficialExternalPluginCatalogFeed( ); } -export function parseOfficialExternalPluginCatalogEntries( +function parseOfficialExternalPluginCatalogEntries( raw: unknown, ): OfficialExternalPluginCatalogEntry[] { if (Array.isArray(raw)) { @@ -495,7 +494,7 @@ function getManifestInstallSourceRefCandidate( }; } -export function validateOfficialExternalPluginCatalogEntrySourceRefs( +function validateOfficialExternalPluginCatalogEntrySourceRefs( entry: OfficialExternalPluginCatalogEntry, params?: { catalogConfig?: OfficialExternalPluginCatalogProfileConfig; @@ -524,7 +523,7 @@ export function validateOfficialExternalPluginCatalogEntrySourceRefs( return errors; } -export function filterOfficialExternalPluginCatalogEntriesBySourceRefs( +function filterOfficialExternalPluginCatalogEntriesBySourceRefs( entries: OfficialExternalPluginCatalogEntry[], params?: { catalogConfig?: OfficialExternalPluginCatalogProfileConfig; @@ -791,24 +790,6 @@ async function snapshotOrBundledFallbackResult(params: { } return bundledFallbackResult(params.error, params.metadata); } - -export function createInMemoryHostedOfficialExternalPluginCatalogSnapshotStore( - initialSnapshots: HostedOfficialExternalPluginCatalogSnapshot[] = [], -): HostedOfficialExternalPluginCatalogSnapshotStore { - const snapshots = new Map(); - for (const snapshot of initialSnapshots) { - snapshots.set(snapshot.metadata.url, snapshot); - } - return { - async read(url) { - return snapshots.get(url) ?? null; - }, - async write(snapshot) { - snapshots.set(snapshot.metadata.url, snapshot); - }, - }; -} - async function resolveHostedCatalogSnapshotStore(params: { snapshotStore?: HostedOfficialExternalPluginCatalogSnapshotStore | null; env?: NodeJS.ProcessEnv; @@ -827,7 +808,7 @@ async function resolveHostedCatalogSnapshotStore(params: { }); } -export async function loadHostedOfficialExternalPluginCatalogEntries(params?: { +async function loadHostedOfficialExternalPluginCatalogEntries(params?: { feedUrl?: string; feedProfile?: string; catalogConfig?: OfficialExternalPluginCatalogProfileConfig; @@ -1267,7 +1248,7 @@ export function resolveOfficialExternalPluginInstall( }; } -export function resolveOfficialExternalPluginCatalogProfileConfigFromConfig(config?: { +function resolveOfficialExternalPluginCatalogProfileConfigFromConfig(config?: { marketplaces?: OfficialExternalPluginCatalogProfileConfig; }): OfficialExternalPluginCatalogProfileConfig | undefined { return config?.marketplaces; @@ -1276,7 +1257,7 @@ export function resolveOfficialExternalPluginCatalogProfileConfigFromConfig(conf export async function loadConfiguredHostedOfficialExternalPluginCatalogEntries( config: { marketplaces?: OfficialExternalPluginCatalogProfileConfig } | undefined, params?: Omit< - Parameters[0], + NonNullable[0]>, "catalogConfig" >, ): Promise { diff --git a/src/plugins/openai-compatible-embedding-provider.test.ts b/src/plugins/openai-compatible-embedding-provider.test.ts index 23b032eef778..f6318e1b4786 100644 --- a/src/plugins/openai-compatible-embedding-provider.test.ts +++ b/src/plugins/openai-compatible-embedding-provider.test.ts @@ -5,10 +5,24 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { withEnvAsync } from "../test-utils/env.js"; import type { EmbeddingProviderCreateOptions } from "./embedding-providers.js"; import { getRegisteredEmbeddingProvider } from "./embedding-providers.js"; -import { - createOpenAICompatibleEmbeddingProvider, - openAICompatibleEmbeddingProviderAdapter, -} from "./openai-compatible-embedding-provider.js"; +import { openAICompatibleEmbeddingProviderAdapter } from "./openai-compatible-embedding-provider.js"; + +async function createOpenAICompatibleEmbeddingProvider(options: EmbeddingProviderCreateOptions) { + const result = await openAICompatibleEmbeddingProviderAdapter.create(options); + if (!result.provider) { + throw new Error("expected OpenAI-compatible embedding provider"); + } + const cacheKeyData = result.runtime?.cacheKeyData as + | { baseUrl?: string; headers?: Record } + | undefined; + return { + provider: result.provider, + client: { + baseUrl: cacheKeyData?.baseUrl, + headers: cacheKeyData?.headers ?? {}, + }, + }; +} type CapturedRequest = { method: string | undefined; @@ -453,7 +467,6 @@ describe("openai-compatible generic embedding provider", () => { expect(provider.model).toBe("text-embedding-bge-m3"); expect(provider.dimensions).toBe(1024); expect(client.baseUrl).toBe(server.baseUrl); - expect(client.headers.authorization).toBe(`Bearer ${token}`); expect(server.requests).toHaveLength(0); await expect(provider.embed("hello")).resolves.toEqual([5, 0.25, 1]); diff --git a/src/plugins/openai-compatible-embedding-provider.ts b/src/plugins/openai-compatible-embedding-provider.ts index 76d295d66632..d9b3979e9b91 100644 --- a/src/plugins/openai-compatible-embedding-provider.ts +++ b/src/plugins/openai-compatible-embedding-provider.ts @@ -21,14 +21,14 @@ import type { } from "./embedding-provider-types.js"; /** Provider id for OpenAI-compatible remote embedding servers. */ -export const OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID = "openai-compatible"; +const OPENAI_COMPATIBLE_EMBEDDING_PROVIDER_ID = "openai-compatible"; const OPENAI_COMPATIBLE_MODEL_APIS = new Set(["openai-completions", "openai-responses"]); const EMBEDDING_ERROR_BODY_MAX_BYTES = 8 * 1024; const EMBEDDING_ERROR_BODY_MAX_CHARS = 1_000; const EMBEDDING_ERROR_TRUNCATED_SUFFIX = "... [truncated]"; /** Normalized OpenAI-compatible embedding client configuration. */ -export type OpenAICompatibleEmbeddingClient = { +type OpenAICompatibleEmbeddingClient = { providerId: string; baseUrl: string; headers: Record; @@ -442,7 +442,7 @@ async function createOpenAICompatibleEmbeddingClient( } /** Creates an OpenAI-compatible embedding provider and its backing client. */ -export async function createOpenAICompatibleEmbeddingProvider( +async function createOpenAICompatibleEmbeddingProvider( options: EmbeddingProviderCreateOptions, ): Promise<{ provider: EmbeddingProvider; diff --git a/src/plugins/package-compat.ts b/src/plugins/package-compat.ts index 811eaa8e9a79..e6361079fe31 100644 --- a/src/plugins/package-compat.ts +++ b/src/plugins/package-compat.ts @@ -2,9 +2,7 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce"; /** Result of reading package.json openclaw.compat.pluginApi metadata. */ -export type PackagePluginApiRangeResult = - | { ok: true; range?: string } - | { ok: false; error: string }; +type PackagePluginApiRangeResult = { ok: true; range?: string } | { ok: false; error: string }; /** Resolves the plugin API compatibility range declared by package metadata. */ export function resolvePackagePluginApiRange( diff --git a/src/plugins/plugin-cache-primitives.ts b/src/plugins/plugin-cache-primitives.ts index 0207da499b6b..005b9112a1fe 100644 --- a/src/plugins/plugin-cache-primitives.ts +++ b/src/plugins/plugin-cache-primitives.ts @@ -76,7 +76,7 @@ export class PluginLruCache { export type ConfigScopedRuntimeCache = WeakMap>; /** Promise loader that coalesces concurrent loads per config object and for the default scope. */ -export type ConfigScopedPromiseLoader = { +type ConfigScopedPromiseLoader = { load(config?: OpenClawConfig): Promise; clear(): void; }; diff --git a/src/plugins/plugin-control-plane-context.test.ts b/src/plugins/plugin-control-plane-context.test.ts index c694e84b89fa..b1031bf7be70 100644 --- a/src/plugins/plugin-control-plane-context.test.ts +++ b/src/plugins/plugin-control-plane-context.test.ts @@ -1,14 +1,7 @@ // Verifies plugin control-plane context construction and boundaries. -import os from "node:os"; -import path from "node:path"; import { describe, expect, it } from "vitest"; import type { InstalledPluginIndex } from "./installed-plugin-index.js"; -import { - resolvePluginControlPlaneContext, - resolvePluginControlPlaneFingerprint, - resolvePluginDiscoveryContext, - resolvePluginDiscoveryFingerprint, -} from "./plugin-control-plane-context.js"; +import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js"; function createIndex(pluginId: string): InstalledPluginIndex { return { @@ -41,21 +34,6 @@ function createIndex(pluginId: string): InstalledPluginIndex { } describe("plugin control-plane context", () => { - it("resolves env-sensitive discovery roots and load paths before fingerprinting", () => { - const config = { plugins: { load: { paths: ["~/plugins", "/opt/shared"] } } }; - const envA = { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv; - const envB = { HOME: "/home/b", OPENCLAW_HOME: "/openclaw/b" } as NodeJS.ProcessEnv; - - const contextA = resolvePluginDiscoveryContext({ config, env: envA }); - const contextB = resolvePluginDiscoveryContext({ config, env: envB }); - - expect(contextA.loadPaths).toEqual(["/openclaw/a/plugins", "/opt/shared"]); - expect(contextB.loadPaths).toEqual(["/openclaw/b/plugins", "/opt/shared"]); - expect(resolvePluginDiscoveryFingerprint({ config, env: envA })).not.toBe( - resolvePluginDiscoveryFingerprint({ config, env: envB }), - ); - }); - it("includes policy, inventory, and activation in one control-plane fingerprint", () => { const config = { plugins: { allow: ["demo"] } }; const base = resolvePluginControlPlaneFingerprint({ @@ -90,30 +68,4 @@ describe("plugin control-plane context", () => { }), ).not.toBe(base); }); - - it("keeps the canonical context inspectable for cache diagnostics", () => { - const context = resolvePluginControlPlaneContext({ - config: { plugins: { load: { paths: ["/opt/plugins"] } } }, - env: { - HOME: "/home/a", - OPENCLAW_HOME: "/openclaw/a", - OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", - } as NodeJS.ProcessEnv, - inventoryFingerprint: "inventory", - policyHash: "policy", - }); - - expect(context).toStrictEqual({ - discovery: { - loadPaths: ["/opt/plugins"], - roots: { - stock: path.join(os.tmpdir(), "openclaw-empty-bundled-plugins"), - global: "/openclaw/a/.openclaw/extensions", - workspace: undefined, - }, - }, - inventoryFingerprint: "inventory", - policyFingerprint: "policy", - }); - }); }); diff --git a/src/plugins/plugin-control-plane-context.ts b/src/plugins/plugin-control-plane-context.ts index e293e807e050..85c2b8af8d06 100644 --- a/src/plugins/plugin-control-plane-context.ts +++ b/src/plugins/plugin-control-plane-context.ts @@ -7,13 +7,13 @@ import { resolveInstalledManifestRegistryIndexFingerprint } from "./manifest-reg import { resolvePluginCacheInputs, type PluginSourceRoots } from "./roots.js"; /** Discovery inputs that affect plugin source resolution. */ -export type PluginDiscoveryContext = { +type PluginDiscoveryContext = { roots: PluginSourceRoots; loadPaths: readonly string[]; }; /** Control-plane fingerprint inputs that affect installed plugin activation. */ -export type PluginControlPlaneContext = { +type PluginControlPlaneContext = { discovery: PluginDiscoveryContext; policyFingerprint: string; inventoryFingerprint?: string; @@ -21,7 +21,7 @@ export type PluginControlPlaneContext = { }; /** Parameters used to resolve plugin discovery roots and load paths. */ -export type ResolvePluginDiscoveryContextParams = { +type ResolvePluginDiscoveryContextParams = { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; workspaceDir?: string; @@ -53,21 +53,13 @@ export function resolvePluginDiscoveryContext( loadPaths: [...(params.loadPaths ?? resolveConfiguredPluginLoadPaths(params.config) ?? [])], }); } - -/** Resolves a stable fingerprint for plugin discovery inputs. */ -export function resolvePluginDiscoveryFingerprint( - params: ResolvePluginDiscoveryContextParams = {}, -): string { - return fingerprintPluginDiscoveryContext(resolvePluginDiscoveryContext(params)); -} - /** Hashes an already resolved plugin discovery context. */ export function fingerprintPluginDiscoveryContext(context: PluginDiscoveryContext): string { return hashJson(context); } /** Resolves all inputs that determine plugin control-plane activation state. */ -export function resolvePluginControlPlaneContext( +function resolvePluginControlPlaneContext( params: ResolvePluginControlPlaneContextParams = {}, ): PluginControlPlaneContext { const inventoryFingerprint = diff --git a/src/plugins/plugin-graceful-init-failure.test.ts b/src/plugins/plugin-graceful-init-failure.test.ts index 0ccaa3bd21dc..8f85289336b0 100644 --- a/src/plugins/plugin-graceful-init-failure.test.ts +++ b/src/plugins/plugin-graceful-init-failure.test.ts @@ -7,7 +7,7 @@ import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fi const fixtureTempDirs: string[] = []; const fixtureRoot = makeTrackedTempDir("openclaw-plugin-graceful", fixtureTempDirs); let tempDirIndex = 0; -const { loadOpenClawPlugins, clearPluginLoaderCache } = await import("./loader.js"); +const { loadOpenClawPlugins, clearPluginLoaderCache } = await import("./loader.test-fixtures.js"); afterAll(() => { cleanupTrackedTempDirs(fixtureTempDirs); diff --git a/src/plugins/plugin-load-profile.ts b/src/plugins/plugin-load-profile.ts index c6415ed783d7..cef7eba40095 100644 --- a/src/plugins/plugin-load-profile.ts +++ b/src/plugins/plugin-load-profile.ts @@ -31,10 +31,10 @@ export function shouldProfilePluginLoader(): boolean { * scrapers see a deterministic field order regardless of object iteration * quirks. */ -export type PluginLoadProfileExtras = ReadonlyArray; +type PluginLoadProfileExtras = ReadonlyArray; /** Per-call scope: which plugin and which source path the probe is for. */ -export type PluginLoadProfileScope = { +type PluginLoadProfileScope = { pluginId?: string; source: string; }; @@ -44,11 +44,7 @@ export type PluginLoadProfileScope = { * emit a `[plugin-load-profile]` line that already includes the bound * `pluginId` and `source`. Build one with `createProfiler(scope)`. */ -export type PluginLoadProfiler = ( - phase: string, - run: () => T, - extras?: PluginLoadProfileExtras, -) => T; +type PluginLoadProfiler = (phase: string, run: () => T, extras?: PluginLoadProfileExtras) => T; /** * Render a `[plugin-load-profile]` line. Exported so that callers needing diff --git a/src/plugins/plugin-lookup-table.test.ts b/src/plugins/plugin-lookup-table.test.ts index d1d411c1bb4c..494499dcabab 100644 --- a/src/plugins/plugin-lookup-table.test.ts +++ b/src/plugins/plugin-lookup-table.test.ts @@ -649,149 +649,6 @@ describe("loadPluginLookUpTable", () => { ); }); - it("reuses lookup tables prepared from the same metadata snapshot and activation config", async () => { - const plugins = [ - createManifestRecord({ - id: "telegram", - origin: "bundled", - channels: ["telegram"], - }), - ]; - const config = { - channels: { - telegram: { token: "configured" }, - }, - } as OpenClawConfig; - const index = { - ...createIndex(plugins), - policyHash: resolveInstalledPluginIndexPolicyHash(config), - }; - const manifestRegistry: PluginManifestRegistry = { - plugins, - diagnostics: [], - }; - loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); - const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); - const { clearPluginLookUpTableMemoForTest, loadPluginLookUpTable } = - await import("./plugin-lookup-table.js"); - clearPluginLookUpTableMemoForTest(); - - const metadataSnapshot = loadPluginMetadataSnapshot({ - config, - env: {}, - index, - }); - const manifestRegistryLoadCount = loadPluginManifestRegistryForInstalledIndex.mock.calls.length; - listPotentialConfiguredChannelIds.mockClear(); - - const first = loadPluginLookUpTable({ - config, - env: {}, - metadataSnapshot, - }); - const second = loadPluginLookUpTable({ - config, - env: {}, - metadataSnapshot, - }); - - expect(second).toBe(first); - expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledTimes( - manifestRegistryLoadCount, - ); - }); - - it("reuses lookup tables when unrelated env values change", async () => { - const plugins = [ - createManifestRecord({ - id: "telegram", - origin: "bundled", - channels: ["telegram"], - }), - ]; - const config = {} as OpenClawConfig; - const index = { - ...createIndex(plugins), - policyHash: resolveInstalledPluginIndexPolicyHash(config), - }; - const manifestRegistry: PluginManifestRegistry = { - plugins, - diagnostics: [], - }; - loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); - const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); - const { clearPluginLookUpTableMemoForTest, loadPluginLookUpTable } = - await import("./plugin-lookup-table.js"); - clearPluginLookUpTableMemoForTest(); - - const metadataSnapshot = loadPluginMetadataSnapshot({ - config, - env: { UNRELATED_ONE: "1" }, - index, - }); - - const first = loadPluginLookUpTable({ - config, - env: { UNRELATED_ONE: "1" }, - metadataSnapshot, - }); - const second = loadPluginLookUpTable({ - config, - env: { UNRELATED_TWO: "2" }, - metadataSnapshot, - }); - - expect(second).toBe(first); - }); - - it("does not reuse lookup tables when channel env presence changes startup scope", async () => { - listPotentialConfiguredChannelIds.mockImplementation( - (_config: OpenClawConfig, env: NodeJS.ProcessEnv) => (env.TELEGRAM_TOKEN ? ["telegram"] : []), - ); - const plugins = [ - createManifestRecord({ - id: "telegram", - origin: "bundled", - channels: ["telegram"], - }), - ]; - const config = {} as OpenClawConfig; - const index = { - ...createIndex(plugins), - policyHash: resolveInstalledPluginIndexPolicyHash(config), - }; - const manifestRegistry: PluginManifestRegistry = { - plugins, - diagnostics: [], - }; - loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); - const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); - const { clearPluginLookUpTableMemoForTest, loadPluginLookUpTable } = - await import("./plugin-lookup-table.js"); - clearPluginLookUpTableMemoForTest(); - - const metadataSnapshot = loadPluginMetadataSnapshot({ - config, - env: {}, - index, - }); - - const first = loadPluginLookUpTable({ - config, - env: {}, - metadataSnapshot, - }); - const second = loadPluginLookUpTable({ - config, - env: { TELEGRAM_TOKEN: "configured" }, - metadataSnapshot, - }); - - expect(second).not.toBe(first); - expect(first.startup.pluginIds).toStrictEqual([]); - expect(second.startup.pluginIds).toStrictEqual(["telegram"]); - }); - it("rebuilds when a provided metadata snapshot has a stale plugin policy", async () => { const plugins = [ createManifestRecord({ diff --git a/src/plugins/plugin-lookup-table.ts b/src/plugins/plugin-lookup-table.ts index 881c04166a6c..16ddf6be1fdf 100644 --- a/src/plugins/plugin-lookup-table.ts +++ b/src/plugins/plugin-lookup-table.ts @@ -14,7 +14,7 @@ import { import type { PluginRegistrySnapshot } from "./plugin-registry-snapshot.js"; import { normalizeWorkerProviderIds } from "./worker-provider-registry.js"; -export type PluginLookUpTableMetrics = PluginMetadataSnapshot["metrics"] & { +type PluginLookUpTableMetrics = PluginMetadataSnapshot["metrics"] & { startupPlanMs: number; startupPluginCount: number; deferredChannelPluginCount: number; @@ -26,7 +26,7 @@ export type PluginLookUpTable = PluginMetadataSnapshot & { metrics: PluginLookUpTableMetrics; }; -export type LoadPluginLookUpTableParams = { +type LoadPluginLookUpTableParams = { config: OpenClawConfig; activationSourceConfig?: OpenClawConfig; workspaceDir?: string; @@ -36,15 +36,10 @@ export type LoadPluginLookUpTableParams = { workerProviderIds?: readonly string[]; }; -let lookupTableMemoBySnapshot = new WeakMap< +const lookupTableMemoBySnapshot = new WeakMap< PluginMetadataSnapshot, Map >(); - -export function clearPluginLookUpTableMemoForTest(): void { - lookupTableMemoBySnapshot = new WeakMap>(); -} - export function loadPluginLookUpTable(params: LoadPluginLookUpTableParams): PluginLookUpTable { const requestedSnapshotConfig = params.activationSourceConfig ?? params.config; const workerProviderIds = normalizeWorkerProviderIds(params.workerProviderIds ?? []); diff --git a/src/plugins/plugin-module-loader-cache.test.ts b/src/plugins/plugin-module-loader-cache.test.ts index 278ddcb751a0..c9de1bfdfba7 100644 --- a/src/plugins/plugin-module-loader-cache.test.ts +++ b/src/plugins/plugin-module-loader-cache.test.ts @@ -134,63 +134,6 @@ describe("getCachedPluginModuleLoader", () => { }; }); - it("resolves deterministic cache entries for equivalent alias maps", async () => { - const { resolvePluginModuleLoaderCacheEntry } = await importFreshModule< - typeof import("./plugin-module-loader-cache.js") - >(import.meta.url, "./plugin-module-loader-cache.js?scope=cache-entry-alias-order"); - - const first = resolvePluginModuleLoaderCacheEntry({ - modulePath: "/repo/extensions/demo/index.ts", - importerUrl: "file:///repo/src/plugins/loader.ts", - loaderFilename: "/repo/src/plugins/loader.ts", - aliasMap: { - alpha: "/repo/alpha.js", - zeta: "/repo/zeta.js", - }, - tryNative: false, - }); - const second = resolvePluginModuleLoaderCacheEntry({ - modulePath: "/repo/extensions/demo/index.ts", - importerUrl: "file:///repo/src/plugins/loader.ts", - loaderFilename: "/repo/src/plugins/loader.ts", - aliasMap: { - zeta: "/repo/zeta.js", - alpha: "/repo/alpha.js", - }, - tryNative: false, - }); - - expect(second.cacheKey).toBe(first.cacheKey); - expect(second.scopedCacheKey).toBe(first.scopedCacheKey); - expect(first.loaderFilename).toBe("/repo/src/plugins/loader.ts"); - }); - - it("keeps explicit shared cache scope keys independent of loader options", async () => { - const { resolvePluginModuleLoaderCacheEntry } = await importFreshModule< - typeof import("./plugin-module-loader-cache.js") - >(import.meta.url, "./plugin-module-loader-cache.js?scope=cache-entry-shared-scope"); - - const first = resolvePluginModuleLoaderCacheEntry({ - modulePath: "/repo/dist/extensions/demo-a/api.js", - importerUrl: "file:///repo/src/plugins/public-surface-loader.ts", - loaderFilename: "/repo/src/plugins/public-surface-loader.ts", - aliasMap: { demo: "/repo/demo-a.js" }, - tryNative: true, - sharedCacheScopeKey: "bundled:native", - }); - const second = resolvePluginModuleLoaderCacheEntry({ - modulePath: "/repo/dist/extensions/demo-b/api.js", - importerUrl: "file:///repo/src/plugins/public-surface-loader.ts", - loaderFilename: "/repo/src/plugins/public-surface-loader.ts", - aliasMap: { demo: "/repo/demo-b.js" }, - tryNative: false, - sharedCacheScopeKey: "bundled:native", - }); - - expect(first.cacheKey).not.toBe(second.cacheKey); - expect(first.scopedCacheKey).toBe(second.scopedCacheKey); - }); - it("reuses cached loaders for the same module config and filename", async () => { const { createJiti, getCachedPluginModuleLoader } = await loadCachedPluginModuleLoader("cached-loader"); diff --git a/src/plugins/plugin-module-loader-cache.ts b/src/plugins/plugin-module-loader-cache.ts index d9cbddf9c9be..5a983a369920 100644 --- a/src/plugins/plugin-module-loader-cache.ts +++ b/src/plugins/plugin-module-loader-cache.ts @@ -15,13 +15,13 @@ import { } from "./sdk-alias.js"; /** Jiti-based module loader used for plugin source/runtime imports. */ -export type PluginModuleLoader = ReturnType; +type PluginModuleLoader = ReturnType; export type PluginModuleLoaderFactory = typeof createJiti; export type PluginModuleLoaderCache = Pick< PluginLruCache, "clear" | "get" | "set" | "size" >; -export type ResolvePluginModuleLoaderCacheEntryParams = { +type ResolvePluginModuleLoaderCacheEntryParams = { modulePath: string; importerUrl: string; argvEntry?: string; @@ -34,14 +34,14 @@ export type ResolvePluginModuleLoaderCacheEntryParams = { cacheScopeKey?: string; sharedCacheScopeKey?: string; }; -export type PluginModuleLoaderCacheEntry = { +type PluginModuleLoaderCacheEntry = { loaderFilename: string; aliasMap: Record; tryNative: boolean; cacheKey: string; scopedCacheKey: string; }; -export type PluginModuleLoaderStatsSnapshot = { +type PluginModuleLoaderStatsSnapshot = { calls: number; nativeHits: number; nativeMisses: number; @@ -135,7 +135,7 @@ function resolveDefaultPluginModuleLoaderConfig( }); } -export function resolvePluginModuleLoaderCacheEntry( +function resolvePluginModuleLoaderCacheEntry( params: ResolvePluginModuleLoaderCacheEntryParams, ): PluginModuleLoaderCacheEntry { const loaderFilename = toSafeImportPath(params.loaderFilename ?? params.modulePath); diff --git a/src/plugins/plugin-registration-transaction.test.ts b/src/plugins/plugin-registration-transaction.test.ts index fdbc41bc7022..3c6a6d1a1cad 100644 --- a/src/plugins/plugin-registration-transaction.test.ts +++ b/src/plugins/plugin-registration-transaction.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getMemoryCapabilityRegistration, registerMemoryCapability } from "./memory-state.js"; +import { + getMemoryCapabilityRegistration, + registerMemoryCapability, +} from "./memory-state.test-fixtures.js"; import { createPluginRegistrationTransaction, type PluginProcessGlobalState, diff --git a/src/plugins/plugin-registration-transaction.ts b/src/plugins/plugin-registration-transaction.ts index 19ad3618d9a1..610a19a4ff27 100644 --- a/src/plugins/plugin-registration-transaction.ts +++ b/src/plugins/plugin-registration-transaction.ts @@ -96,7 +96,7 @@ function restorePluginRegistry(registry: PluginRegistry, snapshot: PluginRegistr Object.assign(registry, snapshot); } -export type PluginRegistrationTransaction = { +type PluginRegistrationTransaction = { commit: (params: { activate: boolean }) => void; rollback: () => void; }; diff --git a/src/plugins/plugin-registry-contributions.ts b/src/plugins/plugin-registry-contributions.ts index e5a28bb89132..a80918e61212 100644 --- a/src/plugins/plugin-registry-contributions.ts +++ b/src/plugins/plugin-registry-contributions.ts @@ -26,22 +26,19 @@ import { type LoadPluginRegistryParams, type PluginRegistrySnapshot, } from "./plugin-registry-snapshot.js"; -export { - createPluginRegistryIdNormalizer, - type PluginRegistryIdNormalizerOptions, -} from "./plugin-registry-id-normalizer.js"; +export { createPluginRegistryIdNormalizer } from "./plugin-registry-id-normalizer.js"; -export type PluginLookUpTable = Pick< +type PluginLookUpTable = Pick< PluginMetadataSnapshot, "index" | "manifestRegistry" | "plugins" | "normalizePluginId" | "owners" >; -export type PluginRegistryContributionOptions = LoadPluginRegistryParams & { +type PluginRegistryContributionOptions = LoadPluginRegistryParams & { includeDisabled?: boolean; lookUpTable?: PluginLookUpTable; }; -export type LoadPluginRegistryManifestParams = LoadPluginRegistryParams & { +type LoadPluginRegistryManifestParams = LoadPluginRegistryParams & { includeDisabled?: boolean; pluginIds?: readonly string[]; bundledChannelConfigCollector?: BundledChannelConfigCollector; @@ -57,37 +54,36 @@ export type PluginRegistryContributionKey = | "commandAliases" | "contracts"; -export type ResolvePluginContributionOwnersParams = PluginRegistryContributionOptions & { +type ResolvePluginContributionOwnersParams = PluginRegistryContributionOptions & { contribution: PluginRegistryContributionKey; matches: string | ((contributionId: string) => boolean); }; -export type ListPluginContributionIdsParams = PluginRegistryContributionOptions & { +type ListPluginContributionIdsParams = PluginRegistryContributionOptions & { contribution: PluginRegistryContributionKey; }; -export type ResolveProviderOwnersParams = PluginRegistryContributionOptions & { +type ResolveProviderOwnersParams = PluginRegistryContributionOptions & { providerId: string; }; -export type ResolveManifestContractPluginIdsParams = LoadPluginRegistryParams & { +type ResolveManifestContractPluginIdsParams = LoadPluginRegistryParams & { contract: PluginManifestContractListKey; origin?: PluginOrigin; onlyPluginIds?: readonly string[]; }; -export type ResolveManifestContractOwnerPluginIdParams = LoadPluginRegistryParams & { +type ResolveManifestContractOwnerPluginIdParams = LoadPluginRegistryParams & { contract: PluginManifestContractListKey; value: string | undefined; origin?: PluginOrigin; }; -export type ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams = - LoadPluginRegistryParams & { - contract: PluginManifestContractListKey; - path: string | undefined; - origin?: PluginOrigin; - }; +type ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams = LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + path: string | undefined; + origin?: PluginOrigin; +}; function normalizeContributionId(value: string): string { return value.trim(); diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts index ed40a0d34a76..8bc92f2e80d7 100644 --- a/src/plugins/plugin-registry-snapshot.ts +++ b/src/plugins/plugin-registry-snapshot.ts @@ -27,7 +27,6 @@ import { extractPluginInstallRecordsFromInstalledPluginIndex, hasMissingConfigPathActivationMetadata, isInstalledPluginEnabled, - listInstalledPluginRecords, loadInstalledPluginIndexWithDiscovery, resolveInstalledPluginIndexPolicyHash, type InstalledPluginIndex, @@ -44,9 +43,9 @@ import { resolvePluginCacheInputs } from "./roots.js"; export type PluginRegistrySnapshot = InstalledPluginIndex; export type PluginRegistryRecord = InstalledPluginIndexRecord; -export type PluginRegistryInspection = InstalledPluginIndexStoreInspection; +type PluginRegistryInspection = InstalledPluginIndexStoreInspection; export type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js"; -export type PluginRegistrySnapshotDiagnosticCode = +type PluginRegistrySnapshotDiagnosticCode = | "persisted-registry-disabled" | "persisted-registry-missing" | "persisted-registry-stale-policy" @@ -58,14 +57,14 @@ export type PluginRegistrySnapshotDiagnostic = { message: string; }; -export type PluginRegistrySnapshotResult = { +type PluginRegistrySnapshotResult = { snapshot: PluginRegistrySnapshot; source: PluginRegistrySnapshotSource; diagnostics: readonly PluginRegistrySnapshotDiagnostic[]; discovery?: PluginDiscoveryResult; }; -export const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY"; +const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY"; const MAX_PLUGIN_REGISTRY_SNAPSHOT_MEMOS = 8; const REGISTRY_SNAPSHOT_MEMO_ENV_KEYS = [ "APPDATA", @@ -106,7 +105,7 @@ export type LoadPluginRegistryParams = LoadInstalledPluginIndexParams & preferPersisted?: boolean; }; -export type GetPluginRecordParams = LoadPluginRegistryParams & { +type GetPluginRecordParams = LoadPluginRegistryParams & { pluginId: string; }; @@ -682,13 +681,6 @@ export function loadPluginRegistrySnapshot( ): PluginRegistrySnapshot { return resolveSnapshot(params); } - -export function listPluginRecords( - params: LoadPluginRegistryParams = {}, -): readonly PluginRegistryRecord[] { - return listInstalledPluginRecords(resolveSnapshot(params)); -} - export function getPluginRecord(params: GetPluginRecordParams): PluginRegistryRecord | undefined { return getInstalledPluginRecord(resolveSnapshot(params), params.pluginId); } diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index c0ef2c607b70..9281d8048cf7 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -20,13 +20,11 @@ import { import { loadPluginLookUpTable } from "./plugin-lookup-table.js"; import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js"; import { - DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV, createPluginRegistryIdNormalizer, getPluginRecord, inspectPluginRegistry, isPluginEnabled, listPluginContributionIds, - listPluginRecords, loadPluginRegistrySnapshot, loadPluginRegistrySnapshotWithMetadata, normalizePluginsConfigWithRegistry, @@ -40,6 +38,11 @@ import { import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; const tempDirs: string[] = []; +const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY"; + +function listPluginRecords(params: { index: InstalledPluginIndex }) { + return params.index.plugins; +} afterEach(() => { closeOpenClawStateDatabaseForTest(); diff --git a/src/plugins/plugin-sdk-dist-alias.ts b/src/plugins/plugin-sdk-dist-alias.ts deleted file mode 100644 index 0e8049a7175a..000000000000 --- a/src/plugins/plugin-sdk-dist-alias.ts +++ /dev/null @@ -1,189 +0,0 @@ -// Resolves plugin SDK distribution aliases for bundled runtime imports. -import fs from "node:fs"; -import path from "node:path"; -import { tryReadJsonSync, writeJsonSync } from "../infra/json-files.js"; - -type OpenClawPackageJson = { - exports?: Record; -}; - -const PRIVATE_LOCAL_ONLY_PLUGIN_SDK_DIST_FILE_NAME_FALLBACK = [ - "codex-mcp-projection.js", - "codex-native-task-runtime.js", - `${["qa", "channel"].join("-")}.js`, - `${["qa", "channel", "protocol"].join("-")}.js`, - `${["qa", "lab"].join("-")}.js`, - `${["qa", "runtime"].join("-")}.js`, - "ssrf-runtime-internal.js", - "test-utils.js", -] as const; - -function isSafePluginSdkSubpathSegment(subpath: string): boolean { - return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(subpath); -} - -function collectLegacyPublicPluginSdkDistFileNames(distRoot: string): Set | undefined { - const pluginSdkDir = path.join(distRoot, "plugin-sdk"); - if (!fs.existsSync(pluginSdkDir)) { - return undefined; - } - const privateFileNames = readPrivateLocalOnlyPluginSdkDistFileNames(distRoot); - const fileNames = new Set(); - for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) { - if (!entry.isFile() || path.extname(entry.name) !== ".js") { - continue; - } - if (privateFileNames.has(entry.name)) { - continue; - } - fileNames.add(entry.name); - } - return fileNames.size > 0 ? fileNames : undefined; -} - -function readPrivateLocalOnlyPluginSdkDistFileNames(distRoot: string): Set { - const packageRoot = path.dirname(path.resolve(distRoot)); - const privateFileNames = new Set(PRIVATE_LOCAL_ONLY_PLUGIN_SDK_DIST_FILE_NAME_FALLBACK); - const subpaths = tryReadJsonSync( - path.join(packageRoot, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"), - ); - if (!Array.isArray(subpaths)) { - return privateFileNames; - } - for (const subpath of subpaths) { - if (typeof subpath === "string" && isSafePluginSdkSubpathSegment(subpath)) { - privateFileNames.add(`${subpath}.js`); - } - } - return privateFileNames; -} - -function readPublicPluginSdkDistFileNames(distRoot: string): Set | undefined { - const packageRoot = path.dirname(path.resolve(distRoot)); - const packageJson = tryReadJsonSync(path.join(packageRoot, "package.json")); - if (!packageJson || typeof packageJson !== "object" || Array.isArray(packageJson)) { - return collectLegacyPublicPluginSdkDistFileNames(distRoot); - } - const packageExports = packageJson.exports; - if (!packageExports || typeof packageExports !== "object" || Array.isArray(packageExports)) { - return collectLegacyPublicPluginSdkDistFileNames(distRoot); - } - - const fileNames = new Set(); - for (const exportKey of Object.keys(packageExports)) { - if (exportKey === "./plugin-sdk") { - fileNames.add("index.js"); - continue; - } - if (!exportKey.startsWith("./plugin-sdk/")) { - continue; - } - const subpath = exportKey.slice("./plugin-sdk/".length); - if (isSafePluginSdkSubpathSegment(subpath)) { - fileNames.add(`${subpath}.js`); - } - } - - return fileNames.size > 0 ? fileNames : collectLegacyPublicPluginSdkDistFileNames(distRoot); -} - -function buildRuntimePluginSdkPackageExports( - publicDistFileNames: ReadonlySet | undefined, -): Record { - if (!publicDistFileNames) { - return { - "./plugin-sdk": "./plugin-sdk/index.js", - }; - } - - const sortedFileNames = [...publicDistFileNames].toSorted((left, right) => { - if (left === "index.js") { - return -1; - } - if (right === "index.js") { - return 1; - } - return left.localeCompare(right); - }); - return Object.fromEntries( - sortedFileNames.map((fileName) => { - const subpath = fileName.slice(0, -".js".length); - return [ - subpath === "index" ? "./plugin-sdk" : `./plugin-sdk/${subpath}`, - `./plugin-sdk/${fileName}`, - ]; - }), - ); -} - -function removeStalePrivatePluginSdkAliasFiles( - pluginSdkAliasDir: string, - publicDistFileNames: ReadonlySet | undefined, -): void { - if (!publicDistFileNames || !fs.existsSync(pluginSdkAliasDir)) { - return; - } - for (const entry of fs.readdirSync(pluginSdkAliasDir, { withFileTypes: true })) { - if (!entry.isFile() || path.extname(entry.name) !== ".js") { - continue; - } - if (!publicDistFileNames.has(entry.name)) { - fs.rmSync(path.join(pluginSdkAliasDir, entry.name), { force: true }); - } - } -} - -function writeRuntimeJsonFile(targetPath: string, value: unknown): void { - writeJsonSync(targetPath, value); -} - -function writeRuntimeModuleWrapper(sourcePath: string, targetPath: string): void { - const relative = `./${path.relative(path.dirname(targetPath), sourcePath).split(path.sep).join("/")}`; - const content = [`export * from ${JSON.stringify(relative)};`, ""].join("\n"); - try { - if (fs.readFileSync(targetPath, "utf8") === content) { - return; - } - } catch { - // Missing or unreadable wrapper; rewrite below. - } - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, content, "utf8"); -} - -export function ensureOpenClawPluginSdkAlias(distRoot: string): void { - const pluginSdkDir = path.join(distRoot, "plugin-sdk"); - if (!fs.existsSync(pluginSdkDir)) { - return; - } - - const publicDistFileNames = readPublicPluginSdkDistFileNames(distRoot); - const aliasDir = path.join(distRoot, "extensions", "node_modules", "openclaw"); - const pluginSdkAliasDir = path.join(aliasDir, "plugin-sdk"); - writeRuntimeJsonFile(path.join(aliasDir, "package.json"), { - name: "openclaw", - type: "module", - exports: buildRuntimePluginSdkPackageExports(publicDistFileNames), - }); - try { - if (fs.existsSync(pluginSdkAliasDir) && !fs.lstatSync(pluginSdkAliasDir).isDirectory()) { - fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true }); - } - } catch { - // Another process may be creating the alias at the same time. - } - fs.mkdirSync(pluginSdkAliasDir, { recursive: true }); - removeStalePrivatePluginSdkAliasFiles(pluginSdkAliasDir, publicDistFileNames); - for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) { - if (!entry.isFile() || path.extname(entry.name) !== ".js") { - continue; - } - if (publicDistFileNames && !publicDistFileNames.has(entry.name)) { - continue; - } - writeRuntimeModuleWrapper( - path.join(pluginSdkDir, entry.name), - path.join(pluginSdkAliasDir, entry.name), - ); - } -} diff --git a/src/plugins/plugin-sdk-native-resolver.test.ts b/src/plugins/plugin-sdk-native-resolver.test.ts index a6c0c62d0e55..77cca01a4eb0 100644 --- a/src/plugins/plugin-sdk-native-resolver.test.ts +++ b/src/plugins/plugin-sdk-native-resolver.test.ts @@ -5,15 +5,8 @@ import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { afterEach, beforeAll, describe, expect, it } from "vitest"; -import { - installOpenClawPluginSdkNativeResolver, - resetOpenClawPluginSdkNativeResolverForTest, -} from "./plugin-sdk-native-resolver.js"; - -afterEach(() => { - resetOpenClawPluginSdkNativeResolverForTest(); -}); +import { beforeAll, describe, expect, it } from "vitest"; +import { installOpenClawPluginSdkNativeResolver } from "./plugin-sdk-native-resolver.js"; type NativeEsmLazyImportProbe = { status: number | null; @@ -290,7 +283,7 @@ describe("installOpenClawPluginSdkNativeResolver", () => { 'import fs from "node:fs";', 'import path from "node:path";', 'import { pathToFileURL } from "node:url";', - `import { installOpenClawPluginSdkNativeResolver, resetOpenClawPluginSdkNativeResolverForTest } from ${JSON.stringify(resolverModuleUrl)};`, + `import { installOpenClawPluginSdkNativeResolver } from ${JSON.stringify(resolverModuleUrl)};`, `const root = ${JSON.stringify(root)};`, "const writeJson = (targetPath, value) => {", " fs.mkdirSync(path.dirname(targetPath), { recursive: true });", @@ -334,7 +327,6 @@ describe("installOpenClawPluginSdkNativeResolver", () => { "});", "const module = await import(pathToFileURL(entryPath).href);", "const lazy = await module.loadLazy();", - "resetOpenClawPluginSdkNativeResolverForTest();", "console.log(`${module.eager}:${lazy.lazy}`);", "", ].join("\n"), diff --git a/src/plugins/plugin-sdk-native-resolver.ts b/src/plugins/plugin-sdk-native-resolver.ts index 0954d5886719..c2a2bfda65bf 100644 --- a/src/plugins/plugin-sdk-native-resolver.ts +++ b/src/plugins/plugin-sdk-native-resolver.ts @@ -38,7 +38,7 @@ type NativeAliasEntry = { }; /** Resolver install options for CJS `_resolveFilename` and modern ESM loader hooks. */ -export type InstallOpenClawPluginSdkNativeResolverOptions = { +type InstallOpenClawPluginSdkNativeResolverOptions = { modulePath?: string; pluginModulePath?: string; allowedParentRoots?: readonly string[]; @@ -133,7 +133,6 @@ const INTERNAL_CORE_PACKAGE_ALIASES = [ const pluginSdkNativeAliases = new Map(); let installed = false; let previousResolveFilename: ResolveFilename | undefined; -let esmHooks: { deregister: () => void } | undefined; function resolveLoaderModulePath(options: InstallOpenClawPluginSdkNativeResolverOptions): string { return options.modulePath ?? fileURLToPath(options.moduleUrl ?? import.meta.url); @@ -366,7 +365,7 @@ function installResolver(): void { } return previousResolveFilename?.(request, parent, isMain, options) ?? request; }) satisfies ResolveFilename; - esmHooks = moduleWithResolver.registerHooks?.({ + moduleWithResolver.registerHooks?.({ resolve(specifier, context, nextResolve) { const aliasTarget = resolveAliasTargetForParentUrl(specifier, context.parentURL); if (aliasTarget) { @@ -439,14 +438,3 @@ export function installOpenClawInternalCorePackageNativeResolver( installResolver(); return [...pluginSdkNativeAliases.keys()].toSorted(); } - -export function resetOpenClawPluginSdkNativeResolverForTest(): void { - pluginSdkNativeAliases.clear(); - esmHooks?.deregister(); - esmHooks = undefined; - if (installed && previousResolveFilename) { - moduleWithResolver[nodeResolveFilenameProperty] = previousResolveFilename; - } - previousResolveFilename = undefined; - installed = false; -} diff --git a/src/plugins/plugin-version-drift.ts b/src/plugins/plugin-version-drift.ts index 747c96fa0dd2..30d05baea478 100644 --- a/src/plugins/plugin-version-drift.ts +++ b/src/plugins/plugin-version-drift.ts @@ -9,7 +9,7 @@ import { resolveTrustedSourceLinkedOfficialNpmSpec, } from "./official-external-install-records.js"; -export type PluginVersionDriftEntry = { +type PluginVersionDriftEntry = { pluginId: string; installedVersion: string; gatewayVersion: string; diff --git a/src/plugins/provider-auth-choice.test.ts b/src/plugins/provider-auth-choice.test.ts index 981885fabc25..3eb815fb5f4b 100644 --- a/src/plugins/provider-auth-choice.test.ts +++ b/src/plugins/provider-auth-choice.test.ts @@ -1,7 +1,6 @@ // Covers provider auth choice selection for plugin-owned providers. -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js"; -import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createNonExitingRuntime } from "../runtime.js"; import type { ProviderPlugin } from "./types.js"; @@ -21,135 +20,7 @@ vi.mock("../wizard/setup.post-install-migration.js", () => ({ offerPostInstallMigrations, })); -const { testing, applyAuthChoicePluginProvider, runProviderPluginAuthMethodUnpersisted } = - await import("./provider-auth-choice.js"); - -function buildProvider(): ProviderPlugin { - return { - id: "openai", - label: "OpenAI", - auth: [ - { - id: "api-key", - label: "API key", - kind: "api_key", - run: vi.fn(async () => ({ - profiles: [], - notes: [], - defaultModel: "gpt-5.5", - })), - }, - ], - }; -} - -describe("applyAuthChoicePluginProvider", () => { - beforeEach(() => { - testing.resetDepsForTest(); - ensureCodexRuntimePluginForModelSelection.mockReset(); - ensureCopilotRuntimePluginForModelSelection.mockReset(); - ensureCopilotRuntimePluginForModelSelection.mockImplementation( - async ({ cfg }: { cfg: OpenClawConfig }) => ({ - cfg, - required: false, - installed: false, - }), - ); - offerPostInstallMigrations.mockReset(); - }); - - it("returns post-install Codex migration config when setting an OpenAI default model", async () => { - const provider = buildProvider(); - const runProviderModelSelectedHook = vi.fn(async () => undefined); - testing.setDepsForTest({ - loadPluginProviderRuntime: async () => - ({ - resolvePluginProviders: () => [provider], - runProviderModelSelectedHook, - }) as never, - }); - ensureCodexRuntimePluginForModelSelection.mockImplementation( - async ({ cfg }: { cfg: OpenClawConfig }) => ({ - installed: true, - cfg: { - ...cfg, - plugins: { - ...cfg.plugins, - entries: { - ...cfg.plugins?.entries, - codex: { enabled: true }, - }, - }, - }, - }), - ); - offerPostInstallMigrations.mockImplementation( - async ({ config }: { config: OpenClawConfig }) => ({ - config: { - ...config, - plugins: { - ...config.plugins, - entries: { - ...config.plugins?.entries, - codex: { - enabled: true, - config: { - codexPlugins: { - enabled: true, - allow_destructive_actions: true, - plugins: { - gmail: { - enabled: true, - marketplaceName: "openai-curated", - pluginName: "gmail", - }, - }, - }, - }, - }, - }, - }, - }, - }), - ); - - const result = await applyAuthChoicePluginProvider( - { - authChoice: "openai-api-key", - config: {}, - runtime: createNonExitingRuntime(), - prompter: createWizardPrompter(), - setDefaultModel: true, - }, - { - authChoice: "openai-api-key", - pluginId: "openai", - providerId: "openai", - methodId: "api-key", - label: "OpenAI", - }, - ); - - expect(runProviderModelSelectedHook).toHaveBeenCalledOnce(); - expect(offerPostInstallMigrations).toHaveBeenCalledWith( - expect.objectContaining({ - installedPluginIds: ["codex"], - }), - ); - const resultConfig = result?.config; - expect(resultConfig?.agents?.defaults?.model).toEqual({ primary: "gpt-5.5" }); - const codexConfig = resultConfig?.plugins?.entries?.codex?.config as - | { codexPlugins?: { plugins?: unknown } } - | undefined; - expect(codexConfig?.codexPlugins?.plugins).toEqual({ - gmail: { - enabled: true, - marketplaceName: "openai-curated", - pluginName: "gmail", - }, - }); - }); -}); +const { runProviderPluginAuthMethodUnpersisted } = await import("./provider-auth-choice.js"); describe("runProviderPluginAuthMethodUnpersisted", () => { it("delegates remote browser destinations to structured wizard clients", async () => { diff --git a/src/plugins/provider-auth-choice.ts b/src/plugins/provider-auth-choice.ts index 4e431f1fa5b1..4e97f5199c0d 100644 --- a/src/plugins/provider-auth-choice.ts +++ b/src/plugins/provider-auth-choice.ts @@ -16,12 +16,7 @@ import type { RuntimeEnv } from "../runtime.js"; import { t } from "../wizard/i18n/index.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { enablePluginInConfig } from "./enable.js"; -import { - applyProviderAuthConfigPatch, - applyDefaultModel, - pickAuthMethod, - resolveProviderMatch, -} from "./provider-auth-choice-helpers.js"; +import { applyProviderAuthConfigPatch, applyDefaultModel } from "./provider-auth-choice-helpers.js"; import { resolveManifestProviderAuthChoice, type ProviderAuthChoiceMetadata, @@ -38,7 +33,7 @@ import type { type UpsertAuthProfileParams = Parameters[0]; -export type ApplyProviderAuthChoiceParams = { +type ApplyProviderAuthChoiceParams = { authChoice: string; config: OpenClawConfig; env?: NodeJS.ProcessEnv; @@ -51,20 +46,12 @@ export type ApplyProviderAuthChoiceParams = { opts?: Partial; }; -export type ApplyProviderAuthChoiceResult = { +type ApplyProviderAuthChoiceResult = { config: OpenClawConfig; agentModelOverride?: string; retrySelection?: boolean; }; -export type PluginProviderAuthChoiceOptions = { - authChoice: string; - pluginId: string; - providerId: string; - methodId?: string; - label: string; -}; - function formatModelRefForDisplay(modelRef: string, provider: ProviderPlugin): string { if (!provider.preserveLiteralProviderPrefix) { return modelRef; @@ -219,15 +206,8 @@ async function applyDefaultModelFromAuthChoice(params: { type ProviderAuthChoiceRuntime = typeof import("./provider-auth-choice.runtime.js"); -const defaultProviderAuthChoiceDeps = { - loadPluginProviderRuntime: async (): Promise => - import("./provider-auth-choice.runtime.js"), -}; - -let providerAuthChoiceDeps = defaultProviderAuthChoiceDeps; - -async function loadPluginProviderRuntime() { - return await providerAuthChoiceDeps.loadPluginProviderRuntime(); +async function loadPluginProviderRuntime(): Promise { + return await import("./provider-auth-choice.runtime.js"); } function resolveManifestAuthChoiceScope(params: { @@ -247,19 +227,6 @@ function resolveManifestAuthChoiceScope(params: { function withProviderPluginId(provider: ProviderPlugin, pluginId: string): ProviderPlugin { return provider.pluginId === pluginId ? provider : { ...provider, pluginId }; } - -export const testing = { - resetDepsForTest(): void { - providerAuthChoiceDeps = defaultProviderAuthChoiceDeps; - }, - setDepsForTest(deps: Partial): void { - providerAuthChoiceDeps = { - ...defaultProviderAuthChoiceDeps, - ...deps, - }; - }, -} as const; - export async function runProviderPluginAuthMethodUnpersisted(params: { config: OpenClawConfig; env?: NodeJS.ProcessEnv; @@ -551,110 +518,6 @@ export async function applyAuthChoiceLoadedPluginProvider( return { config: nextConfig, agentModelOverride }; } - -export async function applyAuthChoicePluginProvider( - params: ApplyProviderAuthChoiceParams, - options: PluginProviderAuthChoiceOptions, -): Promise { - if (params.authChoice !== options.authChoice) { - return null; - } - - const enableResult = enablePluginInConfig(params.config, options.pluginId); - let nextConfig = enableResult.config; - if (!enableResult.enabled) { - await params.prompter.note( - `${options.label} plugin is disabled (${enableResult.reason ?? "blocked"}).`, - options.label, - ); - return { config: nextConfig }; - } - - const agentId = params.agentId ?? resolveDefaultAgentId(nextConfig); - const agentDir = params.agentDir ?? resolveAgentDir(nextConfig, agentId); - const workspaceDir = - resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir(); - - const { resolvePluginProviders, runProviderModelSelectedHook } = - await loadPluginProviderRuntime(); - const providers = resolvePluginProviders({ - config: nextConfig, - workspaceDir, - env: params.env, - mode: "setup", - }); - const provider = resolveProviderMatch(providers, options.providerId); - if (!provider) { - await params.prompter.note( - `${options.label} auth plugin is not available. Install or enable the plugin, then rerun onboarding. If this started after an update, run "openclaw doctor --fix" first.`, - options.label, - ); - return { config: nextConfig }; - } - - const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0]; - if (!method) { - await params.prompter.note(`${options.label} auth method missing.`, options.label); - return { config: nextConfig }; - } - - const configBeforeProviderAuth = nextConfig; - const applied = await runProviderPluginAuthMethod({ - config: nextConfig, - env: params.env, - runtime: params.runtime, - prompter: params.prompter, - method, - agentDir, - agentId, - workspaceDir, - secretInputMode: params.opts?.secretInputMode, - allowSecretRefPrompt: false, - opts: params.opts, - }); - - nextConfig = applied.config; - if (applied.defaultModel) { - const selectedModel = applied.defaultModel; - const selectedModelDisplay = formatModelRefForDisplay(selectedModel, provider); - if (params.setDefaultModel) { - nextConfig = await applyDefaultModelFromAuthChoice({ - config: nextConfig, - configBeforeProviderAuth, - selectedModel, - selectedModelDisplay, - preserveExistingDefaultModel: params.preserveExistingDefaultModel, - prompter: params.prompter, - runtime: params.runtime, - workspaceDir, - runSelectedModelHook: async (config) => { - await runProviderModelSelectedHook({ - config, - model: selectedModel, - prompter: params.prompter, - agentDir, - workspaceDir, - }); - }, - }); - return { config: nextConfig }; - } - if (params.agentId) { - await params.prompter.note( - t("wizard.model.defaultSetForAgent", { - agent: params.agentId, - model: selectedModelDisplay, - }), - t("wizard.model.configuredTitle"), - ); - } - nextConfig = restoreConfiguredPrimaryModel(nextConfig, params.config); - return { config: nextConfig, agentModelOverride: selectedModel }; - } - - return { config: nextConfig }; -} - async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams): Promise { const updated = await upsertAuthProfileWithLock(params); if (!updated) { @@ -663,4 +526,3 @@ async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams) ); } } -export { testing as __testing }; diff --git a/src/plugins/provider-auth-choices.test.ts b/src/plugins/provider-auth-choices.test.ts index f19f2689e141..e4f837d14f9a 100644 --- a/src/plugins/provider-auth-choices.test.ts +++ b/src/plugins/provider-auth-choices.test.ts @@ -50,7 +50,6 @@ const { resolveManifestDeprecatedProviderAuthChoice, resolveManifestProviderAuthChoice, resolveManifestProviderAuthChoices, - resolveManifestProviderOnboardAuthFlags, resolveProviderOnboardAuthFlags, } = await import("./provider-auth-choices.js"); const { resetProviderAuthAliasMapCacheForTest, resolveProviderIdForAuth } = @@ -260,7 +259,7 @@ describe("provider auth choice manifest helpers", () => { ]), ], run: () => - expect(resolveManifestProviderOnboardAuthFlags()).toEqual([ + expect(resolveProviderOnboardAuthFlags()).toEqual([ { optionKey: "moonshotApiKey", authChoice: "moonshot-api-key", @@ -391,7 +390,7 @@ describe("provider auth choice manifest helpers", () => { }).map((choice) => choice.choiceId), ).not.toContain("evil-openai-api-key"); expect( - resolveManifestProviderOnboardAuthFlags({ + resolveProviderOnboardAuthFlags({ includeUntrustedWorkspacePlugins: false, }), ).toEqual([ @@ -625,7 +624,7 @@ describe("provider auth choice manifest helpers", () => { }, ]); expect(resolveManifestProviderAuthChoice("openai-api-key")?.providerId).toBe("openai"); - expect(resolveManifestProviderOnboardAuthFlags()).toEqual([ + expect(resolveProviderOnboardAuthFlags()).toEqual([ { optionKey: "openaiApiKey", authChoice: "openai-api-key", @@ -685,7 +684,7 @@ describe("provider auth choice manifest helpers", () => { }, ]); expect(resolveManifestProviderAuthChoice("openai-api-key")?.providerId).toBe("custom-openai"); - expect(resolveManifestProviderOnboardAuthFlags()).toEqual([ + expect(resolveProviderOnboardAuthFlags()).toEqual([ { optionKey: "openaiApiKey", authChoice: "openai-api-key", diff --git a/src/plugins/provider-auth-choices.ts b/src/plugins/provider-auth-choices.ts index cd328e922a39..49a2f1f8d44e 100644 --- a/src/plugins/provider-auth-choices.ts +++ b/src/plugins/provider-auth-choices.ts @@ -33,7 +33,7 @@ export type ProviderAuthChoiceMetadata = { onboardingScopes?: ("text-inference" | "image-generation" | "music-generation")[]; }; -export type ProviderOnboardAuthFlag = { +type ProviderOnboardAuthFlag = { optionKey: string; authChoice: string; cliFlag: string; @@ -317,7 +317,7 @@ export function resolveManifestDeprecatedProviderAuthChoice( }); } -export function resolveManifestProviderOnboardAuthFlags( +function resolveManifestProviderOnboardAuthFlags( params?: ManifestProviderAuthChoiceParams, ): ProviderOnboardAuthFlag[] { const preferredByFlag = new Map(); diff --git a/src/plugins/provider-auth-input.test.ts b/src/plugins/provider-auth-input.test.ts index e5a5fd945bde..bb6817494950 100644 --- a/src/plugins/provider-auth-input.test.ts +++ b/src/plugins/provider-auth-input.test.ts @@ -4,7 +4,6 @@ import type { WizardPrompter } from "../wizard/prompts.js"; import { ensureApiKeyFromEnvOrPrompt, ensureApiKeyFromOptionEnvOrPrompt, - maybeApplyApiKeyFromOption, normalizeApiKeyInput, normalizeTokenProviderInput, validateApiKeyInput, @@ -169,18 +168,6 @@ async function runEnsureMinimaxApiKeyFlow(params: { confirmResult: boolean; text return { result, setCredential, confirm, text }; } -async function runMaybeApplyDemoToken(tokenProvider: string) { - const setCredential = vi.fn(async () => undefined); - const result = await maybeApplyApiKeyFromOption({ - token: " opt-key ", - tokenProvider, - expectedProviders: ["demo-provider"], - normalize: (value) => value.trim(), - setCredential, - }); - return { result, setCredential }; -} - function expectMinimaxEnvRefCredentialStored(setCredential: ReturnType) { expect(setCredential).toHaveBeenCalledWith( { source: "env", provider: "default", id: "MINIMAX_API_KEY" }, @@ -254,50 +241,6 @@ describe("validateApiKeyInput", () => { }); }); -describe("maybeApplyApiKeyFromOption", () => { - it.each(["demo-provider", " DeMo-PrOvIdEr "])( - "stores normalized token when provider %p matches", - async (tokenProvider) => { - const { result, setCredential } = await runMaybeApplyDemoToken(tokenProvider); - - expect(result).toBe("opt-key"); - expect(setCredential).toHaveBeenCalledWith("opt-key", undefined); - }, - ); - - it("skips when provider does not match", async () => { - const setCredential = vi.fn(async () => undefined); - - const result = await maybeApplyApiKeyFromOption({ - token: "opt-key", - tokenProvider: "other-provider", - expectedProviders: ["demo-provider"], - normalize: (value) => value.trim(), - setCredential, - }); - - expect(result).toBeUndefined(); - expect(setCredential).not.toHaveBeenCalled(); - }); - - it("rejects malformed command-shaped option keys before storing them", async () => { - const setCredential = vi.fn(async () => undefined); - - await expect( - maybeApplyApiKeyFromOption({ - token: - "openclaw onboard --non-interactive --auth-choice=zai-coding-global --zai-api-key $ZAI_API_KEY", - tokenProvider: "zai", - expectedProviders: ["zai"], - normalize: normalizeApiKeyInput, - validate: validateApiKeyInput, - setCredential, - }), - ).rejects.toThrow("Paste the API key value, not an OpenClaw onboarding command."); - expect(setCredential).not.toHaveBeenCalled(); - }); -}); - describe("ensureApiKeyFromEnvOrPrompt", () => { it("uses env credential when user confirms", async () => { const { result, setCredential, text } = await runEnsureMinimaxApiKeyFlow({ diff --git a/src/plugins/provider-auth-input.ts b/src/plugins/provider-auth-input.ts index 8578c5363c8d..7bc984943365 100644 --- a/src/plugins/provider-auth-input.ts +++ b/src/plugins/provider-auth-input.ts @@ -18,16 +18,8 @@ import { } from "./provider-auth-ref.js"; import type { SecretInputMode } from "./provider-auth-types.js"; -export { - extractEnvVarFromSourceLabel, - promptSecretRefForSetup, - resolveRefFallbackInput, - type SecretRefSetupPromptCopy, -} from "./provider-auth-ref.js"; -export { - resolveSecretInputModeForEnvSelection, - type SecretInputModePromptCopy, -} from "./provider-auth-mode.js"; +export { promptSecretRefForSetup } from "./provider-auth-ref.js"; +export { resolveSecretInputModeForEnvSelection } from "./provider-auth-mode.js"; const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 }; @@ -111,7 +103,7 @@ export function normalizeSecretInputModeInput( } /** Applies a CLI-provided API key when its provider selector matches this auth method. */ -export async function maybeApplyApiKeyFromOption(params: { +async function maybeApplyApiKeyFromOption(params: { token: string | undefined; tokenProvider: string | undefined; secretInputMode?: SecretInputMode; diff --git a/src/plugins/provider-auth-mode.ts b/src/plugins/provider-auth-mode.ts index 0f4a8fed3079..4283b9214fb8 100644 --- a/src/plugins/provider-auth-mode.ts +++ b/src/plugins/provider-auth-mode.ts @@ -3,7 +3,7 @@ import type { WizardPrompter } from "../wizard/prompts.js"; import type { SecretInputMode } from "./provider-auth-types.js"; /** Prompt copy overrides for provider secret input mode selection. */ -export type SecretInputModePromptCopy = { +type SecretInputModePromptCopy = { modeMessage?: string; plaintextLabel?: string; plaintextHint?: string; diff --git a/src/plugins/provider-catalog-result.ts b/src/plugins/provider-catalog-result.ts index 2e284f7abf24..4157e9b66cd5 100644 --- a/src/plugins/provider-catalog-result.ts +++ b/src/plugins/provider-catalog-result.ts @@ -46,7 +46,7 @@ const MODEL_DEFINITION_CONFIG_KEYS = [ ] as const satisfies readonly (keyof ModelDefinitionConfig)[]; /** Projection of a provider catalog result into provider config entries. */ -export type ProviderCatalogResultProjection = +type ProviderCatalogResultProjection = | { kind: "provider"; provider: ModelProviderConfig } | { kind: "providers"; providers: Array<[string, ModelProviderConfig]> } | { kind: "empty" }; diff --git a/src/plugins/provider-discovery.test.ts b/src/plugins/provider-discovery.test.ts index d00b89b0501b..ce1efb387fe3 100644 --- a/src/plugins/provider-discovery.test.ts +++ b/src/plugins/provider-discovery.test.ts @@ -1,66 +1,17 @@ /** Tests provider discovery normalization, grouping, and manifest contribution handling. */ import fs from "node:fs"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import type { ModelDefinitionConfig, ModelProviderConfig } from "../config/types.js"; -import type { PluginCandidate } from "./discovery.js"; import { groupPluginDiscoveryProvidersByOrder, normalizePluginDiscoveryResult, - resolveInstalledPluginProviderContributionIds, runProviderCatalog, runProviderStaticCatalog, } from "./provider-discovery.js"; import * as providerDiscoveryModule from "./provider-discovery.js"; -import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; import type { ProviderCatalogResult, ProviderDiscoveryOrder, ProviderPlugin } from "./types.js"; -const tempDirs: string[] = []; - -afterEach(() => { - cleanupTrackedTempDirs(tempDirs); -}); - -function makeTempDir() { - return makeTrackedTempDir("openclaw-provider-discovery", tempDirs); -} - -function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { - return { - OPENCLAW_BUNDLED_PLUGINS_DIR: undefined, - OPENCLAW_VERSION: "2026.4.25", - VITEST: "true", - ...overrides, - }; -} - -function createProviderContributionCandidate(params: { - pluginId?: string; - providerIds?: readonly string[]; -}): PluginCandidate { - const rootDir = makeTempDir(); - fs.writeFileSync( - path.join(rootDir, "index.ts"), - "throw new Error('runtime provider entry should not load for cold contribution ids');\n", - "utf-8", - ); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: params.pluginId ?? "demo", - configSchema: { type: "object" }, - providers: params.providerIds ?? ["demo"], - }), - "utf-8", - ); - return { - idHint: params.pluginId ?? "demo", - source: path.join(rootDir, "index.ts"), - rootDir, - origin: "global", - }; -} - function makeProvider(params: { id: string; label?: string; @@ -199,50 +150,6 @@ describe("resolveInstalledPluginProviderContributionIds", () => { it("does not keep exporting the ambiguous runtime-discovery alias", () => { expect(Object.keys(providerDiscoveryModule)).not.toContain("resolvePluginDiscoveryProviders"); }); - - it("reads provider ids from the installed plugin index without importing runtime entries", () => { - const candidate = createProviderContributionCandidate({ - pluginId: "demo", - providerIds: ["demo", "demo-alias"], - }); - - expect( - resolveInstalledPluginProviderContributionIds({ - candidates: [candidate], - env: hermeticEnv(), - preferPersisted: false, - }), - ).toEqual(["demo", "demo-alias"]); - }); - - it("omits disabled plugin provider ids unless explicitly requested", () => { - const candidate = createProviderContributionCandidate({ - pluginId: "demo", - providerIds: ["demo"], - }); - const params = { - candidates: [candidate], - config: { - plugins: { - entries: { - demo: { - enabled: false, - }, - }, - }, - }, - env: hermeticEnv(), - preferPersisted: false, - }; - - expect(resolveInstalledPluginProviderContributionIds(params)).toStrictEqual([]); - expect( - resolveInstalledPluginProviderContributionIds({ - ...params, - includeDisabled: true, - }), - ).toEqual(["demo"]); - }); }); describe("groupPluginDiscoveryProvidersByOrder", () => { diff --git a/src/plugins/provider-discovery.ts b/src/plugins/provider-discovery.ts index 060ddc756e9b..c5342e93c807 100644 --- a/src/plugins/provider-discovery.ts +++ b/src/plugins/provider-discovery.ts @@ -1,12 +1,9 @@ /** Control-plane provider discovery helpers that keep runtime imports lazy until catalog hooks run. */ -import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { normalizeProviderId } from "../agents/model-selection.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; -import { listManifestProviderContributionIds } from "./manifest-contribution-ids.js"; import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js"; -import type { LoadPluginRegistryParams, PluginRegistrySnapshot } from "./plugin-registry.js"; import { copyProviderCatalogResultProjection } from "./provider-catalog-result.js"; import type { ProviderDiscoveryOrder, ProviderPlugin } from "./types.js"; @@ -37,7 +34,7 @@ function isSafeProviderConfigKey(value: string): boolean { } /** Options for resolving plugin providers that can contribute model catalog entries. */ -export type ResolveRuntimePluginDiscoveryProvidersParams = { +type ResolveRuntimePluginDiscoveryProvidersParams = { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; @@ -50,28 +47,6 @@ export type ResolveRuntimePluginDiscoveryProvidersParams = { pluginMetadataSnapshot?: PluginMetadataRegistryView; }; -export type ResolveInstalledPluginProviderContributionIdsParams = LoadPluginRegistryParams & { - index?: PluginRegistrySnapshot; - includeDisabled?: boolean; -}; - -/** Lists provider ids advertised by installed manifests, including disabled entries when requested. */ -export function resolveInstalledPluginProviderContributionIds( - params: ResolveInstalledPluginProviderContributionIdsParams = {}, -): string[] { - const registryParams = - params.candidates && params.preferPersisted === undefined - ? { ...params, preferPersisted: false } - : params; - return sortUniqueStrings( - listManifestProviderContributionIds({ - ...registryParams, - index: params.index, - includeDisabled: params.includeDisabled, - }), - ); -} - /** Loads provider runtime discovery and filters to providers that can produce catalog order entries. */ export async function resolveRuntimePluginDiscoveryProviders( params: ResolveRuntimePluginDiscoveryProvidersParams, diff --git a/src/plugins/provider-hook-runtime.ts b/src/plugins/provider-hook-runtime.ts index c9be8c5fc562..b6465d8de9a3 100644 --- a/src/plugins/provider-hook-runtime.ts +++ b/src/plugins/provider-hook-runtime.ts @@ -38,7 +38,7 @@ let providerRuntimePluginCache: ConfigScopedRuntimeCache const defaultProviderRuntimePluginCache = new PluginLruCache(128); const PREPARED_PROVIDER_RUNTIME_SURFACES = ["channel"] as const; -export type ProviderRuntimePluginLookupParams = { +type ProviderRuntimePluginLookupParams = { provider: string; modelId?: string | null; config?: OpenClawConfig; @@ -53,7 +53,7 @@ export type ProviderRuntimePluginHandle = ProviderRuntimePluginLookupParams & { plugin?: ProviderPlugin; }; -export type ProviderRuntimePluginHandleParams = ProviderRuntimePluginLookupParams & { +type ProviderRuntimePluginHandleParams = ProviderRuntimePluginLookupParams & { runtimeHandle?: ProviderRuntimePluginHandle; }; diff --git a/src/plugins/provider-model-compat.ts b/src/plugins/provider-model-compat.ts index 17a42059108c..f4456e9f6b2f 100644 --- a/src/plugins/provider-model-compat.ts +++ b/src/plugins/provider-model-compat.ts @@ -1,8 +1,5 @@ // Normalizes provider model compatibility metadata from plugins. -import { - resolveUnsupportedToolSchemaKeywords, - shouldOmitEmptyArrayItems, -} from "@openclaw/ai/internal/openai"; +import { resolveUnsupportedToolSchemaKeywords } from "@openclaw/ai/internal/openai"; import { detectOpenAICompletionsCompat } from "../agents/openai-completions-compat.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import type { Model } from "../llm/types.js"; @@ -60,7 +57,7 @@ export function resolveToolCallArgumentsEncoding( // Tool-schema compat predicates moved into @openclaw/ai (agent-tools-parameter-schema); // re-export so existing core/plugin callers keep one canonical import site. -export { resolveUnsupportedToolSchemaKeywords, shouldOmitEmptyArrayItems }; +export { resolveUnsupportedToolSchemaKeywords }; function isOpenAiCompletionsModel(model: Model): model is Model<"openai-completions"> { return model.api === "openai-completions"; diff --git a/src/plugins/provider-oauth-flow.ts b/src/plugins/provider-oauth-flow.ts index afc5d46e58d5..da396cb0ea54 100644 --- a/src/plugins/provider-oauth-flow.ts +++ b/src/plugins/provider-oauth-flow.ts @@ -3,7 +3,7 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; /** Prompt payload used when OAuth flow code entry needs user input. */ -export type OAuthPrompt = { message: string; placeholder?: string }; +type OAuthPrompt = { message: string; placeholder?: string }; const validateRequiredInput = (value: string) => (value.trim().length > 0 ? undefined : "Required"); diff --git a/src/plugins/provider-openai-chatgpt-oauth-tls.ts b/src/plugins/provider-openai-chatgpt-oauth-tls.ts index dc9f245428e9..894dd8fc1662 100644 --- a/src/plugins/provider-openai-chatgpt-oauth-tls.ts +++ b/src/plugins/provider-openai-chatgpt-oauth-tls.ts @@ -28,7 +28,7 @@ const OPENAI_PROVIDER_ID = "openai"; type PreflightFailureKind = "tls-cert" | "network"; -export type OpenAIOAuthTlsPreflightResult = +type OpenAIOAuthTlsPreflightResult = | { ok: true } | { ok: false; diff --git a/src/plugins/provider-runtime.runtime.ts b/src/plugins/provider-runtime.runtime.ts index 92ac06ba9081..4fdf70702c8f 100644 --- a/src/plugins/provider-runtime.runtime.ts +++ b/src/plugins/provider-runtime.runtime.ts @@ -7,8 +7,6 @@ type AugmentModelCatalogWithProviderPlugins = ProviderRuntimeModule["augmentModelCatalogWithProviderPlugins"]; type BuildProviderAuthDoctorHintWithPlugin = ProviderRuntimeModule["buildProviderAuthDoctorHintWithPlugin"]; -type BuildProviderMissingAuthMessageWithPlugin = - ProviderRuntimeModule["buildProviderMissingAuthMessageWithPlugin"]; type FormatProviderAuthProfileApiKeyWithPlugin = ProviderRuntimeModule["formatProviderAuthProfileApiKeyWithPlugin"]; type PrepareProviderRuntimeAuth = ProviderRuntimeModule["prepareProviderRuntimeAuth"]; @@ -41,14 +39,6 @@ export async function buildProviderAuthDoctorHintWithPlugin( return runtime.buildProviderAuthDoctorHintWithPlugin(...args); } -/** Lazily builds missing-auth messages with provider plugin context. */ -export async function buildProviderMissingAuthMessageWithPlugin( - ...args: Parameters -): Promise>> { - const runtime = await loadProviderRuntime(); - return runtime.buildProviderMissingAuthMessageWithPlugin(...args); -} - /** Lazily formats API-key auth profile display text with provider plugin rules. */ export async function formatProviderAuthProfileApiKeyWithPlugin( ...args: Parameters diff --git a/src/plugins/provider-runtime.test-support.ts b/src/plugins/provider-runtime.test-support.ts index 7993d4358a25..7e61fbda2ec7 100644 --- a/src/plugins/provider-runtime.test-support.ts +++ b/src/plugins/provider-runtime.test-support.ts @@ -13,7 +13,7 @@ const openaiCodexCatalogEntries = [ { provider: "openai", id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, ]; -export const expectedAugmentedOpenaiCodexCatalogEntries = [ +const expectedAugmentedOpenaiCodexCatalogEntries = [ { provider: "openai", id: "gpt-5.4", name: "gpt-5.4" }, { provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, { provider: "openai", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index 55b8f84c4888..db3cf6198cb3 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -7,7 +7,6 @@ import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js"; import { expectAugmentedCodexCatalog, expectCodexMissingAuthHint, - expectedAugmentedOpenaiCodexCatalogEntries, } from "./provider-runtime.test-support.js"; import type { AnyAgentTool, @@ -2612,40 +2611,6 @@ describe("provider-runtime", () => { }); }); - it("resolves bundled catalog hooks through provider plugins", async () => { - resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]); - resolvePluginProvidersMock.mockImplementation((params?: { onlyPluginIds?: string[] }) => { - const onlyPluginIds = params?.onlyPluginIds; - if (!onlyPluginIds || !onlyPluginIds.includes("openai")) { - return []; - } - return [createOpenAiCatalogProviderPlugin()]; - }); - - await expect( - augmentModelCatalogWithProviderPlugins({ - env: process.env, - context: { - env: process.env, - entries: [ - { provider: "openai", id: "gpt-5.4", name: "GPT-5.2" }, - { provider: "openai", id: "gpt-5.4-pro", name: "GPT-5.2 Pro" }, - { provider: "openai", id: "gpt-5.4-mini", name: "GPT-5 mini" }, - { provider: "openai", id: "gpt-5.4-nano", name: "GPT-5 nano" }, - { provider: "openai", id: "gpt-5.4", name: "GPT-5.4" }, - ], - }, - }), - ).resolves.toEqual(expectedAugmentedOpenaiCodexCatalogEntries); - - expectRecordFields(getLastResolvePluginProvidersParams(), { - onlyPluginIds: ["openai"], - activate: false, - }); - expect(resolveCatalogHookProviderPluginIdsMock).toHaveBeenCalledTimes(1); - expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1); - }); - it("does not stack-overflow when provider hook resolution reenters the same plugin load", () => { let providerLoadInFlight = false; isPluginProvidersLoadInFlightMock.mockImplementation(() => providerLoadInFlight); diff --git a/src/plugins/provider-thinking.types.ts b/src/plugins/provider-thinking.types.ts index b7fc7a800cb0..6792916e9cdb 100644 --- a/src/plugins/provider-thinking.types.ts +++ b/src/plugins/provider-thinking.types.ts @@ -10,7 +10,7 @@ export type ProviderThinkingPolicyContext = { modelId: string; }; -export type ProviderThinkingModelCompat = { +type ProviderThinkingModelCompat = { thinkingFormat?: string; supportedReasoningEfforts?: readonly string[] | null; }; @@ -34,7 +34,7 @@ export type ProviderDefaultThinkingPolicyContext = ProviderThinkingPolicyContext compat?: ProviderThinkingModelCompat | null; }; -export type ProviderThinkingLevelId = +type ProviderThinkingLevelId = | "off" | "minimal" | "low" @@ -45,7 +45,7 @@ export type ProviderThinkingLevelId = | "max" | "ultra"; -export type ProviderThinkingLevel = { +type ProviderThinkingLevel = { id: ProviderThinkingLevelId; /** * Optional display label. Use this when the stored value differs from the diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index 7383f5925866..b060717416c5 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -19,7 +19,7 @@ import type { const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:"; -export type ProviderWizardOption = { +type ProviderWizardOption = { value: string; label: string; hint?: string; diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index e8a7e26e9ec4..611c15490c18 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -37,7 +37,7 @@ type NormalizedPluginsConfig = ReturnType 0 ? [{ pluginId, providerIds }] : []; }); } -export { testing as __testing }; diff --git a/src/plugins/public-surface-loader.ts b/src/plugins/public-surface-loader.ts index 4131634c51ef..28f39d14966d 100644 --- a/src/plugins/public-surface-loader.ts +++ b/src/plugins/public-surface-loader.ts @@ -223,9 +223,3 @@ export function loadBundledPluginPublicArtifactModuleFromCandidatesSync { /must stay plugin-local/, ); }); - - it("rejects bundled plugin directory traversal", () => { - expect(normalizeBundledPluginDirName("document-extract")).toBe("document-extract"); - expect(() => normalizeBundledPluginDirName("../outside")).toThrow(/single directory/); - expect(() => normalizeBundledPluginDirName("nested/plugin")).toThrow(/single directory/); - expect(() => normalizeBundledPluginDirName("nested\\plugin")).toThrow(/single directory/); - expect(() => normalizeBundledPluginDirName("C:plugin")).toThrow(/single directory/); - }); }); diff --git a/src/plugins/public-surface-runtime.ts b/src/plugins/public-surface-runtime.ts index 7b10e685eb04..37a67da07668 100644 --- a/src/plugins/public-surface-runtime.ts +++ b/src/plugins/public-surface-runtime.ts @@ -42,7 +42,7 @@ export function normalizeBundledPluginArtifactSubpath(artifactBasename: string): } /** Normalizes a bundled plugin directory name and rejects path-like values. */ -export function normalizeBundledPluginDirName(dirName: string): string { +function normalizeBundledPluginDirName(dirName: string): string { const normalized = dirName.trim(); if ( !normalized || diff --git a/src/plugins/register-plugin-cli-command-groups.ts b/src/plugins/register-plugin-cli-command-groups.ts index f59c43ba3387..e00c034e37cb 100644 --- a/src/plugins/register-plugin-cli-command-groups.ts +++ b/src/plugins/register-plugin-cli-command-groups.ts @@ -9,12 +9,12 @@ import { } from "../cli/program/register-command-groups.js"; import type { OpenClawPluginCliCommandDescriptor, PluginLogger } from "./types.js"; -export type PluginCliCommandGroupEntry = CommandGroupEntry & { +type PluginCliCommandGroupEntry = CommandGroupEntry & { pluginId: string; parentPath?: readonly string[]; }; -export type PluginCliCommandGroupMode = "eager" | "lazy"; +type PluginCliCommandGroupMode = "eager" | "lazy"; function canRegisterPluginCliLazily(entry: PluginCliCommandGroupEntry): boolean { if (entry.placeholders.length === 0) { diff --git a/src/plugins/registry-types.ts b/src/plugins/registry-types.ts index f77b11e7ffd3..2abac8001758 100644 --- a/src/plugins/registry-types.ts +++ b/src/plugins/registry-types.ts @@ -90,7 +90,7 @@ export type PluginToolRegistration = { rootDir?: string; }; -export type PluginCliRegistration = { +type PluginCliRegistration = { pluginId: string; pluginName?: string; register: OpenClawPluginCliRegistrar; @@ -118,7 +118,7 @@ export type PluginHttpRouteRegistration = { source?: string; }; -export type PluginHostedMediaResolverRegistration = { +type PluginHostedMediaResolverRegistration = { pluginId: string; pluginName?: string; resolver: OpenClawPluginHostedMediaResolver; @@ -136,7 +136,7 @@ export type PluginChannelRegistration = { rootDir?: string; }; -export type PluginChannelSetupRegistration = { +type PluginChannelSetupRegistration = { pluginId: string; pluginName?: string; plugin: ChannelPlugin; @@ -147,7 +147,7 @@ export type PluginChannelSetupRegistration = { rootDir?: string; }; -export type PluginProviderRegistration = { +type PluginProviderRegistration = { pluginId: string; pluginName?: string; provider: ProviderPlugin; @@ -155,7 +155,7 @@ export type PluginProviderRegistration = { rootDir?: string; }; -export type PluginModelCatalogProviderRegistration = { +type PluginModelCatalogProviderRegistration = { pluginId: string; pluginName?: string; provider: UnifiedModelCatalogProviderPlugin; @@ -163,7 +163,7 @@ export type PluginModelCatalogProviderRegistration = { rootDir?: string; }; -export type PluginSessionCatalogRegistration = { +type PluginSessionCatalogRegistration = { pluginId: string; pluginName?: string; provider: SessionCatalogProvider; @@ -171,7 +171,7 @@ export type PluginSessionCatalogRegistration = { rootDir?: string; }; -export type PluginCliBackendRegistration = { +type PluginCliBackendRegistration = { pluginId: string; pluginName?: string; backend: CliBackendPlugin; @@ -195,34 +195,30 @@ type PluginOwnedProviderRegistration = { rootDir?: string; }; -export type PluginSpeechProviderRegistration = - PluginOwnedProviderRegistration; -export type PluginEmbeddingProviderRegistration = +type PluginSpeechProviderRegistration = PluginOwnedProviderRegistration; +type PluginEmbeddingProviderRegistration = PluginOwnedProviderRegistration; -export type PluginRealtimeTranscriptionProviderRegistration = +type PluginRealtimeTranscriptionProviderRegistration = PluginOwnedProviderRegistration; -export type PluginRealtimeVoiceProviderRegistration = +type PluginRealtimeVoiceProviderRegistration = PluginOwnedProviderRegistration; -export type PluginMediaUnderstandingProviderRegistration = +type PluginMediaUnderstandingProviderRegistration = PluginOwnedProviderRegistration; -export type PluginTranscriptsSourceProviderRegistration = +type PluginTranscriptsSourceProviderRegistration = PluginOwnedProviderRegistration; -export type PluginImageGenerationProviderRegistration = +type PluginImageGenerationProviderRegistration = PluginOwnedProviderRegistration; -export type PluginVideoGenerationProviderRegistration = +type PluginVideoGenerationProviderRegistration = PluginOwnedProviderRegistration; -export type PluginMusicGenerationProviderRegistration = +type PluginMusicGenerationProviderRegistration = PluginOwnedProviderRegistration; -export type PluginWebFetchProviderRegistration = - PluginOwnedProviderRegistration; -export type PluginWebSearchProviderRegistration = - PluginOwnedProviderRegistration; -export type PluginWorkerProviderRegistration = PluginOwnedProviderRegistration; -export type PluginMigrationProviderRegistration = - PluginOwnedProviderRegistration; -export type PluginMemoryEmbeddingProviderRegistration = +type PluginWebFetchProviderRegistration = PluginOwnedProviderRegistration; +type PluginWebSearchProviderRegistration = PluginOwnedProviderRegistration; +type PluginWorkerProviderRegistration = PluginOwnedProviderRegistration; +type PluginMigrationProviderRegistration = PluginOwnedProviderRegistration; +type PluginMemoryEmbeddingProviderRegistration = PluginOwnedProviderRegistration; -export type PluginCodexAppServerExtensionFactoryRegistration = { +type PluginCodexAppServerExtensionFactoryRegistration = { pluginId: string; pluginName?: string; rawFactory: CodexAppServerExtensionFactory; @@ -239,7 +235,7 @@ export type PluginAgentToolResultMiddlewareRegistration = { source: string; rootDir?: string; }; -export type PluginAgentHarnessRegistration = { +type PluginAgentHarnessRegistration = { pluginId: string; pluginName?: string; harness: AgentHarness; @@ -247,7 +243,7 @@ export type PluginAgentHarnessRegistration = { rootDir?: string; }; -export type PluginHookRegistration = { +type PluginHookRegistration = { pluginId: string; entry: HookEntry; events: string[]; @@ -273,7 +269,7 @@ export type PluginGatewayDiscoveryServiceRegistration = { rootDir?: string; }; -export type PluginReloadRegistration = { +type PluginReloadRegistration = { pluginId: string; pluginName?: string; registration: OpenClawPluginReloadRegistration; @@ -289,7 +285,7 @@ export type PluginNodeHostCommandRegistration = { rootDir?: string; }; -export type PluginNodeInvokePolicyRegistration = { +type PluginNodeInvokePolicyRegistration = { pluginId: string; pluginName?: string; policy: import("./types.js").OpenClawPluginNodeInvokePolicy; @@ -298,7 +294,7 @@ export type PluginNodeInvokePolicyRegistration = { rootDir?: string; }; -export type PluginSecurityAuditCollectorRegistration = { +type PluginSecurityAuditCollectorRegistration = { pluginId: string; pluginName?: string; collector: OpenClawPluginSecurityAuditCollector; @@ -314,13 +310,13 @@ export type PluginCommandRegistration = { rootDir?: string; }; -export type PluginInteractiveHandlerRegistryRegistration = PluginInteractiveHandlerRegistration & { +type PluginInteractiveHandlerRegistryRegistration = PluginInteractiveHandlerRegistration & { pluginId: string; pluginName?: string; pluginRoot?: string; }; -export type PluginSessionExtensionRegistryRegistration = { +type PluginSessionExtensionRegistryRegistration = { pluginId: string; pluginName?: string; extension: PluginSessionExtensionRegistration; @@ -337,7 +333,7 @@ export type PluginTrustedToolPolicyRegistryRegistration = { rootDir?: string; }; -export type PluginToolMetadataRegistryRegistration = { +type PluginToolMetadataRegistryRegistration = { pluginId: string; pluginName?: string; metadata: PluginToolMetadataRegistration; @@ -345,7 +341,7 @@ export type PluginToolMetadataRegistryRegistration = { rootDir?: string; }; -export type PluginControlUiDescriptorRegistryRegistration = { +type PluginControlUiDescriptorRegistryRegistration = { pluginId: string; pluginName?: string; descriptor: PluginControlUiDescriptor; @@ -353,7 +349,7 @@ export type PluginControlUiDescriptorRegistryRegistration = { rootDir?: string; }; -export type PluginRuntimeLifecycleRegistryRegistration = { +type PluginRuntimeLifecycleRegistryRegistration = { pluginId: string; pluginName?: string; lifecycle: PluginRuntimeLifecycleRegistration; @@ -361,7 +357,7 @@ export type PluginRuntimeLifecycleRegistryRegistration = { rootDir?: string; }; -export type PluginAgentEventSubscriptionRegistryRegistration = { +type PluginAgentEventSubscriptionRegistryRegistration = { pluginId: string; pluginName?: string; subscription: PluginAgentEventSubscriptionRegistration; @@ -369,7 +365,7 @@ export type PluginAgentEventSubscriptionRegistryRegistration = { rootDir?: string; }; -export type PluginSessionSchedulerJobRegistryRegistration = { +type PluginSessionSchedulerJobRegistryRegistration = { pluginId: string; pluginName?: string; job: PluginSessionSchedulerJobRegistration; @@ -386,7 +382,7 @@ export type PluginSessionActionRegistryRegistration = { rootDir?: string; }; -export type PluginConversationBindingResolvedHandlerRegistration = { +type PluginConversationBindingResolvedHandlerRegistration = { pluginId: string; pluginName?: string; pluginRoot?: string; @@ -522,11 +518,3 @@ export type PluginRegistryParams = { }; activateGlobalSideEffects?: boolean; }; - -export type PluginRegistrationMode = import("./types.js").PluginRegistrationMode; -export type OpenClawPluginNodeHostCommand = import("./types.js").OpenClawPluginNodeHostCommand; -export type OpenClawPluginToolContext = import("./types.js").OpenClawPluginToolContext; -export type OpenClawPluginHttpRouteParams = import("./types.js").OpenClawPluginHttpRouteParams; -export type OpenClawPluginHookOptions = import("./types.js").OpenClawPluginHookOptions; -export type PluginHookHandlerMap = import("./types.js").PluginHookHandlerMap; -export type OpenClawPluginApi = import("./types.js").OpenClawPluginApi; diff --git a/src/plugins/registry.channel-guard.test.ts b/src/plugins/registry.channel-guard.test.ts index 38b4fab9316d..1d4d8d2cd66f 100644 --- a/src/plugins/registry.channel-guard.test.ts +++ b/src/plugins/registry.channel-guard.test.ts @@ -4,7 +4,7 @@ import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createPluginRegistry } from "./registry.js"; import type { PluginRuntime } from "./runtime/types.js"; -import { createPluginRecord } from "./status.test-helpers.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; import type { OpenClawPluginChannelRegistration } from "./types.js"; function createTestRegistry() { diff --git a/src/plugins/registry.control-ui.test.ts b/src/plugins/registry.control-ui.test.ts index 20074459459d..a330d6a7d867 100644 --- a/src/plugins/registry.control-ui.test.ts +++ b/src/plugins/registry.control-ui.test.ts @@ -4,7 +4,7 @@ import { registerTestPlugin, } from "openclaw/plugin-sdk/plugin-test-contracts"; import { describe, expect, it } from "vitest"; -import { createPluginRecord } from "./status.test-helpers.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; describe("plugin registry Control UI descriptors", () => { it("keeps legacy flat descriptors loadable for shipped JavaScript plugins", () => { diff --git a/src/plugins/registry.dual-kind-memory-gate.test.ts b/src/plugins/registry.dual-kind-memory-gate.test.ts index 4c07d52adb08..0deef0b0fd38 100644 --- a/src/plugins/registry.dual-kind-memory-gate.test.ts +++ b/src/plugins/registry.dual-kind-memory-gate.test.ts @@ -10,8 +10,8 @@ import { clearMemoryPluginState, getMemoryCapabilityRegistration, getMemoryRuntime, -} from "./memory-state.js"; -import { createPluginRecord } from "./status.test-helpers.js"; +} from "./memory-state.test-fixtures.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; afterEach(() => { clearMemoryPluginState(); diff --git a/src/plugins/registry.test-fixtures.ts b/src/plugins/registry.test-fixtures.ts new file mode 100644 index 000000000000..7a098730d0ef --- /dev/null +++ b/src/plugins/registry.test-fixtures.ts @@ -0,0 +1,8 @@ +/** Test-only registry type projections. */ +import type { PluginRegistry } from "./registry.js"; + +export * from "./registry.js"; + +export type PluginProviderRegistration = PluginRegistry["providers"][number]; +export type PluginMemoryEmbeddingProviderRegistration = + PluginRegistry["memoryEmbeddingProviders"][number]; diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index a00daced5b94..8a1448e59fb4 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -17,43 +17,7 @@ export type PluginHttpRouteRegistration = RegistryTypesPluginHttpRouteRegistrati gatewayRuntimeScopeSurface?: OpenClawPluginGatewayRuntimeScopeSurface; }; -export type { - PluginChannelRegistration, - PluginChannelSetupRegistration, - PluginCliBackendRegistration, - PluginCliRegistration, - PluginCommandRegistration, - PluginConversationBindingResolvedHandlerRegistration, - PluginHookRegistration, - PluginAgentHarnessRegistration, - PluginMemoryEmbeddingProviderRegistration, - PluginNodeHostCommandRegistration, - PluginProviderRegistration, - PluginWorkerProviderRegistration, - PluginControlUiDescriptorRegistryRegistration, - PluginHostedMediaResolverRegistration, - PluginRecord, - PluginRegistry, - PluginRegistryParams, - PluginTextTransformsRegistration, - PluginToolMetadataRegistryRegistration, - PluginTrustedToolPolicyRegistryRegistration, - PluginToolRegistration, - PluginSpeechProviderRegistration, - PluginRealtimeTranscriptionProviderRegistration, - PluginRealtimeVoiceProviderRegistration, - PluginMediaUnderstandingProviderRegistration, - PluginImageGenerationProviderRegistration, - PluginVideoGenerationProviderRegistration, - PluginMusicGenerationProviderRegistration, - PluginWebFetchProviderRegistration, - PluginWebSearchProviderRegistration, - PluginReloadRegistration, - PluginRuntimeLifecycleRegistryRegistration, - PluginSecurityAuditCollectorRegistration, - PluginServiceRegistration, - PluginSessionExtensionRegistryRegistration, -} from "./registry-types.js"; +export type { PluginRecord, PluginRegistry } from "./registry-types.js"; export { createEmptyPluginRegistry } from "./registry-empty.js"; /** diff --git a/src/plugins/roots.ts b/src/plugins/roots.ts index 3e6558ee1ec3..0032331d5de1 100644 --- a/src/plugins/roots.ts +++ b/src/plugins/roots.ts @@ -10,7 +10,7 @@ export type PluginSourceRoots = { workspace?: string; }; -export type PluginCacheInputs = { +type PluginCacheInputs = { roots: PluginSourceRoots; loadPaths: string[]; }; diff --git a/src/plugins/runtime-channel-state.ts b/src/plugins/runtime-channel-state.ts index c1f5d0438449..d7a3dffd4505 100644 --- a/src/plugins/runtime-channel-state.ts +++ b/src/plugins/runtime-channel-state.ts @@ -2,7 +2,7 @@ import type { ActivePluginChannelRegistry } from "./channel-registry-state.types.js"; /** Global symbol that stores process-current plugin registry state. */ -export const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); +const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); type GlobalChannelRegistryState = typeof globalThis & { [PLUGIN_REGISTRY_STATE]?: { diff --git a/src/plugins/runtime-state.ts b/src/plugins/runtime-state.ts index 64e1e1c4c885..4b1154ed1e46 100644 --- a/src/plugins/runtime-state.ts +++ b/src/plugins/runtime-state.ts @@ -5,7 +5,7 @@ export const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); type PluginRegistry = import("./registry-types.js").PluginRegistry; -export type RuntimeTrackedPluginRegistry = PluginRegistry; +type RuntimeTrackedPluginRegistry = PluginRegistry; export type RegistrySurfaceState = { registry: RuntimeTrackedPluginRegistry | null; @@ -33,12 +33,6 @@ type GlobalRegistryState = typeof globalThis & { export function getPluginRegistryState(): RegistryState | undefined { return (globalThis as GlobalRegistryState)[PLUGIN_REGISTRY_STATE]; } - -export function getActivePluginChannelRegistryFromState(): RuntimeTrackedPluginRegistry | null { - const state = getPluginRegistryState(); - return state?.channel.registry ?? state?.activeRegistry ?? null; -} - export function getActivePluginRegistryWorkspaceDirFromState(): string | undefined { return getPinnedWorkspaceDirFromState(); } diff --git a/src/plugins/runtime.test.ts b/src/plugins/runtime.test.ts index 33adcc5b60e5..b11668660610 100644 --- a/src/plugins/runtime.test.ts +++ b/src/plugins/runtime.test.ts @@ -21,7 +21,7 @@ import { resolveActivePluginHttpRouteRegistry, setActivePluginRegistry, } from "./runtime.js"; -import { createPluginRecord } from "./status.test-helpers.js"; +import { createPluginRecord } from "./status.test-fixtures.js"; function createRegistryWithRoute(path: string) { const registry = createEmptyPluginRegistry(); diff --git a/src/plugins/runtime/gateway-bindings.test-fixtures.ts b/src/plugins/runtime/gateway-bindings.test-fixtures.ts new file mode 100644 index 000000000000..69ec2baab266 --- /dev/null +++ b/src/plugins/runtime/gateway-bindings.test-fixtures.ts @@ -0,0 +1,7 @@ +/** Clears gateway plugin runtime bindings between tests. */ +import { gatewaySubagentState } from "./gateway-bindings.js"; + +export function clearGatewaySubagentRuntime(): void { + gatewaySubagentState.subagent = undefined; + gatewaySubagentState.nodes = undefined; +} diff --git a/src/plugins/runtime/gateway-bindings.ts b/src/plugins/runtime/gateway-bindings.ts index bd7bf18c9929..f7cca5fce277 100644 --- a/src/plugins/runtime/gateway-bindings.ts +++ b/src/plugins/runtime/gateway-bindings.ts @@ -32,12 +32,3 @@ export function setGatewaySubagentRuntime(subagent: PluginRuntime["subagent"]): export function setGatewayNodesRuntime(nodes: PluginRuntime["nodes"]): void { gatewaySubagentState.nodes = nodes; } - -/** - * Reset the process-global gateway subagent runtime. - * Used by tests to avoid leaking gateway state across module reloads. - */ -export function clearGatewaySubagentRuntime(): void { - gatewaySubagentState.subagent = undefined; - gatewaySubagentState.nodes = undefined; -} diff --git a/src/plugins/runtime/gateway-request-scope.test-fixtures.ts b/src/plugins/runtime/gateway-request-scope.test-fixtures.ts new file mode 100644 index 000000000000..bedac5791985 --- /dev/null +++ b/src/plugins/runtime/gateway-request-scope.test-fixtures.ts @@ -0,0 +1,8 @@ +/** Test-only gateway request scope type projections. */ +import { withPluginRuntimeGatewayRequestScope } from "./gateway-request-scope.js"; + +export * from "./gateway-request-scope.js"; + +export type PluginRuntimeGatewayRequestScope = Parameters< + typeof withPluginRuntimeGatewayRequestScope +>[0]; diff --git a/src/plugins/runtime/gateway-request-scope.test.ts b/src/plugins/runtime/gateway-request-scope.test.ts index f3ffb04ab6ff..cde5bd594120 100644 --- a/src/plugins/runtime/gateway-request-scope.test.ts +++ b/src/plugins/runtime/gateway-request-scope.test.ts @@ -1,6 +1,6 @@ // Gateway request scope tests cover request-local plugin runtime context propagation. import { describe, expect, it, vi } from "vitest"; -import type { PluginRuntimeGatewayRequestScope } from "./gateway-request-scope.js"; +import type { PluginRuntimeGatewayRequestScope } from "./gateway-request-scope.test-fixtures.js"; const TEST_SCOPE: PluginRuntimeGatewayRequestScope = { context: {} as PluginRuntimeGatewayRequestScope["context"], diff --git a/src/plugins/runtime/gateway-request-scope.ts b/src/plugins/runtime/gateway-request-scope.ts index b893e4fd2911..d5ae5cd96a09 100644 --- a/src/plugins/runtime/gateway-request-scope.ts +++ b/src/plugins/runtime/gateway-request-scope.ts @@ -7,7 +7,7 @@ import type { import { resolveGlobalSingleton } from "../../shared/global-singleton.js"; import type { PluginOrigin } from "../plugin-origin.types.js"; -export type PluginRuntimeGatewayRequestScope = { +type PluginRuntimeGatewayRequestScope = { context?: GatewayRequestContext; client?: GatewayRequestOptions["client"]; isWebchatConnect: GatewayRequestOptions["isWebchatConnect"]; @@ -18,7 +18,7 @@ export type PluginRuntimeGatewayRequestScope = { gatewayMethodDispatchAllowed?: boolean; }; -export type PluginRuntimePluginScope = { +type PluginRuntimePluginScope = { pluginId: string; pluginSource?: string; pluginOrigin?: PluginOrigin; diff --git a/src/plugins/runtime/index.test.ts b/src/plugins/runtime/index.test.ts index e96463a4aead..c6f976840f6d 100644 --- a/src/plugins/runtime/index.test.ts +++ b/src/plugins/runtime/index.test.ts @@ -24,12 +24,9 @@ const runtimeModelAuthMocks = vi.hoisted(() => ({ vi.mock("./runtime-model-auth.runtime.js", () => runtimeModelAuthMocks); -import { - clearGatewaySubagentRuntime, - createPluginRuntime, - setGatewayNodesRuntime, - setGatewaySubagentRuntime, -} from "./index.js"; +import { setGatewayNodesRuntime, setGatewaySubagentRuntime } from "./gateway-bindings.js"; +import { clearGatewaySubagentRuntime } from "./gateway-bindings.test-fixtures.js"; +import { createPluginRuntime } from "./index.js"; function createCommandResult() { return { diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index 56f1011b3f98..5fadd4446155 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -35,13 +35,6 @@ import { createRuntimeTaskFlow } from "./runtime-taskflow.js"; import { createRuntimeTasks } from "./runtime-tasks.js"; import type { CreatePluginRuntimeOptions, PluginRuntime } from "./types.js"; -export type { CreatePluginRuntimeOptions } from "./types.js"; -export { - clearGatewaySubagentRuntime, - setGatewayNodesRuntime, - setGatewaySubagentRuntime, -} from "./gateway-bindings.js"; - const loadTtsRuntime = createLazyRuntimeModule(() => import("../../tts/tts.js")); const loadMediaUnderstandingRuntime = createLazyRuntimeModule( () => import("../../media-understanding/runtime.js"), @@ -264,6 +257,7 @@ function createRuntimeWorktrees(): PluginRuntime["worktrees"] { }; } +// Loaded by path from the plugin loader, so static export analysis cannot see this contract. export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}): PluginRuntime { const mediaUnderstanding = createRuntimeMediaUnderstandingFacade(); const taskFlow = createRuntimeTaskFlow(); diff --git a/src/plugins/runtime/load-context.ts b/src/plugins/runtime/load-context.ts index 99d3933fdfcb..9291a51e650a 100644 --- a/src/plugins/runtime/load-context.ts +++ b/src/plugins/runtime/load-context.ts @@ -125,7 +125,7 @@ export type PluginRuntimeLoadContext = { }; /** Runtime load option values that can be passed directly to plugin loading. */ -export type PluginRuntimeResolvedLoadValues = Pick< +type PluginRuntimeResolvedLoadValues = Pick< PluginLoadOptions, | "config" | "activationSourceConfig" @@ -138,7 +138,7 @@ export type PluginRuntimeResolvedLoadValues = Pick< >; /** Options accepted while resolving plugin runtime load context. */ -export type PluginRuntimeLoadContextOptions = { +type PluginRuntimeLoadContextOptions = { config?: OpenClawConfig; activationSourceConfig?: OpenClawConfig; env?: NodeJS.ProcessEnv; diff --git a/src/plugins/runtime/native-deps.ts b/src/plugins/runtime/native-deps.ts index 7e523065d37b..934c0d47be69 100644 --- a/src/plugins/runtime/native-deps.ts +++ b/src/plugins/runtime/native-deps.ts @@ -1,5 +1,5 @@ /** Inputs used to format native dependency install/rebuild guidance. */ -export type NativeDependencyHintParams = { +type NativeDependencyHintParams = { packageName: string; manager?: "pnpm" | "npm" | "yarn"; rebuildCommand?: string; diff --git a/src/plugins/runtime/runtime-plugin-boundary.ts b/src/plugins/runtime/runtime-plugin-boundary.ts index 4c6c74e7342a..b9fddbf40df7 100644 --- a/src/plugins/runtime/runtime-plugin-boundary.ts +++ b/src/plugins/runtime/runtime-plugin-boundary.ts @@ -27,28 +27,6 @@ function readPluginBoundaryConfigSafely() { return {}; } } - -export function resolvePluginRuntimeRecord( - pluginId: string, - onMissing?: () => never, -): PluginRuntimeRecord | null { - const manifestRegistry = loadPluginManifestRegistry({ - config: readPluginBoundaryConfigSafely(), - }); - const record = manifestRegistry.plugins.find((plugin) => plugin.id === pluginId); - if (!record?.source) { - if (onMissing) { - onMissing(); - } - return null; - } - return { - ...(record.origin ? { origin: record.origin } : {}), - rootDir: record.rootDir, - source: record.source, - }; -} - export function resolvePluginRuntimeRecordByEntryBaseNames( entryBaseNames: string[], onMissing?: () => never, diff --git a/src/plugins/runtime/runtime-taskflow.types.ts b/src/plugins/runtime/runtime-taskflow.types.ts index 6490d902aea5..c18f7e3061ce 100644 --- a/src/plugins/runtime/runtime-taskflow.types.ts +++ b/src/plugins/runtime/runtime-taskflow.types.ts @@ -16,7 +16,7 @@ export type ManagedTaskFlowRecord = TaskFlowRecord & { controllerId: string; }; -export type ManagedTaskFlowMutationErrorCode = +type ManagedTaskFlowMutationErrorCode = | "not_found" | "not_managed" | "revision_conflict" @@ -33,7 +33,7 @@ export type ManagedTaskFlowMutationResult = current?: TaskFlowRecord; }; -export type ManagedTaskFlowCreateParams = { +type ManagedTaskFlowCreateParams = { controllerId: string; goal: string; status?: ManagedTaskFlowRecord["status"]; @@ -47,7 +47,7 @@ export type ManagedTaskFlowCreateParams = { endedAt?: number | null; }; -export type BoundTaskFlowTaskRunResult = +type BoundTaskFlowTaskRunResult = | { created: true; flow: ManagedTaskFlowRecord; @@ -60,7 +60,7 @@ export type BoundTaskFlowTaskRunResult = flow?: TaskFlowRecord; }; -export type BoundTaskFlowCancelResult = { +type BoundTaskFlowCancelResult = { found: boolean; cancelled: boolean; reason?: string; diff --git a/src/plugins/runtime/runtime-tasks.types.ts b/src/plugins/runtime/runtime-tasks.types.ts index 1477f056eb73..6761b00b1eb2 100644 --- a/src/plugins/runtime/runtime-tasks.types.ts +++ b/src/plugins/runtime/runtime-tasks.types.ts @@ -11,14 +11,7 @@ import type { TaskRunDetail, TaskRunView, } from "./task-domain-types.js"; -export type { - TaskFlowDetail, - TaskFlowView, - TaskRunAggregateSummary, - TaskRunCancelResult, - TaskRunDetail, - TaskRunView, -} from "./task-domain-types.js"; +export type { TaskFlowDetail, TaskRunCancelResult } from "./task-domain-types.js"; export type { DetachedTaskLifecycleRuntime } from "../../tasks/detached-task-runtime-contract.js"; export type BoundTaskRunsRuntime = { diff --git a/src/plugins/runtime/standalone-runtime-registry-loader.test.ts b/src/plugins/runtime/standalone-runtime-registry-loader.test.ts index dd31d1f82a8e..f1ad60602539 100644 --- a/src/plugins/runtime/standalone-runtime-registry-loader.test.ts +++ b/src/plugins/runtime/standalone-runtime-registry-loader.test.ts @@ -1,6 +1,6 @@ // Standalone runtime registry loader tests cover registry loading outside gateway startup. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { clearPluginLoaderCache, testing } from "../loader.js"; +import { clearPluginLoaderCache } from "../loader.test-fixtures.js"; import { createEmptyPluginRegistry } from "../registry-empty.js"; import type { PluginRegistry } from "../registry-types.js"; import { @@ -45,83 +45,6 @@ afterEach(() => { resetPluginRuntimeStateForTest(); }); -describe("ensureStandaloneRuntimePluginRegistryLoaded", () => { - it("reuses a compatible gateway startup registry for gateway-bindable dispatch load options", () => { - const activeRegistry = createRegistryWithPlugin("telegram"); - activeRegistry.coreGatewayMethodNames = ["sessions.get", "sessions.list"]; - const config = { plugins: { allow: ["telegram"] } }; - const startupLoadOptions = { - config, - activationSourceConfig: config, - autoEnabledReasons: {}, - workspaceDir: "/tmp/ws", - onlyPluginIds: ["telegram"], - coreGatewayMethodNames: ["sessions.get", "sessions.list"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - preferBuiltPluginArtifacts: true, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupLoadOptions); - setActivePluginRegistry(activeRegistry, cacheKey, "gateway-bindable", "/tmp/ws"); - - const result = ensureStandaloneRuntimePluginRegistryLoaded({ - loadOptions: { - config, - onlyPluginIds: ["telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - workspaceDir: "/tmp/ws", - }, - }); - - expect(result).toBe(activeRegistry); - expect(loaderMocks.loadOpenClawPlugins).not.toHaveBeenCalled(); - }); - - it("loads a fresh registry when dispatch config is not startup-compatible", () => { - const activeRegistry = createRegistryWithPlugin("telegram"); - activeRegistry.coreGatewayMethodNames = ["sessions.get", "sessions.list"]; - const config = { plugins: { allow: ["telegram"] } }; - const startupLoadOptions = { - config, - activationSourceConfig: config, - autoEnabledReasons: {}, - workspaceDir: "/tmp/ws", - onlyPluginIds: ["telegram"], - coreGatewayMethodNames: ["sessions.get", "sessions.list"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - preferBuiltPluginArtifacts: true, - }; - const { cacheKey } = testing.resolvePluginLoadCacheContext(startupLoadOptions); - setActivePluginRegistry(activeRegistry, cacheKey, "gateway-bindable", "/tmp/ws"); - const loadedRegistry = createRegistryWithPlugin("telegram"); - loaderMocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry); - - const result = ensureStandaloneRuntimePluginRegistryLoaded({ - loadOptions: { - config: { - plugins: { - allow: ["telegram"], - load: { paths: ["/tmp/changed.js"] }, - }, - }, - onlyPluginIds: ["telegram"], - runtimeOptions: { - allowGatewaySubagentBinding: true, - }, - workspaceDir: "/tmp/ws", - }, - }); - - expect(result).toBe(loadedRegistry); - expect(loaderMocks.loadOpenClawPlugins).toHaveBeenCalledOnce(); - }); -}); - describe("ensureStandaloneRuntimePluginRegistryLoaded tool-discovery installs", () => { it("does not replace active or pinned channel registries during tool discovery", () => { const activeRegistry = createRegistryWithPlugin("provider-only"); diff --git a/src/plugins/runtime/types-channel.ts b/src/plugins/runtime/types-channel.ts index ebb859d326ee..c8c62d1f79d0 100644 --- a/src/plugins/runtime/types-channel.ts +++ b/src/plugins/runtime/types-channel.ts @@ -30,7 +30,7 @@ type RecordSessionMetaFromInbound = type UpdateLastRoute = import("../../config/sessions/runtime-types.js").UpdateLastRoute; type RecordInboundSession = import("../../channels/session.types.js").RecordInboundSession; -export type RuntimeThreadBindingLifecycleRecord = +type RuntimeThreadBindingLifecycleRecord = | import("../../infra/outbound/session-binding.types.js").SessionBindingRecord | { boundAt: number; @@ -39,13 +39,13 @@ export type RuntimeThreadBindingLifecycleRecord = maxAgeMs?: number; }; -export type PluginRuntimeChannelContextKey = { +type PluginRuntimeChannelContextKey = { channelId: string; accountId?: string | null; capability: string; }; -export type PluginRuntimeChannelContextEvent = { +type PluginRuntimeChannelContextEvent = { type: "registered" | "unregistered"; key: { channelId: string; @@ -55,7 +55,7 @@ export type PluginRuntimeChannelContextEvent = { context?: unknown; }; -export type PluginRuntimeChannelContextRegistry = { +type PluginRuntimeChannelContextRegistry = { register: ( params: PluginRuntimeChannelContextKey & { context: unknown; diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index a1f6e79d1908..db2c0a87ac60 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -10,16 +10,11 @@ import type { } from "../../plugin-sdk/tts-runtime.types.js"; import type { PluginRuntimeTaskFlows, PluginRuntimeTaskRuns } from "./runtime-tasks.types.js"; -export type { HeartbeatRunResult }; - -export type RuntimeRequestHeartbeatOptions = Parameters< +type RuntimeRequestHeartbeatOptions = Parameters< typeof import("../../infra/heartbeat-wake.js").requestHeartbeat >[0]; -export type RuntimeRequestHeartbeatNowOptions = Omit< - RuntimeRequestHeartbeatOptions, - "source" | "intent" -> & +type RuntimeRequestHeartbeatNowOptions = Omit & Partial>; type RuntimeWriteConfigOptions = { @@ -28,7 +23,7 @@ type RuntimeWriteConfigOptions = { unsetPaths?: string[][]; }; -export type DeepReadonly = T extends (...args: never[]) => unknown +type DeepReadonly = T extends (...args: never[]) => unknown ? T : T extends readonly (infer U)[] ? ReadonlyArray> diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index 43d8d51d1e29..8c234c3c5b54 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -24,7 +24,7 @@ export type SubagentRunParams = { cwd?: string; }; -export type PluginManagedWorktree = { +type PluginManagedWorktree = { id: string; path: string; branch: string; @@ -34,41 +34,41 @@ export type SubagentRunResult = { runId: string; }; -export type SubagentWaitParams = { +type SubagentWaitParams = { runId: string; timeoutMs?: number; }; -export type SubagentWaitResult = { +type SubagentWaitResult = { status: "ok" | "error" | "timeout"; error?: string; }; -export type SubagentGetSessionMessagesParams = { +type SubagentGetSessionMessagesParams = { sessionKey: string; limit?: number; }; -export type SubagentGetSessionMessagesResult = { +type SubagentGetSessionMessagesResult = { messages: unknown[]; }; /** @deprecated Use SubagentGetSessionMessagesParams. */ -export type SubagentGetSessionParams = SubagentGetSessionMessagesParams; +type SubagentGetSessionParams = SubagentGetSessionMessagesParams; /** @deprecated Use SubagentGetSessionMessagesResult. */ -export type SubagentGetSessionResult = SubagentGetSessionMessagesResult; +type SubagentGetSessionResult = SubagentGetSessionMessagesResult; -export type SubagentDeleteSessionParams = { +type SubagentDeleteSessionParams = { sessionKey: string; deleteTranscript?: boolean; }; -export type RuntimeNodeListParams = { +type RuntimeNodeListParams = { connected?: boolean; }; -export type RuntimeNodeListResult = { +type RuntimeNodeListResult = { nodes: Array<{ nodeId: string; displayName?: string; @@ -80,7 +80,7 @@ export type RuntimeNodeListResult = { }>; }; -export type RuntimeNodeInvokeParams = { +type RuntimeNodeInvokeParams = { nodeId: string; command: string; params?: unknown; diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index 8d5a5f30e37c..f0b823ab9b03 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -14,18 +14,9 @@ import { buildPluginLoaderAliasMap, createPluginLoaderModuleCacheKey, buildPluginLoaderJitiOptions, - isBundledPluginExtensionPath, - listPluginSdkAliasCandidates, - listPluginSdkExportedSubpaths, - normalizeJitiAliasTargetPath, - resolvePluginLoaderJitiFsCacheDir, - resolvePluginLoaderJitiFsCacheOption, resolvePluginLoaderModuleConfig, resolvePluginLoaderTryNative, - resolveExtensionApiAlias, - resolvePluginRuntimeModulePath, resolvePluginRuntimeModulePathWithDiagnostics, - resolvePluginSdkAliasFile, shouldPreferNativeModuleLoad, type PluginSdkResolutionPreference, } from "./sdk-alias.js"; @@ -136,6 +127,19 @@ function createPluginSdkAliasFixture(params?: { return { root, srcFile, distFile }; } +function writePluginSdkSubpathArtifacts(root: string, subpaths: readonly string[]) { + for (const subpath of subpaths) { + for (const candidate of [ + path.join(root, "src", "plugin-sdk", `${subpath}.ts`), + path.join(root, "dist", "plugin-sdk", `${subpath}.js`), + ]) { + if (!fs.existsSync(candidate)) { + fs.writeFileSync(candidate, "export {};\n", "utf-8"); + } + } + } +} + function createExtensionApiAliasFixture(params?: { srcBody?: string; distBody?: string; @@ -333,23 +337,6 @@ function createUserInstalledPluginSdkAliasFixture() { }; } -function resolvePluginSdkAlias(params: { - srcFile: string; - distFile: string; - modulePath: string; - argv1?: string; - env?: NodeJS.ProcessEnv; -}) { - const run = () => - resolvePluginSdkAliasFile({ - srcFile: params.srcFile, - distFile: params.distFile, - modulePath: params.modulePath, - argv1: params.argv1, - }); - return params.env ? withEnv(params.env, run) : run(); -} - function resolvePluginRuntimeModule(params: { modulePath: string; argv1?: string; @@ -358,12 +345,12 @@ function resolvePluginRuntimeModule(params: { pluginSdkResolution?: PluginSdkResolutionPreference; }) { const run = () => - resolvePluginRuntimeModulePath({ + resolvePluginRuntimeModulePathWithDiagnostics({ modulePath: params.modulePath, argv1: params.argv1, devSourceRoot: params.devSourceRoot, pluginSdkResolution: params.pluginSdkResolution, - }); + }).resolvedPath; return params.env ? withEnv(params.env, run) : run(); } @@ -409,29 +396,6 @@ function expectPluginSdkAliasTargets( } } -function expectPluginSdkAliasResolution(params: { - fixture: { root: string; srcFile: string; distFile: string }; - srcFile: string; - distFile: string; - modulePath: (root: string) => string; - argv1?: (root: string) => string; - env?: NodeJS.ProcessEnv; - expected: "src" | "dist"; -}) { - const resolved = resolvePluginSdkAlias({ - srcFile: params.srcFile, - distFile: params.distFile, - modulePath: params.modulePath(params.fixture.root), - argv1: params.argv1?.(params.fixture.root), - env: params.env, - }); - expectResolvedFixturePath({ - resolved, - fixture: params.fixture, - expected: params.expected, - }); -} - function expectExtensionApiAliasResolution(params: { fixture: { root: string; srcFile: string; distFile: string }; modulePath: (root: string) => string; @@ -439,11 +403,13 @@ function expectExtensionApiAliasResolution(params: { env?: NodeJS.ProcessEnv; expected: "src" | "dist"; }) { - const resolved = withEnv(params.env ?? {}, () => - resolveExtensionApiAlias({ - modulePath: params.modulePath(params.fixture.root), - argv1: params.argv1?.(params.fixture.root), - }), + const resolved = withEnv( + params.env ?? {}, + () => + buildPluginLoaderAliasMap( + params.modulePath(params.fixture.root), + params.argv1?.(params.fixture.root) ?? "", + )["openclaw/extension-api"] ?? null, ); expectResolvedFixturePath({ resolved, @@ -452,16 +418,34 @@ function expectExtensionApiAliasResolution(params: { }); } +function listPluginSdkExportedSubpaths(params: { + modulePath: string; + argv1?: string; + moduleUrl?: string; + devSourceRoot?: string | null; + pluginSdkResolution?: PluginSdkResolutionPreference; +}): string[] { + return Object.keys( + buildPluginLoaderAliasMap( + params.modulePath, + params.argv1 ?? "", + params.moduleUrl, + params.pluginSdkResolution, + params.devSourceRoot, + ), + ) + .filter((key) => key.startsWith("openclaw/plugin-sdk/")) + .map((key) => key.slice("openclaw/plugin-sdk/".length)) + .toSorted(); +} + function expectExportedSubpaths(params: { fixture: { root: string }; modulePath: string; expected: readonly string[]; cwd?: string; }) { - const run = () => - listPluginSdkExportedSubpaths({ - modulePath: params.modulePath, - }); + const run = () => listPluginSdkExportedSubpaths({ modulePath: params.modulePath }); const subpaths = params.cwd ? withCwd(params.cwd, run) : run(); expect(subpaths).toEqual(params.expected); } @@ -471,12 +455,13 @@ function expectCwdFallbackPluginSdkAliasResolution(params: { expected: "src" | "dist" | null; }) { const resolved = withCwd(params.fixture.root, () => - resolvePluginSdkAlias({ - srcFile: "channel-runtime.ts", - distFile: "channel-runtime.js", - modulePath: "/tmp/tsx-cache/openclaw-loader.js", - env: { NODE_ENV: undefined }, - }), + withEnv( + { NODE_ENV: undefined }, + () => + buildPluginLoaderAliasMap("/tmp/tsx-cache/openclaw-loader.js", "")[ + "openclaw/plugin-sdk/channel-runtime" + ] ?? null, + ), ); if (params.expected === null) { expect(resolved).toBeNull(); @@ -571,15 +556,24 @@ describe("plugin sdk alias helpers", () => { }, ])("$name", ({ buildFixture, modulePath, argv1, srcFile, distFile, env, expected }) => { const fixture = buildFixture(); - expectPluginSdkAliasResolution({ - fixture, - srcFile, - distFile, - modulePath, - argv1, - env, - expected, - }); + const sourceCandidate = path.join(fixture.root, "src", "plugin-sdk", srcFile); + const distCandidate = path.join(fixture.root, "dist", "plugin-sdk", distFile); + const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"); + const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs"); + if (fs.existsSync(sourceCandidate) && sourceCandidate !== sourceRootAlias) { + fs.copyFileSync(sourceCandidate, sourceRootAlias); + } + if (fs.existsSync(distCandidate) && distCandidate !== distRootAlias) { + fs.copyFileSync(distCandidate, distRootAlias); + } + const resolved = withEnv( + env ?? {}, + () => + buildPluginLoaderAliasMap(modulePath(fixture.root), argv1?.(fixture.root) ?? "")[ + "openclaw/plugin-sdk" + ] ?? null, + ); + expect(resolved).toBe(expected === "dist" ? distRootAlias : sourceRootAlias); }); it.each([ @@ -594,19 +588,11 @@ describe("plugin sdk alias helpers", () => { env: { NODE_ENV: undefined }, expected: "src" as const, }, - { - name: "resolves extension-api alias from package root when loader runs from transpiler cache path", - modulePath: () => "/tmp/tsx-cache/openclaw-loader.js", - argv1: (root: string) => path.join(root, "openclaw.mjs"), - env: { NODE_ENV: undefined }, - expected: "src" as const, - }, - ])("$name", ({ modulePath, argv1, env, expected }) => { + ])("$name", ({ modulePath, env, expected }) => { const fixture = createExtensionApiAliasFixture(); expectExtensionApiAliasResolution({ fixture, modulePath, - argv1, env, expected, }); @@ -654,16 +640,18 @@ describe("plugin sdk alias helpers", () => { }, ])("$name", ({ env, expectedFirst }) => { const fixture = createPluginSdkAliasFixture(); - const candidates = withEnv(env ?? {}, () => - listPluginSdkAliasCandidates({ - srcFile: "index.ts", - distFile: "index.js", - modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"), - }), + const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"); + const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs"); + fs.copyFileSync(fixture.srcFile, sourceRootAlias); + fs.copyFileSync(fixture.distFile, distRootAlias); + const resolved = withEnv( + env ?? {}, + () => + buildPluginLoaderAliasMap(path.join(fixture.root, "src", "plugins", "loader.ts"), "")[ + "openclaw/plugin-sdk" + ], ); - const first = expectedFirst === "dist" ? fixture.distFile : fixture.srcFile; - const second = expectedFirst === "dist" ? fixture.srcFile : fixture.distFile; - expect(candidates.indexOf(first)).toBeLessThan(candidates.indexOf(second)); + expect(resolved).toBe(expectedFirst === "dist" ? distRootAlias : sourceRootAlias); }); it("derives plugin-sdk subpaths from package exports", () => { @@ -677,6 +665,7 @@ describe("plugin sdk alias helpers", () => { "./plugin-sdk/.hidden": { default: "./dist/plugin-sdk/hidden.js" }, }, }); + writePluginSdkSubpathArtifacts(fixture.root, ["compat", "core"]); const subpaths = listPluginSdkExportedSubpaths({ modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"), }); @@ -689,6 +678,7 @@ describe("plugin sdk alias helpers", () => { "./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" }, }, }); + writePluginSdkSubpathArtifacts(fixture.root, ["core"]); fs.writeFileSync( path.join(fixture.root, "src", "plugin-sdk", "qa-channel.ts"), "export const qaChannel = true;\n", @@ -728,6 +718,7 @@ describe("plugin sdk alias helpers", () => { "./plugin-sdk/qa-runner-runtime": { default: "./dist/plugin-sdk/qa-runner-runtime.js" }, }, }); + writePluginSdkSubpathArtifacts(fixture.root, ["core"]); const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"); const sourceQaRunnerPath = path.join(fixture.root, "src", "plugin-sdk", "qa-runner-runtime.ts"); const distQaLiveTransportScenariosPath = path.join( @@ -782,6 +773,7 @@ describe("plugin sdk alias helpers", () => { "./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" }, }, }); + writePluginSdkSubpathArtifacts(fixture.root, ["core"]); fs.rmSync( path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"), { force: true }, @@ -881,6 +873,7 @@ describe("plugin sdk alias helpers", () => { "./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" }, }, }); + writePluginSdkSubpathArtifacts(fixture.root, ["core"]); fs.writeFileSync( path.join(fixture.root, "src", "plugin-sdk", "qa-channel.ts"), "export const qaChannel = true;\n", @@ -949,6 +942,7 @@ describe("plugin sdk alias helpers", () => { }, ] as const)("$name", ({ fixture: buildFixture, expected }) => { const fixture = buildFixture(); + writePluginSdkSubpathArtifacts(fixture.root, ["channel-runtime", "core"]); expectExportedSubpaths({ fixture, cwd: fixture.root, @@ -1254,6 +1248,7 @@ describe("plugin sdk alias helpers", () => { "./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" }, }, }); + writePluginSdkSubpathArtifacts(fixture.root, ["core"]); const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"); const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs"); const sourceSsrFInternalPath = path.join( @@ -2275,46 +2270,6 @@ describe("plugin sdk alias helpers", () => { ); }); - it("detects bundled plugin extension paths across source and dist roots", () => { - expect( - isBundledPluginExtensionPath({ - modulePath: "/repo/extensions/demo/api.js", - openClawPackageRoot: "/repo", - }), - ).toBe(true); - expect( - isBundledPluginExtensionPath({ - modulePath: "/repo/dist/extensions/demo/api.js", - openClawPackageRoot: "/repo", - }), - ).toBe(true); - expect( - isBundledPluginExtensionPath({ - modulePath: "/repo/vendor/demo/api.js", - openClawPackageRoot: "/repo", - }), - ).toBe(false); - }); - - it("normalizes Windows alias targets before handing them to the source transformer", () => { - const originalPlatform = process.platform; - Object.defineProperty(process, "platform", { - configurable: true, - value: "win32", - }); - - try { - expect(normalizeJitiAliasTargetPath(String.raw`C:\repo\dist\plugin-sdk\root-alias.cjs`)).toBe( - "C:/repo/dist/plugin-sdk/root-alias.cjs", - ); - } finally { - Object.defineProperty(process, "platform", { - configurable: true, - value: originalPlatform, - }); - } - }); - it("loads source runtime shims through the non-native module loading boundary", async () => { const copiedExtensionRoot = path.join(makeTempDir(), bundledPluginRoot("discord")); const copiedSourceDir = path.join(copiedExtensionRoot, "src"); @@ -2426,7 +2381,7 @@ export const syntheticRuntimeMarker = { fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8"); expect( - resolvePluginRuntimeModulePath({ + resolvePluginRuntimeModule({ modulePath: loaderCachePath, argv1: path.join(root, "bin", "openclaw"), pluginSdkResolution: "dist", @@ -2449,7 +2404,7 @@ export const syntheticRuntimeMarker = { process.argv[1] = path.join(root, "bin", "openclaw"); try { expect( - resolvePluginRuntimeModulePath({ + resolvePluginRuntimeModule({ modulePath: loaderCachePath, pluginSdkResolution: "dist", }), @@ -2478,7 +2433,7 @@ export const syntheticRuntimeMarker = { fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8"); expect( - resolvePluginRuntimeModulePath({ + resolvePluginRuntimeModule({ modulePath: loaderCachePath, argv1: path.join(root, "bin", "openclaw"), pluginSdkResolution: "dist", @@ -2503,7 +2458,7 @@ export const syntheticRuntimeMarker = { fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8"); expect( - resolvePluginRuntimeModulePath({ + resolvePluginRuntimeModule({ modulePath: loaderCachePath, argv1: shimFile, pluginSdkResolution: "dist", @@ -2528,7 +2483,7 @@ export const syntheticRuntimeMarker = { fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8"); expect( - resolvePluginRuntimeModulePath({ + resolvePluginRuntimeModule({ modulePath: loaderCachePath, argv1: binFile, pluginSdkResolution: "dist", @@ -2736,45 +2691,6 @@ describe("buildPluginLoaderAliasMap memoization", () => { }); describe("buildPluginLoaderJitiOptions", () => { - it("scopes jiti fs cache by OpenClaw package version and install metadata", () => { - const root = createTrustedOpenClawPackageFixture("1.2.3-beta.4"); - const tmpDir = path.join(root, "tmp"); - - const fsCache = withEnv({ TMPDIR: tmpDir }, () => - resolvePluginLoaderJitiFsCacheDir({ - modulePath: path.join(root, "dist", "plugins", "loader.js"), - }), - ); - - expect(fsCache).toContain(path.join(tmpDir, "jiti", "openclaw", "1.2.3-beta.4") + path.sep); - expect(path.basename(fsCache)).toMatch(/^\d+-\d+$/u); - }); - - it("preserves jiti's tmpdir guard when TMPDIR resolves to cwd", () => { - const root = createTrustedOpenClawPackageFixture("1.2.3-beta.4"); - - const guardedFsCache = withEnv({ TMPDIR: root, JITI_RESPECT_TMPDIR_ENV: undefined }, () => - withCwd(root, () => - resolvePluginLoaderJitiFsCacheDir({ - modulePath: path.join(root, "dist", "plugins", "loader.js"), - }), - ), - ); - const respectedFsCache = withEnv({ TMPDIR: root, JITI_RESPECT_TMPDIR_ENV: "1" }, () => - withCwd(root, () => - resolvePluginLoaderJitiFsCacheDir({ - modulePath: path.join(root, "dist", "plugins", "loader.js"), - }), - ), - ); - - expect(guardedFsCache).toContain(path.join("jiti", "openclaw", "1.2.3-beta.4") + path.sep); - expect(guardedFsCache.startsWith(path.join(root, "jiti") + path.sep)).toBe(false); - expect(respectedFsCache).toContain( - path.join(root, "jiti", "openclaw", "1.2.3-beta.4") + path.sep, - ); - }); - it("adds the versioned fs cache directory to plugin loader jiti options", () => { const root = createTrustedOpenClawPackageFixture("2.0.0"); const tmpDir = path.join(root, "tmp"); @@ -2789,25 +2705,6 @@ describe("buildPluginLoaderJitiOptions", () => { expect(options.fsCache).toContain(path.join(tmpDir, "jiti", "openclaw", "2.0.0")); }); - it("preserves jiti's fs cache environment opt-out", () => { - const root = createTrustedOpenClawPackageFixture("2.0.0"); - - const explicitOptOut = withEnv({ JITI_FS_CACHE: "false" }, () => - resolvePluginLoaderJitiFsCacheOption({ - modulePath: path.join(root, "dist", "plugins", "loader.js"), - }), - ); - const legacyOptOut = withEnv({ JITI_CACHE: "false", JITI_FS_CACHE: undefined }, () => - buildPluginLoaderJitiOptions( - { "openclaw/plugin-sdk": path.join(root, "dist", "plugin-sdk", "root-alias.cjs") }, - { modulePath: path.join(root, "dist", "plugins", "loader.js") }, - ), - ); - - expect(explicitOptOut).toBe(false); - expect(legacyOptOut.fsCache).toBe(false); - }); - it("pre-normalizes and marks alias maps for source transforms", () => { const marker = Symbol.for("pathe:normalizedAlias"); const aliasMap = { diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 6503338e3dca..e3d27d66d192 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -12,7 +12,7 @@ import { PluginLruCache } from "./plugin-cache-primitives.js"; type PluginSdkAliasCandidateKind = "dist" | "src"; export type PluginSdkResolutionPreference = "auto" | "dist" | "src"; -export type LoaderModuleResolveParams = { +type LoaderModuleResolveParams = { modulePath?: string; argv1?: string; cwd?: string; @@ -92,7 +92,7 @@ function resolvePluginLoaderJitiNativeModules(): string[] { } } -export function normalizeJitiAliasTargetPath(targetPath: string): string { +function normalizeJitiAliasTargetPath(targetPath: string): string { return process.platform === "win32" ? targetPath.replace(/\\/g, "/") : targetPath; } @@ -125,7 +125,7 @@ function resolveJitiCacheModulePath(params: LoaderModuleResolveParams = {}): str return resolveLoaderModulePath(params); } -export function resolvePluginLoaderJitiFsCacheDir(params: LoaderModuleResolveParams = {}): string { +function resolvePluginLoaderJitiFsCacheDir(params: LoaderModuleResolveParams = {}): string { const modulePath = resolveJitiCacheModulePath(params); const packageRoot = resolveLoaderPackageRoot({ ...params, modulePath }) ?? path.dirname(modulePath); @@ -149,7 +149,7 @@ export function resolvePluginLoaderJitiFsCacheDir(params: LoaderModuleResolvePar ); } -export function resolvePluginLoaderJitiFsCacheOption( +function resolvePluginLoaderJitiFsCacheOption( params: LoaderModuleResolveParams = {}, ): false | string { return shouldUseJitiFsCache() ? resolvePluginLoaderJitiFsCacheDir(params) : false; @@ -384,7 +384,7 @@ function resolveLoaderPluginSdkPackageRoot( ); } -export function resolvePluginSdkAliasCandidateOrder(params: { +function resolvePluginSdkAliasCandidateOrder(params: { modulePath: string; isProduction: boolean; pluginSdkResolution?: PluginSdkResolutionPreference; @@ -400,7 +400,7 @@ export function resolvePluginSdkAliasCandidateOrder(params: { return isDistRuntime || params.isProduction ? ["dist", "src"] : ["src", "dist"]; } -export function listPluginSdkAliasCandidates(params: { +function listPluginSdkAliasCandidates(params: { srcFile: string; distFile: string; modulePath: string; @@ -442,7 +442,7 @@ export function listPluginSdkAliasCandidates(params: { return candidates; } -export function resolvePluginSdkAliasFile(params: { +function resolvePluginSdkAliasFile(params: { srcFile: string; distFile: string; modulePath?: string; @@ -1544,7 +1544,7 @@ function listPrivateLocalOnlyPluginSdkSubpaths(params: { ); } -export function listPluginSdkExportedSubpaths( +function listPluginSdkExportedSubpaths( params: { modulePath?: string; argv1?: string; @@ -1588,7 +1588,7 @@ export function listPluginSdkExportedSubpaths( return subpaths; } -export function resolvePluginSdkScopedAliasMap( +function resolvePluginSdkScopedAliasMap( params: { modulePath?: string; argv1?: string; @@ -1671,7 +1671,7 @@ export function resolvePluginSdkScopedAliasMap( return aliasMap; } -export function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {}): string | null { +function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {}): string | null { try { const modulePath = resolveLoaderModulePath(params); const packageRoot = @@ -1944,12 +1944,6 @@ export function buildPluginLoaderAliasMap( return result; } -export function resolvePluginRuntimeModulePath( - params: LoaderModuleResolveParams = {}, -): string | null { - return resolvePluginRuntimeModulePathWithDiagnostics(params).resolvedPath; -} - export function resolvePluginRuntimeModulePathWithDiagnostics( params: LoaderModuleResolveParams = {}, ): PluginRuntimeModuleResolution { @@ -2125,21 +2119,3 @@ export function resolvePluginLoaderModuleConfig(params: { pluginLoaderModuleConfigCache.set(configCacheKey, result); return result; } - -export function isBundledPluginExtensionPath(params: { - modulePath: string; - openClawPackageRoot: string; - bundledPluginsDir?: string; -}): boolean { - const normalizedModulePath = path.resolve(params.modulePath); - const roots = [ - params.bundledPluginsDir ? path.resolve(params.bundledPluginsDir) : null, - path.join(params.openClawPackageRoot, "extensions"), - path.join(params.openClawPackageRoot, "dist", "extensions"), - path.join(params.openClawPackageRoot, "dist-runtime", "extensions"), - ].filter((root): root is string => typeof root === "string"); - return roots.some( - (root) => - normalizedModulePath === root || normalizedModulePath.startsWith(`${root}${path.sep}`), - ); -} diff --git a/src/plugins/session-entry-slot-keys.ts b/src/plugins/session-entry-slot-keys.ts index c544fea0e889..243bc478e282 100644 --- a/src/plugins/session-entry-slot-keys.ts +++ b/src/plugins/session-entry-slot-keys.ts @@ -150,13 +150,15 @@ type ReservedSessionEntrySlotKey = Extract< (typeof SESSION_ENTRY_RESERVED_SLOT_KEY_LIST)[number], keyof SessionEntry >; -type MissingSessionEntryReservedSlotKeys = Exclude; -type AssertNever = T; -/** Compile-time guard that every SessionEntry key is excluded from plugin extension slot names. */ -export type _AssertAllSessionEntryKeysAreReserved = - AssertNever; +type MissingSessionEntryReservedSlotKey = Exclude; +type SessionEntryReservedSlotSetValue = [MissingSessionEntryReservedSlotKey] extends [never] + ? string + : never; -const SESSION_ENTRY_RESERVED_SLOT_KEYS = new Set(SESSION_ENTRY_RESERVED_SLOT_KEY_LIST); +// Keep the value type impossible if a new SessionEntry field is missing from the reserved list. +const SESSION_ENTRY_RESERVED_SLOT_KEYS = new Set( + SESSION_ENTRY_RESERVED_SLOT_KEY_LIST, +); const OBJECT_PROTOTYPE_RESERVED_SLOT_KEYS = new Set([ "prototype", ...Object.getOwnPropertyNames(Object.prototype), diff --git a/src/plugins/setup-registry-loader-state.ts b/src/plugins/setup-registry-loader-state.ts new file mode 100644 index 000000000000..5d73a3d756fa --- /dev/null +++ b/src/plugins/setup-registry-loader-state.ts @@ -0,0 +1,10 @@ +/** Shared loader state for plugin setup registration and test fixtures. */ +import { + createPluginModuleLoaderCache, + type PluginModuleLoaderFactory, +} from "./plugin-module-loader-cache.js"; + +export const pluginSetupRegistryLoaderState = { + moduleLoaders: createPluginModuleLoaderCache(), + moduleLoaderFactory: undefined as PluginModuleLoaderFactory | undefined, +}; diff --git a/src/plugins/setup-registry.runtime.test.ts b/src/plugins/setup-registry.runtime.test.ts index c7711f479518..3a9a8e31e90d 100644 --- a/src/plugins/setup-registry.runtime.test.ts +++ b/src/plugins/setup-registry.runtime.test.ts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { clearCurrentPluginMetadataSnapshot, - resolvePluginMetadataControlPlaneFingerprint, setCurrentPluginMetadataSnapshot, } from "./current-plugin-metadata-snapshot.js"; import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; @@ -85,15 +84,7 @@ function createCurrentSnapshot(params: { }; return { policyHash, - configFingerprint: resolvePluginMetadataControlPlaneFingerprint( - {}, - { - env: process.env, - index, - policyHash, - workspaceDir: params.workspaceDir, - }, - ), + configFingerprint: params.manifestHash, workspaceDir: params.workspaceDir, index, plugins: [ @@ -148,9 +139,7 @@ describe("setup-registry descriptor lookup", () => { ], }); - const { testing, resolvePluginSetupCliBackendDescriptor } = - await import("./setup-registry.runtime.js"); - testing.resetDescriptorCache(); + const { resolvePluginSetupCliBackendDescriptor } = await import("./setup-registry.runtime.js"); expect(resolvePluginSetupCliBackendDescriptor({ backend: "codex-cli" })).toEqual({ pluginId: "openai", @@ -169,9 +158,7 @@ describe("setup-registry descriptor lookup", () => { }); it("refreshes cliBackends when the current metadata snapshot changes", async () => { - const { testing, resolvePluginSetupCliBackendDescriptor } = - await import("./setup-registry.runtime.js"); - testing.resetDescriptorCache(); + const { resolvePluginSetupCliBackendDescriptor } = await import("./setup-registry.runtime.js"); setCurrentPluginMetadataSnapshot( createCurrentSnapshot({ @@ -204,9 +191,7 @@ describe("setup-registry descriptor lookup", () => { }); it("uses workspace-scoped current metadata through the active plugin runtime", async () => { - const { testing, resolvePluginSetupCliBackendDescriptor } = - await import("./setup-registry.runtime.js"); - testing.resetDescriptorCache(); + const { resolvePluginSetupCliBackendDescriptor } = await import("./setup-registry.runtime.js"); setActivePluginRegistry( createEmptyPluginRegistry(), @@ -259,9 +244,7 @@ describe("setup-registry descriptor lookup", () => { plugins: [], }); - const { testing, resolvePluginSetupCliBackendDescriptor } = - await import("./setup-registry.runtime.js"); - testing.resetDescriptorCache(); + const { resolvePluginSetupCliBackendDescriptor } = await import("./setup-registry.runtime.js"); setCurrentPluginMetadataSnapshot( createCurrentSnapshot({ diff --git a/src/plugins/setup-registry.runtime.ts b/src/plugins/setup-registry.runtime.ts index 1a9f5409040f..6bc679a9d8b4 100644 --- a/src/plugins/setup-registry.runtime.ts +++ b/src/plugins/setup-registry.runtime.ts @@ -28,14 +28,6 @@ type SetupCliBackendDescriptorCache = { }; let cachedSetupCliBackendDescriptors: SetupCliBackendDescriptorCache | undefined; - -/** Test hook for resetting the descriptor cache. */ -export const testing = { - resetDescriptorCache(): void { - cachedSetupCliBackendDescriptors = undefined; - }, -}; - function resolveMetadataSnapshotForSetupCliBackends( params: Omit = {}, ): { diff --git a/src/plugins/setup-registry.test-fixtures.ts b/src/plugins/setup-registry.test-fixtures.ts new file mode 100644 index 000000000000..7b4cf31ef0dd --- /dev/null +++ b/src/plugins/setup-registry.test-fixtures.ts @@ -0,0 +1,15 @@ +/** Test-only controls for plugin setup registry loading. */ +import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js"; +import type { PluginModuleLoaderFactory } from "./plugin-module-loader-cache.js"; +import { pluginSetupRegistryLoaderState } from "./setup-registry-loader-state.js"; + +export function clearPluginSetupRegistryCache(): void { + clearPluginMetadataLifecycleCaches(); +} + +export function setPluginSetupRegistryModuleLoaderFactoryForTest( + factory: PluginModuleLoaderFactory | undefined, +): void { + pluginSetupRegistryLoaderState.moduleLoaderFactory = factory; + clearPluginSetupRegistryCache(); +} diff --git a/src/plugins/setup-registry.test.ts b/src/plugins/setup-registry.test.ts index d2f07f3b7c42..35c4d735202f 100644 --- a/src/plugins/setup-registry.test.ts +++ b/src/plugins/setup-registry.test.ts @@ -22,13 +22,13 @@ vi.mock("./native-module-require.js", () => ({ const tempDirs: string[] = []; const mocks = getRegistryJitiMocks(); -let clearPluginSetupRegistryCache: typeof import("./setup-registry.js").clearPluginSetupRegistryCache; +let clearPluginSetupRegistryCache: typeof import("./setup-registry.test-fixtures.js").clearPluginSetupRegistryCache; let resolvePluginSetupRegistry: typeof import("./setup-registry.js").resolvePluginSetupRegistry; let resolvePluginSetupProvider: typeof import("./setup-registry.js").resolvePluginSetupProvider; let resolvePluginSetupCliBackend: typeof import("./setup-registry.js").resolvePluginSetupCliBackend; let runPluginSetupConfigMigrations: typeof import("./setup-registry.js").runPluginSetupConfigMigrations; let setPluginSetupRegistryModuleLoaderFactoryForTest: - | typeof import("./setup-registry.js").setPluginSetupRegistryModuleLoaderFactoryForTest + | typeof import("./setup-registry.test-fixtures.js").setPluginSetupRegistryModuleLoaderFactoryForTest | undefined; function forceNodeRuntimeVersionsForTest(): () => void { @@ -217,8 +217,9 @@ describe("setup-registry module loader", () => { resetRegistryJitiMocks(); vi.resetModules(); const module = await import("./setup-registry.js"); - module.setPluginSetupRegistryModuleLoaderFactoryForTest(mocks.createJiti); - module.clearPluginSetupRegistryCache(); + const fixtures = await import("./setup-registry.test-fixtures.js"); + fixtures.setPluginSetupRegistryModuleLoaderFactoryForTest(mocks.createJiti); + fixtures.clearPluginSetupRegistryCache(); const pluginRoot = makeTempDir(); fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8"); mocks.loadPluginManifestRegistry.mockReturnValue({ @@ -245,20 +246,20 @@ describe("setup-registry module loader", () => { filename: mockArg(mocks.createJiti, 0, 0), options: requireRecord(mockArg(mocks.createJiti, 0, 1)), }; - module.setPluginSetupRegistryModuleLoaderFactoryForTest(undefined); + fixtures.setPluginSetupRegistryModuleLoaderFactoryForTest(undefined); }); beforeEach(async () => { resetRegistryJitiMocks(); vi.resetModules(); ({ - clearPluginSetupRegistryCache, resolvePluginSetupRegistry, resolvePluginSetupProvider, resolvePluginSetupCliBackend, runPluginSetupConfigMigrations, - setPluginSetupRegistryModuleLoaderFactoryForTest, } = await import("./setup-registry.js")); + ({ clearPluginSetupRegistryCache, setPluginSetupRegistryModuleLoaderFactoryForTest } = + await import("./setup-registry.test-fixtures.js")); setPluginSetupRegistryModuleLoaderFactoryForTest(mocks.createJiti); clearPluginSetupRegistryCache(); }); diff --git a/src/plugins/setup-registry.ts b/src/plugins/setup-registry.ts index 51af91e3b32d..21889d6e6f01 100644 --- a/src/plugins/setup-registry.ts +++ b/src/plugins/setup-registry.ts @@ -16,15 +16,11 @@ import { createPluginCacheKey, PluginLruCache } from "./plugin-cache-primitives. import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js"; import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js"; import { resolvePluginMetadataSnapshotMemoEnvFingerprint } from "./plugin-metadata-snapshot.js"; -import { - createPluginModuleLoaderCache, - getCachedPluginModuleLoader, - type PluginModuleLoaderFactory, - type PluginModuleLoaderCache, -} from "./plugin-module-loader-cache.js"; +import { getCachedPluginModuleLoader } from "./plugin-module-loader-cache.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; import type { PluginRuntime } from "./runtime/types.js"; import { listSetupCliBackendIds, listSetupProviderIds } from "./setup-descriptors.js"; +import { pluginSetupRegistryLoaderState } from "./setup-registry-loader-state.js"; import type { CliBackendPlugin, OpenClawPluginModule, @@ -60,7 +56,7 @@ type SetupAutoEnableProbeEntry = { probe: PluginSetupAutoEnableProbe; }; -export type PluginSetupRegistryDiagnosticCode = +type PluginSetupRegistryDiagnosticCode = | "setup-descriptor-runtime-disabled" | "setup-descriptor-provider-missing-runtime" | "setup-descriptor-provider-runtime-undeclared" @@ -97,9 +93,6 @@ const NOOP_LOGGER: PluginLogger = { error() {}, }; -const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); -let moduleLoaderFactoryForTest: PluginModuleLoaderFactory | undefined; - const MAX_SETUP_REGISTRY_CACHE_ENTRIES = 16; let setupRegistrySnapshotIdSeq = 0; let setupRegistrySnapshotIds = new WeakMap(); @@ -110,29 +103,22 @@ const pluginSetupRegistryCache = new PluginLruCache( MAX_SETUP_REGISTRY_CACHE_ENTRIES, ); -export function clearPluginSetupRegistryCache(): void { - moduleLoaders.clear(); +function clearPluginSetupRegistryCache(): void { + pluginSetupRegistryLoaderState.moduleLoaders.clear(); setupRegistrySnapshotIds = new WeakMap(); setupManifestRegistryCache.clear(); pluginSetupRegistryCache.clear(); } registerPluginMetadataProcessMemoLifecycleClear(clearPluginSetupRegistryCache); - -export function setPluginSetupRegistryModuleLoaderFactoryForTest( - factory: PluginModuleLoaderFactory | undefined, -): void { - moduleLoaderFactoryForTest = factory; - // Cached results were built through the previous factory; drop them too. - clearPluginSetupRegistryCache(); -} - function getModuleLoader(modulePath: string) { return getCachedPluginModuleLoader({ - cache: moduleLoaders, + cache: pluginSetupRegistryLoaderState.moduleLoaders, modulePath, importerUrl: import.meta.url, - ...(moduleLoaderFactoryForTest ? { createLoader: moduleLoaderFactoryForTest } : {}), + ...(pluginSetupRegistryLoaderState.moduleLoaderFactory + ? { createLoader: pluginSetupRegistryLoaderState.moduleLoaderFactory } + : {}), }); } diff --git a/src/plugins/slots.test.ts b/src/plugins/slots.test.ts index 42634ff75e20..4253d01af6ed 100644 --- a/src/plugins/slots.test.ts +++ b/src/plugins/slots.test.ts @@ -1,13 +1,7 @@ /** Tests plugin slot normalization and exclusive slot selection behavior. */ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { - applyExclusiveSlotSelection, - hasKind, - kindsEqual, - normalizeKinds, - slotKeysForPluginKind, -} from "./slots.js"; +import { applyExclusiveSlotSelection, hasKind, kindsEqual } from "./slots.js"; import type { PluginKind } from "./types.js"; describe("applyExclusiveSlotSelection", () => { @@ -265,20 +259,6 @@ describe("applyExclusiveSlotSelection", () => { }); }); -describe("normalizeKinds", () => { - it("returns empty array for undefined", () => { - expect(normalizeKinds(undefined)).toStrictEqual([]); - }); - - it("wraps a single kind in an array", () => { - expect(normalizeKinds("memory")).toEqual(["memory"]); - }); - - it("returns an array kind as-is", () => { - expect(normalizeKinds(["memory", "context-engine"])).toEqual(["memory", "context-engine"]); - }); -}); - describe("hasKind", () => { it("returns false for undefined kind", () => { expect(hasKind(undefined, "memory")).toBe(false); @@ -295,23 +275,6 @@ describe("hasKind", () => { }); }); -describe("slotKeysForPluginKind", () => { - it("returns empty for undefined", () => { - expect(slotKeysForPluginKind(undefined)).toStrictEqual([]); - }); - - it("returns single slot key for single kind", () => { - expect(slotKeysForPluginKind("memory")).toEqual(["memory"]); - }); - - it("returns multiple slot keys for multi-kind", () => { - expect(slotKeysForPluginKind(["memory", "context-engine"])).toEqual([ - "memory", - "contextEngine", - ]); - }); -}); - describe("kindsEqual", () => { it("treats undefined as equal to undefined", () => { expect(kindsEqual(undefined, undefined)).toBe(true); diff --git a/src/plugins/slots.ts b/src/plugins/slots.ts index 73b166874222..284664e64cc9 100644 --- a/src/plugins/slots.ts +++ b/src/plugins/slots.ts @@ -21,7 +21,7 @@ const DEFAULT_SLOT_BY_KEY: Record = { }; /** Normalize a kind field to an array for uniform iteration. */ -export function normalizeKinds(kind?: PluginKind | PluginKind[]): PluginKind[] { +function normalizeKinds(kind?: PluginKind | PluginKind[]): PluginKind[] { if (!kind) { return []; } @@ -47,7 +47,7 @@ export function kindsEqual( } /** Return all slot keys that a plugin's kind field maps to. */ -export function slotKeysForPluginKind(kind?: PluginKind | PluginKind[]): PluginSlotKey[] { +function slotKeysForPluginKind(kind?: PluginKind | PluginKind[]): PluginSlotKey[] { return normalizeKinds(kind) .map((k) => SLOT_BY_KIND[k]) .filter((k): k is PluginSlotKey => k != null); @@ -58,7 +58,7 @@ export function defaultSlotIdForKey(slotKey: PluginSlotKey): string { return DEFAULT_SLOT_BY_KEY[slotKey]; } -export type SlotSelectionResult = { +type SlotSelectionResult = { config: OpenClawConfig; warnings: string[]; changed: boolean; diff --git a/src/plugins/source-checkout-runtime.test.ts b/src/plugins/source-checkout-runtime.test.ts index c9df997db886..effde92cd2fe 100644 --- a/src/plugins/source-checkout-runtime.test.ts +++ b/src/plugins/source-checkout-runtime.test.ts @@ -1,18 +1,9 @@ /** Verifies source-checkout plugin runtime resolution and dependency diagnostics. */ import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { setBundledPluginsDirOverrideForTest } from "./bundled-dir.js"; +import { describe, expect, it } from "vitest"; import { loadOpenClawPlugins } from "./loader.js"; describe("source checkout bundled plugin runtime", () => { - beforeEach(() => { - setBundledPluginsDirOverrideForTest(path.join(process.cwd(), "extensions")); - }); - - afterEach(() => { - setBundledPluginsDirOverrideForTest(undefined); - }); - it("loads enabled bundled plugins from source checkout", () => { const registry = loadOpenClawPlugins({ cache: false, diff --git a/src/plugins/source-display.ts b/src/plugins/source-display.ts index 26a96371347d..c48f67bf6002 100644 --- a/src/plugins/source-display.ts +++ b/src/plugins/source-display.ts @@ -5,7 +5,6 @@ import { shortenHomeInString } from "../utils.js"; import type { PluginRecord } from "./registry.js"; import type { PluginSourceRoots } from "./roots.js"; export { resolvePluginSourceRoots } from "./roots.js"; -export type { PluginSourceRoots } from "./roots.js"; function tryRelative(root: string, filePath: string): string | null { if (!isPathInside(root, filePath)) { diff --git a/src/plugins/status-dependencies-core.ts b/src/plugins/status-dependencies-core.ts index 7f74ed243bc9..58f7ad64482c 100644 --- a/src/plugins/status-dependencies-core.ts +++ b/src/plugins/status-dependencies-core.ts @@ -6,7 +6,7 @@ import path from "node:path"; export type PluginDependencySpecMap = Record; /** Installation status for one plugin dependency. */ -export type PluginDependencyEntry = { +type PluginDependencyEntry = { name: string; spec: string; installed: boolean; diff --git a/src/plugins/status.test-fixtures.ts b/src/plugins/status.test-fixtures.ts new file mode 100644 index 000000000000..b9449e26d5b1 --- /dev/null +++ b/src/plugins/status.test-fixtures.ts @@ -0,0 +1,117 @@ +/** Test-only plugin status and registry fixture builders. */ +import { createEmptyPluginRegistry } from "./registry-empty.js"; +import type { PluginRecord, PluginRegistry } from "./registry.js"; +import type { PluginCompatibilityNotice } from "./status.js"; +import { createPluginRecord } from "./status.test-helpers.js"; +import type { PluginHookName } from "./types.js"; + +export { createPluginRecord }; + +export const LEGACY_BEFORE_AGENT_START_MESSAGE = + "still uses legacy before_agent_start; keep regression coverage on this plugin, and prefer before_model_resolve/before_prompt_build for new work."; +export const HOOK_ONLY_MESSAGE = + "is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet."; +export const DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE = + "uses deprecated memory-specific embedding provider API; use api.registerEmbeddingProvider and contracts.embeddingProviders for new embedding providers."; +export const REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE = + "references removed session/transcript file APIs; migrate to session identity, SessionTranscriptUpdate.target, and Gateway/runtime session helpers."; + +export function createCompatibilityNotice( + params: Pick, +): PluginCompatibilityNotice { + switch (params.code) { + case "legacy-before-agent-start": + return { + pluginId: params.pluginId, + code: params.code, + compatCode: "legacy-before-agent-start", + severity: "warn", + message: LEGACY_BEFORE_AGENT_START_MESSAGE, + }; + case "hook-only": + return { + pluginId: params.pluginId, + code: params.code, + compatCode: "hook-only-plugin-shape", + severity: "info", + message: HOOK_ONLY_MESSAGE, + }; + case "deprecated-memory-embedding-provider-api": + return { + pluginId: params.pluginId, + code: params.code, + compatCode: "deprecated-memory-embedding-provider-api", + severity: "warn", + message: DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE, + }; + case "removed-session-transcript-file-api": + return { + pluginId: params.pluginId, + code: params.code, + compatCode: "removed-session-transcript-file-api", + severity: "warn", + message: REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE, + }; + } + throw new Error("Unsupported compatibility notice code"); +} + +export function createBundledPluginRecord(id: string): PluginRecord { + return createPluginRecord({ + id, + source: `bundled:${id}`, + rootDir: `/bundled/${id}`, + origin: "bundled", + }); +} + +export function createTypedHook(params: { + pluginId: string; + hookName: PluginHookName; + source?: string; +}): PluginRegistry["typedHooks"][number] { + return { + pluginId: params.pluginId, + hookName: params.hookName, + handler: () => undefined, + source: params.source ?? `/tmp/${params.pluginId}/index.ts`, + }; +} + +export function createCustomHook(params: { + pluginId: string; + events: string[]; + name?: string; +}): PluginRegistry["hooks"][number] { + const source = `/tmp/${params.pluginId}/handler.ts`; + return { + pluginId: params.pluginId, + events: params.events, + source, + entry: { + hook: { + name: params.name ?? "legacy", + description: "", + source: "openclaw-plugin", + pluginId: params.pluginId, + filePath: `/tmp/${params.pluginId}/HOOK.md`, + baseDir: `/tmp/${params.pluginId}`, + handlerPath: source, + }, + frontmatter: {}, + }, + }; +} + +export function createPluginLoadResult( + overrides: Partial & Pick = { plugins: [] }, +): PluginRegistry { + const registry = createEmptyPluginRegistry(); + for (const key of Object.keys(overrides) as Array) { + const value = overrides[key]; + if (value !== undefined) { + Object.assign(registry, { [key]: value }); + } + } + return registry; +} diff --git a/src/plugins/status.test-helpers.ts b/src/plugins/status.test-helpers.ts index f267fbb16837..baa5db0b4710 100644 --- a/src/plugins/status.test-helpers.ts +++ b/src/plugins/status.test-helpers.ts @@ -1,61 +1,5 @@ /** Shared helpers for plugin status tests and installed-index fixture setup. */ -import type { PluginLoadResult } from "./loader.js"; -import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRecord } from "./registry.js"; -import type { PluginCompatibilityNotice } from "./status.js"; -import type { PluginHookName } from "./types.js"; - -export const LEGACY_BEFORE_AGENT_START_MESSAGE = - "still uses legacy before_agent_start; keep regression coverage on this plugin, and prefer before_model_resolve/before_prompt_build for new work."; -export const HOOK_ONLY_MESSAGE = - "is hook-only. This remains a supported compatibility path, but it has not migrated to explicit capability registration yet."; -export const DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE = - "uses deprecated memory-specific embedding provider API; use api.registerEmbeddingProvider and contracts.embeddingProviders for new embedding providers."; -export const REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE = - "references removed session/transcript file APIs; migrate to session identity, SessionTranscriptUpdate.target, and Gateway/runtime session helpers."; - -export function createCompatibilityNotice( - params: Pick, -): PluginCompatibilityNotice { - switch (params.code) { - case "legacy-before-agent-start": - return { - pluginId: params.pluginId, - code: params.code, - compatCode: "legacy-before-agent-start", - severity: "warn", - message: LEGACY_BEFORE_AGENT_START_MESSAGE, - }; - case "hook-only": - return { - pluginId: params.pluginId, - code: params.code, - compatCode: "hook-only-plugin-shape", - severity: "info", - message: HOOK_ONLY_MESSAGE, - }; - case "deprecated-memory-embedding-provider-api": - return { - pluginId: params.pluginId, - code: params.code, - compatCode: "deprecated-memory-embedding-provider-api", - severity: "warn", - message: DEPRECATED_MEMORY_EMBEDDING_PROVIDER_API_MESSAGE, - }; - case "removed-session-transcript-file-api": - return { - pluginId: params.pluginId, - code: params.code, - compatCode: "removed-session-transcript-file-api", - severity: "warn", - message: REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE, - }; - } - const unsupportedCode: never = params.code; - void unsupportedCode; - throw new Error("unsupported compatibility notice code"); -} - export function createPluginRecord( overrides: Partial & Pick, ): PluginRecord { @@ -103,63 +47,3 @@ export function createPluginRecord( ...rest, }; } - -export function createBundledPluginRecord(id: string): PluginRecord { - return createPluginRecord({ - id, - source: `bundled:${id}`, - rootDir: `/bundled/${id}`, - origin: "bundled", - }); -} - -export function createTypedHook(params: { - pluginId: string; - hookName: PluginHookName; - source?: string; -}): PluginLoadResult["typedHooks"][number] { - return { - pluginId: params.pluginId, - hookName: params.hookName, - handler: () => undefined, - source: params.source ?? `/tmp/${params.pluginId}/index.ts`, - }; -} - -export function createCustomHook(params: { - pluginId: string; - events: string[]; - name?: string; -}): PluginLoadResult["hooks"][number] { - const source = `/tmp/${params.pluginId}/handler.ts`; - return { - pluginId: params.pluginId, - events: params.events, - source, - entry: { - hook: { - name: params.name ?? "legacy", - description: "", - source: "openclaw-plugin", - pluginId: params.pluginId, - filePath: `/tmp/${params.pluginId}/HOOK.md`, - baseDir: `/tmp/${params.pluginId}`, - handlerPath: source, - }, - frontmatter: {}, - }, - }; -} - -export function createPluginLoadResult( - overrides: Partial & Pick = { plugins: [] }, -): PluginLoadResult { - const registry = createEmptyPluginRegistry(); - for (const key of Object.keys(overrides) as Array) { - const value = overrides[key]; - if (value !== undefined) { - Object.assign(registry, { [key]: value }); - } - } - return registry; -} diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index 2a0cfa877998..ec83084f232d 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -2,7 +2,7 @@ import { expectDefined } from "@openclaw/normalization-core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { PluginMemoryEmbeddingProviderRegistration } from "./registry-types.js"; +import type { PluginMemoryEmbeddingProviderRegistration } from "./registry.test-fixtures.js"; import { createCompatibilityNotice, createCustomHook, @@ -13,7 +13,7 @@ import { HOOK_ONLY_MESSAGE, LEGACY_BEFORE_AGENT_START_MESSAGE, REMOVED_SESSION_TRANSCRIPT_FILE_API_MESSAGE, -} from "./status.test-helpers.js"; +} from "./status.test-fixtures.js"; const loadConfigMock = vi.fn(); const loadOpenClawPluginsMock = vi.fn(); diff --git a/src/plugins/tool-descriptor-cache.test.ts b/src/plugins/tool-descriptor-cache.test.ts index 54a828edde4a..90e77b676c0f 100644 --- a/src/plugins/tool-descriptor-cache.test.ts +++ b/src/plugins/tool-descriptor-cache.test.ts @@ -19,13 +19,13 @@ import { buildPluginToolDescriptorCacheKey, capturePluginToolDescriptor, createPluginToolDescriptorConfigCacheKeyMemo, - resetPluginToolDescriptorCache, } from "./tool-descriptor-cache.js"; +import { resetPluginToolDescriptorCacheForTest } from "./tools.test-fixtures.js"; describe("plugin tool descriptor cache keys", () => { afterEach(() => { hoisted.resolveRuntimeConfigCacheKey.mockClear(); - resetPluginToolDescriptorCache(); + resetPluginToolDescriptorCacheForTest(); }); it("memoizes config cache keys across plugin descriptor keys in one resolution pass", () => { diff --git a/src/plugins/tool-descriptor-cache.ts b/src/plugins/tool-descriptor-cache.ts index 2768c1cbd84d..31ef008ff2d4 100644 --- a/src/plugins/tool-descriptor-cache.ts +++ b/src/plugins/tool-descriptor-cache.ts @@ -4,6 +4,7 @@ import type { AnyAgentTool } from "../agents/tools/common.js"; import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js"; import type { JsonObject, ToolDescriptor } from "../tools/types.js"; import type { PluginLoadOptions } from "./loader.js"; +import type { PluginRegistry } from "./registry-types.js"; import type { OpenClawPluginToolContext } from "./types.js"; const PLUGIN_TOOL_DESCRIPTOR_CACHE_VERSION = 3; @@ -17,9 +18,12 @@ export type CachedPluginToolDescriptor = { optional: boolean; }; -const descriptorCache = new Map(); -let descriptorCacheObjectIds = new WeakMap(); -let nextDescriptorCacheObjectId = 1; +export const pluginToolDescriptorCacheState = { + descriptors: new Map(), + objectIds: new WeakMap(), + nextObjectId: 1, + runtimeRegistries: new WeakMap(), +}; export type PluginToolDescriptorConfigCacheKeyMemo = WeakMap; @@ -28,12 +32,6 @@ export function createPluginToolDescriptorConfigCacheKeyMemo(): PluginToolDescri return new WeakMap(); } -export function resetPluginToolDescriptorCache(): void { - descriptorCache.clear(); - descriptorCacheObjectIds = new WeakMap(); - nextDescriptorCacheObjectId = 1; -} - function sourceFingerprint(source: string): string { try { const stat = fs.statSync(source); @@ -47,12 +45,12 @@ function getDescriptorCacheObjectId(value: object | null | undefined): number | if (!value) { return null; } - const existing = descriptorCacheObjectIds.get(value); + const existing = pluginToolDescriptorCacheState.objectIds.get(value); if (existing !== undefined) { return existing; } - const next = nextDescriptorCacheObjectId++; - descriptorCacheObjectIds.set(value, next); + const next = pluginToolDescriptorCacheState.nextObjectId++; + pluginToolDescriptorCacheState.objectIds.set(value, next); return next; } @@ -174,7 +172,7 @@ export function capturePluginToolDescriptor(params: { export function readCachedPluginToolDescriptors( cacheKey: string, ): readonly CachedPluginToolDescriptor[] | undefined { - return descriptorCache.get(cacheKey); + return pluginToolDescriptorCacheState.descriptors.get(cacheKey); } export function writeCachedPluginToolDescriptors(params: { @@ -182,13 +180,13 @@ export function writeCachedPluginToolDescriptors(params: { descriptors: readonly CachedPluginToolDescriptor[]; }): void { if ( - !descriptorCache.has(params.cacheKey) && - descriptorCache.size >= PLUGIN_TOOL_DESCRIPTOR_CACHE_LIMIT + !pluginToolDescriptorCacheState.descriptors.has(params.cacheKey) && + pluginToolDescriptorCacheState.descriptors.size >= PLUGIN_TOOL_DESCRIPTOR_CACHE_LIMIT ) { - const oldestKey = descriptorCache.keys().next().value; + const oldestKey = pluginToolDescriptorCacheState.descriptors.keys().next().value; if (oldestKey !== undefined) { - descriptorCache.delete(oldestKey); + pluginToolDescriptorCacheState.descriptors.delete(oldestKey); } } - descriptorCache.set(params.cacheKey, [...params.descriptors]); + pluginToolDescriptorCacheState.descriptors.set(params.cacheKey, [...params.descriptors]); } diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 20ee2f8fa0ca..29a95cca7c68 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -37,7 +37,7 @@ let resolvePluginTools: typeof import("./tools.js").resolvePluginTools; let ensureStandalonePluginToolRegistryLoaded: typeof import("./tools.js").ensureStandalonePluginToolRegistryLoaded; let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey; let getPluginToolMeta: typeof import("./tools.js").getPluginToolMeta; -let resetPluginToolFactoryCache: typeof import("./tools.js").resetPluginToolFactoryCache; +let resetPluginToolDescriptorCacheForTest: typeof import("./tools.test-fixtures.js").resetPluginToolDescriptorCacheForTest; let getActivePluginRegistry: typeof import("./runtime.js").getActivePluginRegistry; let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry; let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest; @@ -482,7 +482,6 @@ describe("resolvePluginTools optional tools", () => { buildPluginToolMetadataKey, ensureStandalonePluginToolRegistryLoaded, getPluginToolMeta, - resetPluginToolFactoryCache, resolvePluginTools, } = await import("./tools.js")); ({ @@ -495,6 +494,7 @@ describe("resolvePluginTools optional tools", () => { await import("./runtime/gateway-request-scope.js")); ({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } = await import("./current-plugin-metadata-snapshot.js")); + ({ resetPluginToolDescriptorCacheForTest } = await import("./tools.test-fixtures.js")); }); beforeEach(() => { @@ -510,13 +510,13 @@ describe("resolvePluginTools optional tools", () => { })); resetPluginRuntimeStateForTest?.(); clearCurrentPluginMetadataSnapshot?.(); - resetPluginToolFactoryCache?.(); + resetPluginToolDescriptorCacheForTest?.(); }); afterEach(() => { resetPluginRuntimeStateForTest?.(); clearCurrentPluginMetadataSnapshot?.(); - resetPluginToolFactoryCache?.(); + resetPluginToolDescriptorCacheForTest?.(); setLoggerOverride(null); loggingState.rawConsole = null; resetLogger(); diff --git a/src/plugins/tools.test-fixtures.ts b/src/plugins/tools.test-fixtures.ts new file mode 100644 index 000000000000..ed0d7d157a07 --- /dev/null +++ b/src/plugins/tools.test-fixtures.ts @@ -0,0 +1,8 @@ +import { pluginToolDescriptorCacheState } from "./tool-descriptor-cache.js"; + +export function resetPluginToolDescriptorCacheForTest(): void { + pluginToolDescriptorCacheState.descriptors.clear(); + pluginToolDescriptorCacheState.objectIds = new WeakMap(); + pluginToolDescriptorCacheState.nextObjectId = 1; + pluginToolDescriptorCacheState.runtimeRegistries = new WeakMap(); +} diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 37e7b5257f02..f245cebff9da 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -36,23 +36,14 @@ import { buildPluginToolDescriptorCacheKey, capturePluginToolDescriptor, createPluginToolDescriptorConfigCacheKeyMemo, + pluginToolDescriptorCacheState, readCachedPluginToolDescriptors, - resetPluginToolDescriptorCache as resetCachedPluginToolDescriptors, type CachedPluginToolDescriptor, type PluginToolDescriptorConfigCacheKeyMemo, writeCachedPluginToolDescriptors, } from "./tool-descriptor-cache.js"; import type { OpenClawPluginToolContext } from "./types.js"; -let cachedDescriptorRuntimeRegistries = new WeakMap(); - -export function resetPluginToolDescriptorCache(): void { - resetCachedPluginToolDescriptors(); - cachedDescriptorRuntimeRegistries = new WeakMap(); -} - -export { resetPluginToolDescriptorCache as resetPluginToolFactoryCache }; - /** MCP bridge metadata attached to plugin tools surfaced through agent tool lists. */ export type PluginToolMcpMeta = { serverName: string; @@ -62,7 +53,7 @@ export type PluginToolMcpMeta = { }; /** Runtime metadata used to trace an agent tool back to its owning plugin registration. */ -export type PluginToolMeta = { +type PluginToolMeta = { pluginId: string; optional: boolean; replaySafe?: boolean; @@ -763,9 +754,9 @@ function createCachedDescriptorPluginTool(params: { const registry = resolvePluginToolRegistry({ loadOptions, onlyPluginIds: [pluginId], - retainedRegistry: cachedDescriptorRuntimeRegistries.get(params.descriptor), + retainedRegistry: pluginToolDescriptorCacheState.runtimeRegistries.get(params.descriptor), onRetainRegistry: (retainedRegistry) => { - cachedDescriptorRuntimeRegistries.set(params.descriptor, retainedRegistry); + pluginToolDescriptorCacheState.runtimeRegistries.set(params.descriptor, retainedRegistry); }, }); const candidates = registry?.tools.filter((candidate) => candidate.pluginId === pluginId); diff --git a/src/plugins/trusted-tool-policy.ts b/src/plugins/trusted-tool-policy.ts index afc64166f689..a6029fb4f41b 100644 --- a/src/plugins/trusted-tool-policy.ts +++ b/src/plugins/trusted-tool-policy.ts @@ -24,7 +24,7 @@ type TrustedToolPolicyRegistry = | undefined; /** Diagnostic entry for an installed trusted tool policy. */ -export type TrustedToolPolicyDiagnosticEntry = { +type TrustedToolPolicyDiagnosticEntry = { id: string; pluginId: string; pluginName?: string; diff --git a/src/plugins/uninstall.test.ts b/src/plugins/uninstall.test.ts index 7467380ff9bc..492d2cbdddc1 100644 --- a/src/plugins/uninstall.test.ts +++ b/src/plugins/uninstall.test.ts @@ -1,6 +1,5 @@ // Covers plugin uninstall flows and install-record cleanup. import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; @@ -16,8 +15,6 @@ import { removePluginFromConfig, planPluginUninstall, resolveUninstallChannelConfigKeys, - resolveUninstallDirectoryTarget, - uninstallPlugin, } from "./uninstall.js"; const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn()); @@ -29,6 +26,21 @@ vi.mock("../process/exec.js", () => ({ type PluginConfig = NonNullable; type PluginInstallRecord = NonNullable[string]; +async function uninstallPlugin(params: Parameters[0]) { + const plan = planPluginUninstall(params); + if (!plan.ok) { + return plan; + } + const directory = await applyPluginUninstallDirectoryRemoval(plan.directoryRemoval); + return { + ok: true as const, + config: plan.config, + pluginId: plan.pluginId, + actions: { ...plan.actions, directory: directory.directoryRemoved }, + warnings: directory.warnings, + }; +} + async function createInstalledNpmPluginFixture(params: { baseDir: string; pluginId?: string; @@ -1729,219 +1741,3 @@ describe("uninstallPlugin", () => { expect(linkStat.isSymbolicLink()).toBe(true); }); }); - -describe("resolveUninstallDirectoryTarget", () => { - it("returns null for linked plugins", () => { - expect( - resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: { - source: "path", - sourcePath: "/tmp/my-plugin", - installPath: "/tmp/my-plugin", - }, - }), - ).toBeNull(); - }); - - it("returns managed install path for copied path installs", () => { - const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const installPath = resolvePluginInstallDir("my-plugin", extensionsDir); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: { - source: "path", - sourcePath: "/tmp/source-plugin", - installPath, - }, - extensionsDir, - }), - ).toBe(installPath); - }); - - it("falls back to default path when configured installPath is untrusted", () => { - const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const target = resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: { - source: "npm", - spec: "my-plugin@1.0.0", - installPath: "/tmp/not-openclaw-plugin-install/my-plugin", - }, - extensionsDir, - }); - - expect(target).toBe(resolvePluginInstallDir("my-plugin", extensionsDir)); - }); - - it("uses configured installPath when it stays inside the managed extensions dir", () => { - const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const installPath = path.join(extensionsDir, "archive-installs", "my-plugin"); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: { - source: "archive", - sourcePath: "/tmp/my-plugin.zip", - installPath, - }, - extensionsDir, - }), - ).toBe(installPath); - }); - - it("uses configured installPath when npm installed it under the managed npm root", () => { - const stateDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const extensionsDir = path.join(stateDir, "extensions"); - const installPath = path.join(stateDir, "npm", "node_modules", "@openclaw", "kitchen-sink"); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "openclaw-kitchen-sink-fixture", - hasInstall: true, - installRecord: { - source: "npm", - spec: "@openclaw/kitchen-sink@latest", - installPath, - }, - extensionsDir, - }), - ).toBe(installPath); - }); - - it("uses configured installPath when npm installed it under a managed npm project", () => { - const stateDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const extensionsDir = path.join(stateDir, "extensions"); - const npmRoot = resolvePluginNpmProjectDir({ - npmDir: path.join(stateDir, "npm"), - packageName: "@openclaw/kitchen-sink", - }); - const installPath = path.join(npmRoot, "node_modules", "@openclaw", "kitchen-sink"); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "openclaw-kitchen-sink-fixture", - hasInstall: true, - installRecord: { - source: "npm", - spec: "@openclaw/kitchen-sink@latest", - installPath, - }, - extensionsDir, - }), - ).toBe(installPath); - }); - - it("uses configured installPath when git installed it under the managed git root", () => { - const stateDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const extensionsDir = path.join(stateDir, "extensions"); - const installPath = path.join(stateDir, "git", "git-abc123", "repo"); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: createGitInstallRecord("my-plugin", installPath), - extensionsDir, - }), - ).toBe(installPath); - }); - - it("uses configured installPath when ClawHub installed it under the managed extensions root", () => { - const stateDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const extensionsDir = path.join(stateDir, "extensions"); - const installPath = resolvePluginInstallDir("clawpack-demo", extensionsDir); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "clawpack-demo", - hasInstall: true, - installRecord: { - source: "clawhub", - spec: "clawhub:clawpack-demo@2026.5.1-beta.2", - installPath, - clawhubUrl: "https://clawhub.ai", - clawhubPackage: "clawpack-demo", - clawhubFamily: "code-plugin", - clawhubChannel: "official", - artifactKind: "npm-pack", - artifactFormat: "tgz", - npmIntegrity: "sha512-clawpack", - npmShasum: "1".repeat(40), - npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz", - clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - clawpackSpecVersion: 1, - clawpackManifestSha256: - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - clawpackSize: 4096, - }, - extensionsDir, - }), - ).toBe(installPath); - }); - - it("does not trust git install paths outside the managed git root", () => { - const stateDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const extensionsDir = path.join(stateDir, "extensions"); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: createGitInstallRecord( - "my-plugin", - path.join(os.tmpdir(), "git", "git-abc123", "repo"), - ), - extensionsDir, - }), - ).toBe(resolvePluginInstallDir("my-plugin", extensionsDir)); - }); - - it("does not trust npm install paths outside the managed npm root", () => { - const stateDir = path.join(os.tmpdir(), "openclaw-uninstall-safe"); - const extensionsDir = path.join(stateDir, "extensions"); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "openclaw-kitchen-sink-fixture", - hasInstall: true, - installRecord: { - source: "npm", - spec: "@openclaw/kitchen-sink@latest", - installPath: path.join(os.tmpdir(), "npm", "node_modules", "@openclaw", "kitchen-sink"), - }, - extensionsDir, - }), - ).toBe(resolvePluginInstallDir("openclaw-kitchen-sink-fixture", extensionsDir)); - }); - - it("uses configured installPath when it is under the recorded managed extensions root", () => { - const currentExtensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-current", "extensions"); - const recordedExtensionsDir = path.join( - os.tmpdir(), - "openclaw-uninstall-recorded", - "extensions", - ); - const installPath = resolvePluginInstallDir("my-plugin", recordedExtensionsDir); - - expect( - resolveUninstallDirectoryTarget({ - pluginId: "my-plugin", - hasInstall: true, - installRecord: { - source: "npm", - spec: "my-plugin@1.0.0", - installPath, - }, - extensionsDir: currentExtensionsDir, - }), - ).toBe(installPath); - }); -}); diff --git a/src/plugins/uninstall.ts b/src/plugins/uninstall.ts index abd5412578aa..39dc0c832a3d 100644 --- a/src/plugins/uninstall.ts +++ b/src/plugins/uninstall.ts @@ -20,7 +20,7 @@ import { import { relinkOpenClawPeerDependenciesInManagedNpmRoot } from "./plugin-peer-link.js"; import { defaultSlotIdForKey } from "./slots.js"; -export type UninstallActions = { +type UninstallActions = { entry: boolean; install: boolean; allowlist: boolean; @@ -56,9 +56,7 @@ const UNINSTALL_ACTION_ORDER = [ "directory", ] as const satisfies ReadonlyArray; -export function createEmptyUninstallActions( - overrides: Partial = {}, -): UninstallActions { +function createEmptyUninstallActions(overrides: Partial = {}): UninstallActions { return { entry: false, install: false, @@ -93,16 +91,6 @@ export function formatUninstallSlotResetPreview(slotKey: "memory" | "contextEngi return `${UNINSTALL_ACTION_LABELS[actionKey]} (will reset to "${defaultSlotIdForKey(slotKey)}")`; } -export type UninstallPluginResult = - | { - ok: true; - config: OpenClawConfig; - pluginId: string; - actions: UninstallActions; - warnings: string[]; - } - | { ok: false; error: string }; - export type PluginUninstallDirectoryRemoval = { target: string; cleanup?: @@ -117,7 +105,7 @@ export type PluginUninstallDirectoryRemoval = { }; }; -export type PluginUninstallPlanResult = +type PluginUninstallPlanResult = | { ok: true; config: OpenClawConfig; @@ -127,7 +115,7 @@ export type PluginUninstallPlanResult = } | { ok: false; error: string }; -export function resolveUninstallDirectoryTarget(params: { +function resolveUninstallDirectoryTarget(params: { pluginId: string; hasInstall: boolean; installRecord?: PluginInstallRecord; @@ -523,7 +511,7 @@ export function removePluginFromConfig( return { config, actions }; } -export type UninstallPluginParams = { +type UninstallPluginParams = { config: OpenClawConfig; pluginId: string; channelIds?: string[]; @@ -758,23 +746,3 @@ export async function applyPluginUninstallDirectoryRemoval( }; } } - -export async function uninstallPlugin( - params: UninstallPluginParams, -): Promise { - const plan = planPluginUninstall(params); - if (!plan.ok) { - return plan; - } - const directory = await applyPluginUninstallDirectoryRemoval(plan.directoryRemoval); - return { - ok: true, - config: plan.config, - pluginId: plan.pluginId, - actions: { - ...plan.actions, - directory: directory.directoryRemoved, - }, - warnings: directory.warnings, - }; -} diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index 280ef60ebc68..e081ee8c9198 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -7,10 +7,20 @@ import { bundledPluginRootAt } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { withEnvAsync } from "../test-utils/env.js"; -import type { PluginNpmIntegrityDriftParams } from "./install.js"; const APP_ROOT = "/app"; +type NpmInstallIntegrityDrift = { + spec: string; + expectedIntegrity: string; + actualIntegrity: string; + resolution: { + integrity?: string; + resolvedSpec?: string; + version?: string; + }; +}; + function appBundledPluginRoot(pluginId: string): string { return bundledPluginRootAt(APP_ROOT, pluginId); } @@ -2990,7 +3000,7 @@ describe("updateNpmInstalledPlugins", () => { installPluginFromNpmSpecMock.mockImplementation( async (params: { spec: string; - onIntegrityDrift?: (drift: PluginNpmIntegrityDriftParams) => boolean | Promise; + onIntegrityDrift?: (drift: NpmInstallIntegrityDrift) => boolean | Promise; }) => { const proceed = await params.onIntegrityDrift?.({ spec: params.spec, diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 808299337287..b8ca35192afb 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -72,7 +72,7 @@ import { linkOpenClawPeerDependencies } from "./plugin-peer-link.js"; import { defaultSlotIdForKey } from "./slots.js"; /** Logger surface used by plugin update flows. */ -export type PluginUpdateLogger = { +type PluginUpdateLogger = { info?: (message: string) => void; warn?: (message: string) => void; error?: (message: string) => void; @@ -80,7 +80,7 @@ export type PluginUpdateLogger = { }; /** Outcome status for one plugin update attempt. */ -export type PluginUpdateStatus = "updated" | "unchanged" | "skipped" | "error"; +type PluginUpdateStatus = "updated" | "unchanged" | "skipped" | "error"; type PluginUpdateChannelFallback = { requestedSpec: string; @@ -110,7 +110,7 @@ export type PluginUpdateOutcome = code?: string; }); -export type PluginUpdateSummary = { +type PluginUpdateSummary = { config: OpenClawConfig; changed: boolean; outcomes: PluginUpdateOutcome[]; @@ -126,7 +126,7 @@ export type PluginUpdateIntegrityDriftParams = { dryRun: boolean; }; -export type PluginChannelSyncSummary = { +type PluginChannelSyncSummary = { switchedToBundled: string[]; switchedToClawHub: string[]; switchedToNpm: string[]; @@ -134,7 +134,7 @@ export type PluginChannelSyncSummary = { errors: string[]; }; -export type PluginChannelSyncResult = { +type PluginChannelSyncResult = { config: OpenClawConfig; changed: boolean; summary: PluginChannelSyncSummary; diff --git a/src/plugins/web-fetch-providers.runtime.test.ts b/src/plugins/web-fetch-providers.runtime.test.ts index 85296748714d..451ac4890c0a 100644 --- a/src/plugins/web-fetch-providers.runtime.test.ts +++ b/src/plugins/web-fetch-providers.runtime.test.ts @@ -251,76 +251,6 @@ describe("resolvePluginWebFetchProviders", () => { expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); }); - it("reuses a compatible active registry for snapshot resolution when config is provided", () => { - const env = createWebFetchEnv(); - const rawConfig = createFirecrawlAllowConfig(); - const { config, activationSourceConfig, autoEnabledReasons } = - webFetchProvidersSharedModule.resolveBundledWebFetchResolutionConfig({ - config: rawConfig, - env, - }); - const { cacheKey } = loaderModule.testing.resolvePluginLoadCacheContext({ - config, - activationSourceConfig, - autoEnabledReasons, - workspaceDir: DEFAULT_WORKSPACE, - env, - onlyPluginIds: ["firecrawl"], - cache: true, - activate: false, - }); - const registry = createEmptyPluginRegistry(); - registry.plugins.push({ id: "firecrawl", status: "loaded" } as never); - registry.webFetchProviders.push(createRuntimeWebFetchProvider()); - setActivePluginRegistry(registry, cacheKey); - - const providers = resolvePluginWebFetchProviders({ - config: rawConfig, - workspaceDir: DEFAULT_WORKSPACE, - env, - }); - - expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ - "firecrawl:firecrawl", - ]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); - }); - - it("inherits workspaceDir from the active registry for compatible web-fetch snapshot reuse", () => { - const env = createWebFetchEnv(); - const rawConfig = createFirecrawlAllowConfig(); - const { config, activationSourceConfig, autoEnabledReasons } = - webFetchProvidersSharedModule.resolveBundledWebFetchResolutionConfig({ - config: rawConfig, - workspaceDir: DEFAULT_WORKSPACE, - env, - }); - const { cacheKey } = loaderModule.testing.resolvePluginLoadCacheContext({ - config, - activationSourceConfig, - autoEnabledReasons, - workspaceDir: DEFAULT_WORKSPACE, - env, - onlyPluginIds: ["firecrawl"], - cache: true, - activate: false, - }); - const registry = createEmptyPluginRegistry(); - registry.plugins.push({ id: "firecrawl", status: "loaded" } as never); - registry.webFetchProviders.push(createRuntimeWebFetchProvider()); - setActivePluginRegistry(registry, cacheKey, "default", DEFAULT_WORKSPACE); - - const providers = resolvePluginWebFetchProviders({ - config: rawConfig, - env, - }); - - expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([ - "firecrawl:firecrawl", - ]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); - }); - it("uses the active registry workspace for candidate discovery when workspaceDir is omitted", () => { const env = createWebFetchEnv(); const rawConfig = createFirecrawlAllowConfig(); diff --git a/src/plugins/web-provider-resolution-shared.ts b/src/plugins/web-provider-resolution-shared.ts index 3a9f60994a11..a7ec6f65c6ce 100644 --- a/src/plugins/web-provider-resolution-shared.ts +++ b/src/plugins/web-provider-resolution-shared.ts @@ -5,11 +5,11 @@ import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js import type { PluginManifestRecord } from "./manifest-registry.js"; import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.js"; -export type WebProviderContract = "webSearchProviders" | "webFetchProviders"; -export type WebProviderConfigKey = "webSearch" | "webFetch"; +type WebProviderContract = "webSearchProviders" | "webFetchProviders"; +type WebProviderConfigKey = "webSearch" | "webFetch"; /** Manifest-backed plugin id candidates for a web provider family. */ -export type WebProviderCandidateResolution = { +type WebProviderCandidateResolution = { pluginIds: string[] | undefined; manifestRecords?: readonly PluginManifestRecord[]; }; diff --git a/src/plugins/web-provider-runtime-shared.ts b/src/plugins/web-provider-runtime-shared.ts index 22bf1791a526..370e56eb8a46 100644 --- a/src/plugins/web-provider-runtime-shared.ts +++ b/src/plugins/web-provider-runtime-shared.ts @@ -13,7 +13,7 @@ import { } from "./runtime/load-context.js"; /** Shared options for resolving plugin-backed web providers. */ -export type ResolvePluginWebProvidersParams = { +type ResolvePluginWebProvidersParams = { config?: PluginLoadOptions["config"]; workspaceDir?: string; env?: PluginLoadOptions["env"]; diff --git a/src/plugins/web-provider-types.ts b/src/plugins/web-provider-types.ts index ece28b9b476b..c0d47339e312 100644 --- a/src/plugins/web-provider-types.ts +++ b/src/plugins/web-provider-types.ts @@ -46,12 +46,12 @@ export type WebFetchProviderContext = { export type WebSearchCredentialResolutionSource = "config" | "secretRef" | "env" | "missing"; -export type WebSearchProviderConfiguredCredentialFallback = { +type WebSearchProviderConfiguredCredentialFallback = { path: string; value: unknown; }; -export type WebFetchProviderConfiguredCredentialFallback = { +type WebFetchProviderConfiguredCredentialFallback = { path: string; value: unknown; }; diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts index 395f346ab3bf..6f16966ad22b 100644 --- a/src/plugins/web-search-providers.runtime.test.ts +++ b/src/plugins/web-search-providers.runtime.test.ts @@ -5,7 +5,6 @@ type RegistryModule = typeof import("./registry.js"); type RuntimeModule = typeof import("./runtime.js"); type WebSearchProvidersRuntimeModule = typeof import("./web-search-providers.runtime.js"); type PluginAutoEnableModule = typeof import("../config/plugin-auto-enable.js"); -type WebSearchProvidersSharedModule = typeof import("./web-search-providers.shared.js"); type PluginManifestRegistry = import("./manifest-registry.js").PluginManifestRegistry; type LoadPluginManifestRegistryForPluginRegistry = typeof import("./plugin-registry.js").loadPluginManifestRegistryForPluginRegistry; @@ -34,12 +33,10 @@ let loadInstalledPluginManifestRegistryMock: ReturnType< >; let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"]; let resolvePluginWebSearchProviders: WebSearchProvidersRuntimeModule["resolvePluginWebSearchProviders"]; -let resolveRuntimeWebSearchProviders: WebSearchProvidersRuntimeModule["resolveRuntimeWebSearchProviders"]; let loadOpenClawPluginsMock: ReturnType; let loaderModule: typeof import("./loader.js"); let pluginAutoEnableModule: PluginAutoEnableModule; let applyPluginAutoEnableSpy: ReturnType; -let webSearchProvidersSharedModule: WebSearchProvidersSharedModule; let resetPluginRuntimeStateForTest: RuntimeModule["resetPluginRuntimeStateForTest"]; let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo; @@ -275,93 +272,6 @@ function expectSnapshotLoaderCalls(params: { expectLoaderCallCount(params.expectedLoaderCalls); } -function createRuntimeWebSearchProvider(params: { - pluginId: string; - pluginName: string; - id: string; - label: string; - hint: string; - envVar: string; - signupUrl: string; - credentialPath: string; -}) { - return { - pluginId: params.pluginId, - pluginName: params.pluginName, - provider: { - id: params.id, - label: params.label, - hint: params.hint, - envVars: [params.envVar], - placeholder: `${params.id}-...`, - signupUrl: params.signupUrl, - autoDetectOrder: 1, - credentialPath: params.credentialPath, - getCredentialValue: () => "configured", - setCredentialValue: () => {}, - createTool: () => ({ - description: params.id, - parameters: {}, - execute: async () => ({}), - }), - }, - source: "test" as const, - }; -} - -function createBraveRuntimeWebSearchProvider() { - return createRuntimeWebSearchProvider({ - pluginId: "brave", - pluginName: "Brave", - id: "brave", - label: "Brave Search", - hint: "Brave runtime provider", - envVar: "BRAVE_API_KEY", - signupUrl: "https://example.com/brave", - credentialPath: "plugins.entries.brave.config.webSearch.apiKey", - }); -} - -function createActiveBraveRegistryFixture(params?: { - includeResolutionWorkspaceDir?: boolean; - activeWorkspaceDir?: string; -}) { - const env = createWebSearchEnv(); - const rawConfig = createBraveAllowConfig(); - const { config, activationSourceConfig, autoEnabledReasons } = - webSearchProvidersSharedModule.resolveBundledWebSearchResolutionConfig({ - config: rawConfig, - ...(params?.includeResolutionWorkspaceDir - ? { workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE } - : {}), - env, - }); - const { cacheKey } = loaderModule.testing.resolvePluginLoadCacheContext({ - config, - activationSourceConfig, - autoEnabledReasons, - workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE, - env, - onlyPluginIds: ["brave"], - cache: true, - activate: false, - }); - const registry = createEmptyPluginRegistry(); - registry.plugins.push({ id: "brave", status: "loaded" } as never); - registry.webSearchProviders.push(createBraveRuntimeWebSearchProvider()); - setActivePluginRegistry(registry, cacheKey, "default", params?.activeWorkspaceDir); - - return { env, rawConfig }; -} - -function expectRuntimeProviderResolution( - providers: ReturnType, - expected: readonly string[], -) { - expect(toRuntimeProviderKeys(providers)).toEqual([...expected]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); -} - describe("resolvePluginWebSearchProviders", () => { beforeAll(async () => { loadPluginManifestRegistryMock = vi.fn(); @@ -414,11 +324,9 @@ describe("resolvePluginWebSearchProviders", () => { ({ createEmptyPluginRegistry } = await import("./registry-empty.js")); loaderModule = await import("./loader.js"); pluginAutoEnableModule = await import("../config/plugin-auto-enable.js"); - webSearchProvidersSharedModule = await import("./web-search-providers.shared.js"); ({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js")); ({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js")); - ({ resolvePluginWebSearchProviders, resolveRuntimeWebSearchProviders } = - await import("./web-search-providers.runtime.js")); + ({ resolvePluginWebSearchProviders } = await import("./web-search-providers.runtime.js")); }); beforeEach(() => { @@ -556,33 +464,6 @@ describe("resolvePluginWebSearchProviders", () => { expect(loaderParams.workspaceDir).toBe("/tmp/runtime-workspace"); expect(loaderParams.onlyPluginIds).toEqual(["brave"]); }); - it("reuses a compatible active registry for snapshot resolution when config is provided", () => { - const { env, rawConfig } = createActiveBraveRegistryFixture(); - - const providers = resolvePluginWebSearchProviders({ - config: rawConfig, - workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE, - env, - }); - - expectRuntimeProviderResolution(providers, ["brave:brave"]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); - }); - - it("inherits workspaceDir from the active registry for compatible web-search snapshot reuse", () => { - const { env, rawConfig } = createActiveBraveRegistryFixture({ - includeResolutionWorkspaceDir: true, - activeWorkspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE, - }); - - const providers = resolvePluginWebSearchProviders({ - config: rawConfig, - env, - }); - - expectRuntimeProviderResolution(providers, ["brave:brave"]); - expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); - }); it("uses the inherited active workspace for each web-search resolution", () => { const env = createWebSearchEnv(); @@ -652,45 +533,4 @@ describe("resolvePluginWebSearchProviders", () => { expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2); }); - - it.each([ - { - name: "prefers the active plugin registry for runtime resolution", - setupRegistry: () => { - const registry = createEmptyPluginRegistry(); - registry.webSearchProviders.push( - createRuntimeWebSearchProvider({ - pluginId: "custom-search", - pluginName: "Custom Search", - id: "custom", - label: "Custom Search", - hint: "Custom runtime provider", - envVar: "CUSTOM_SEARCH_API_KEY", - signupUrl: "https://example.com/signup", - credentialPath: "tools.web.search.custom.apiKey", - }), - ); - setActivePluginRegistry(registry); - }, - params: {}, - expected: ["custom-search:custom"], - }, - { - name: "reuses a compatible active registry for runtime resolution when config is provided", - setupRegistry: () => { - const { env, rawConfig } = createActiveBraveRegistryFixture(); - return { - config: rawConfig, - workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE, - env, - }; - }, - expected: ["brave:brave"], - }, - ] as const)("$name", ({ setupRegistry, params, expected }) => { - const runtimeParams = setupRegistry() ?? params ?? {}; - const providers = resolveRuntimeWebSearchProviders(runtimeParams); - - expectRuntimeProviderResolution(providers, expected); - }); }); diff --git a/src/plugins/wired-hooks-gateway.test.ts b/src/plugins/wired-hooks-gateway.test.ts index 142cc5b6362f..2a8d00a412cf 100644 --- a/src/plugins/wired-hooks-gateway.test.ts +++ b/src/plugins/wired-hooks-gateway.test.ts @@ -6,7 +6,7 @@ * and validating the integration pattern. */ import { describe, expect, it, vi } from "vitest"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; import type { PluginHookCronChangedEvent, PluginHookCronReconciledContext, diff --git a/src/plugins/wired-hooks-inbound-claim.test.ts b/src/plugins/wired-hooks-inbound-claim.test.ts index 765af189f0e1..a19e237b2d35 100644 --- a/src/plugins/wired-hooks-inbound-claim.test.ts +++ b/src/plugins/wired-hooks-inbound-claim.test.ts @@ -2,7 +2,7 @@ import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; const inboundClaimEvent = { content: "who are you", diff --git a/src/plugins/wired-hooks-llm.test.ts b/src/plugins/wired-hooks-llm.test.ts index 16704e40c374..4549691ae011 100644 --- a/src/plugins/wired-hooks-llm.test.ts +++ b/src/plugins/wired-hooks-llm.test.ts @@ -1,6 +1,6 @@ // Covers wired plugin hooks around LLM request handling. import { describe, expect, it, vi } from "vitest"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; const hookCtx = { agentId: "main", diff --git a/src/plugins/wired-hooks-message.test.ts b/src/plugins/wired-hooks-message.test.ts index 8c495d88cd1c..3daa54aafa13 100644 --- a/src/plugins/wired-hooks-message.test.ts +++ b/src/plugins/wired-hooks-message.test.ts @@ -4,7 +4,7 @@ * Tests the hook runner methods directly since outbound delivery is deeply integrated. */ import { describe, expect, it, vi } from "vitest"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; import type { PluginHookMessageSendingEvent, PluginHookMessageSendingResult, diff --git a/src/plugins/wired-hooks-reply-dispatch.test.ts b/src/plugins/wired-hooks-reply-dispatch.test.ts index 132b118277b6..2015986ab8b4 100644 --- a/src/plugins/wired-hooks-reply-dispatch.test.ts +++ b/src/plugins/wired-hooks-reply-dispatch.test.ts @@ -3,7 +3,7 @@ import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import { buildTestCtx } from "../auto-reply/reply/test-ctx.js"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; const replyDispatchEvent = { ctx: buildTestCtx({ SessionKey: "agent:test:session", BodyForAgent: "hello" }), diff --git a/src/plugins/wired-hooks-reply-payload-sending.test.ts b/src/plugins/wired-hooks-reply-payload-sending.test.ts index cf9ee2676e72..9e812e006689 100644 --- a/src/plugins/wired-hooks-reply-payload-sending.test.ts +++ b/src/plugins/wired-hooks-reply-payload-sending.test.ts @@ -6,7 +6,7 @@ import { type ReplyPayload, } from "../auto-reply/reply-payload.js"; import type { PluginHookReplyPayload } from "./hook-types.js"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; function createDeferred() { let resolve!: (value: T | PromiseLike) => void; diff --git a/src/plugins/wired-hooks-session.test.ts b/src/plugins/wired-hooks-session.test.ts index 61ec013678de..5e3397e62c74 100644 --- a/src/plugins/wired-hooks-session.test.ts +++ b/src/plugins/wired-hooks-session.test.ts @@ -4,7 +4,7 @@ * Tests the hook runner methods directly since session init is deeply integrated. */ import { describe, expect, it, vi } from "vitest"; -import { createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; import type { PluginHookSessionContext, PluginHookSessionEndEvent, diff --git a/src/plugins/wired-hooks-subagent.test.ts b/src/plugins/wired-hooks-subagent.test.ts index e51072cd8a99..9d28780fe34f 100644 --- a/src/plugins/wired-hooks-subagent.test.ts +++ b/src/plugins/wired-hooks-subagent.test.ts @@ -2,7 +2,7 @@ * Test: subagent_spawning, subagent_delivery_target, subagent_spawned & subagent_ended hook wiring */ import { describe, expect, it, vi } from "vitest"; -import { addStaticTestHooks, createHookRunnerWithRegistry } from "./hooks.test-helpers.js"; +import { addStaticTestHooks, createHookRunnerWithRegistry } from "./hooks.test-fixtures.js"; describe("subagent hook runner methods", () => { const baseRequester = { diff --git a/src/plugins/worker-provider-registry.test.ts b/src/plugins/worker-provider-registry.test.ts index b4b2845bae43..0a8b4427b251 100644 --- a/src/plugins/worker-provider-registry.test.ts +++ b/src/plugins/worker-provider-registry.test.ts @@ -1,15 +1,10 @@ /** Covers cloud-worker provider manifest ownership, uniqueness, and lookup ordering. */ import { describe, expect, it } from "vitest"; import { createPluginRecord } from "./loader-records.js"; -import { createEmptyPluginRegistry } from "./registry-empty.js"; import { createPluginRegistry } from "./registry.js"; import type { PluginRuntime } from "./runtime/types.js"; import type { WorkerProvider } from "./types.js"; -import { - listWorkerProviderRegistrations, - resolveDurableWorkerProviderAutoEnabledReasons, - resolveWorkerProvider, -} from "./worker-provider-registry.js"; +import { resolveDurableWorkerProviderAutoEnabledReasons } from "./worker-provider-registry.js"; function createTestRegistry() { return createPluginRegistry({ @@ -48,17 +43,6 @@ function createOwner(id: string, workerProviders: string[] = []) { } describe("worker provider registry", () => { - it("registers providers declared by the plugin manifest", () => { - const pluginRegistry = createTestRegistry(); - const provider = createWorkerProvider("static-ssh"); - - pluginRegistry.registerWorkerProvider(createOwner("owner", ["static-ssh"]), provider); - - expect(listWorkerProviderRegistrations(pluginRegistry.registry)).toEqual([ - expect.objectContaining({ pluginId: "owner", provider }), - ]); - }); - it("rejects registrations missing manifest ownership", () => { const pluginRegistry = createTestRegistry(); @@ -161,27 +145,6 @@ describe("worker provider registry", () => { ); }); - it("lists deterministically and resolves normalized provider ids", () => { - const registry = createEmptyPluginRegistry(); - const alphaA = createWorkerProvider("alpha"); - const beta = createWorkerProvider("beta"); - registry.workerProviders.set("beta", { pluginId: "b-owner", provider: beta, source: "b" }); - registry.workerProviders.set("alpha", { - pluginId: "a-owner", - provider: alphaA, - source: "a", - }); - - expect( - listWorkerProviderRegistrations(registry).map((entry) => [entry.provider.id, entry.pluginId]), - ).toEqual([ - ["alpha", "a-owner"], - ["beta", "b-owner"], - ]); - expect(resolveWorkerProvider(registry, " ALPHA ")).toBe(alphaA); - expect(resolveWorkerProvider(registry, "__proto__")).toBeUndefined(); - }); - it("auto-enables only bundled owners needed by durable leases", () => { const reasons = resolveDurableWorkerProviderAutoEnabledReasons( { diff --git a/src/plugins/worker-provider-registry.ts b/src/plugins/worker-provider-registry.ts index 369de25f3d96..d1d6e2bc31bb 100644 --- a/src/plugins/worker-provider-registry.ts +++ b/src/plugins/worker-provider-registry.ts @@ -2,7 +2,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import { normalizeCapabilityProviderId } from "./provider-registry-shared.js"; -import type { PluginWorkerProviderRegistration, PluginRegistry } from "./registry-types.js"; +import type { PluginRegistry } from "./registry-types.js"; import type { WorkerProvider } from "./types.js"; type WorkerProviderRegistryView = Pick; @@ -95,16 +95,6 @@ export function validateWorkerProviderContract( ? { ok: true, id } : { ok: false, message: `plugin must declare contracts.workerProviders for provider: ${id}` }; } - -/** Returns provider registrations in stable provider-id order. */ -export function listWorkerProviderRegistrations( - registry: WorkerProviderRegistryView, -): PluginWorkerProviderRegistration[] { - return [...registry.workerProviders.entries()] - .toSorted(([leftId], [rightId]) => compareText(leftId, rightId)) - .map(([, registration]) => registration); -} - /** Resolves one provider by its normalized manifest capability id. */ export function resolveWorkerProvider( registry: WorkerProviderRegistryView, diff --git a/src/secrets/runtime.integration.test-helpers.ts b/src/secrets/runtime.integration.test-helpers.ts index f3766b5ab0dc..491c4976f9df 100644 --- a/src/secrets/runtime.integration.test-helpers.ts +++ b/src/secrets/runtime.integration.test-helpers.ts @@ -1,7 +1,7 @@ /** Integration-test helpers for preparing secrets runtime fixtures. */ import { vi } from "vitest"; import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; -import { clearPluginLoaderCache } from "../plugins/loader.js"; +import { clearPluginLoaderCache } from "../plugins/loader.test-fixtures.js"; import { captureEnv } from "../test-utils/env.js"; import type { SecretsRuntimeEnvSnapshot } from "./runtime-openai-file-fixture.test-helper.js"; diff --git a/src/skills/lifecycle/archive-install.test.ts b/src/skills/lifecycle/archive-install.test.ts index 0e6767de5748..e370721c6ada 100644 --- a/src/skills/lifecycle/archive-install.test.ts +++ b/src/skills/lifecycle/archive-install.test.ts @@ -8,7 +8,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../../plugins/hooks.test-fixtures.js"; import { createTrackedTempDirs } from "../../test-utils/tracked-temp-dirs.js"; import { CLAWHUB_SKILL_ARCHIVE_ROOT_MARKERS, diff --git a/src/skills/lifecycle/install.test.ts b/src/skills/lifecycle/install.test.ts index b12f3a140a49..efe4d683bd52 100644 --- a/src/skills/lifecycle/install.test.ts +++ b/src/skills/lifecycle/install.test.ts @@ -6,7 +6,7 @@ import { initializeGlobalHookRunner, resetGlobalHookRunner, } from "../../plugins/hook-runner-global.js"; -import { createMockPluginRegistry } from "../../plugins/hooks.test-helpers.js"; +import { createMockPluginRegistry } from "../../plugins/hooks.test-fixtures.js"; import { captureEnv } from "../../test-utils/env.js"; import { createFixtureSuite } from "../../test-utils/fixture-suite.js"; import { resolveOpenClawMetadata, resolveSkillInvocationPolicy } from "../loading/frontmatter.js"; diff --git a/src/test-utils/bundled-plugin-public-surface.ts b/src/test-utils/bundled-plugin-public-surface.ts index cba8a419eb5a..48ce1967a3ca 100644 --- a/src/test-utils/bundled-plugin-public-surface.ts +++ b/src/test-utils/bundled-plugin-public-surface.ts @@ -8,10 +8,7 @@ import { loadBundledPluginPublicSurfaceModuleSync, } from "../plugin-sdk/facade-loader.js"; import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; -import { - findBundledPluginMetadataById, - type BundledPluginMetadata, -} from "../plugins/bundled-plugin-metadata.js"; +import { findBundledPluginMetadataById } from "../plugins/bundled-plugin-metadata.js"; import { getCachedPluginSourceModuleLoader, type PluginModuleLoaderCache, @@ -25,7 +22,10 @@ const OPENCLAW_PACKAGE_ROOT = moduleUrl: import.meta.url, }) ?? fileURLToPath(new URL("../..", import.meta.url)); -type BundledPluginPublicSurfaceMetadata = Pick; +type BundledPluginPublicSurfaceMetadata = Pick< + NonNullable>, + "dirName" +>; const sourceModuleLoaders: PluginModuleLoaderCache = new Map(); function isSafeBundledPluginDirName(pluginId: string): boolean { diff --git a/test/bundled-provider-auth-literal-parity.test.ts b/test/bundled-provider-auth-literal-parity.test.ts index 4662e3ffc6eb..959c9d04baa3 100644 --- a/test/bundled-provider-auth-literal-parity.test.ts +++ b/test/bundled-provider-auth-literal-parity.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterAll, describe, expect, it } from "vitest"; import { listBundledPluginMetadata } from "../src/plugins/bundled-plugin-metadata.js"; -import type { PluginManifestProviderAuthChoice } from "../src/plugins/manifest.js"; +import type { PluginManifest } from "../src/plugins/manifest.js"; import type { ProviderAuthMethod, ProviderPlugin, @@ -21,6 +21,8 @@ type ApiKeyStyleChoice = PluginManifestProviderAuthChoice & { cliFlag: string; }; +type PluginManifestProviderAuthChoice = NonNullable[number]; + type ParityCase = { pluginId: string; providerId: string; diff --git a/test/plugins/bundled-provider-auth-literal-parity.test.ts b/test/plugins/bundled-provider-auth-literal-parity.test.ts index 533059a03f72..2f2a5b8abbd6 100644 --- a/test/plugins/bundled-provider-auth-literal-parity.test.ts +++ b/test/plugins/bundled-provider-auth-literal-parity.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterAll, describe, expect, it } from "vitest"; import { listBundledPluginMetadata } from "../../src/plugins/bundled-plugin-metadata.js"; -import type { PluginManifestProviderAuthChoice } from "../../src/plugins/manifest.js"; +import type { PluginManifest } from "../../src/plugins/manifest.js"; import type { ProviderAuthMethod, ProviderPlugin, @@ -21,6 +21,8 @@ type ApiKeyStyleChoice = PluginManifestProviderAuthChoice & { cliFlag: string; }; +type PluginManifestProviderAuthChoice = NonNullable[number]; + type ParityCase = { pluginId: string; providerId: string; diff --git a/test/scripts/test-live-shard.test.ts b/test/scripts/test-live-shard.test.ts index 562d4347b02e..4519b0050049 100644 --- a/test/scripts/test-live-shard.test.ts +++ b/test/scripts/test-live-shard.test.ts @@ -88,6 +88,9 @@ describe("scripts/test-live-shard", () => { expect(selectLiveShardFiles("native-live-src-agents", allFiles)).toContain( "src/llm/providers/stream-wrappers/anthropic-family-tool-payload-compat.live.test.ts", ); + expect(selectLiveShardFiles("native-live-src-agents", allFiles)).toContain( + "src/skills/workshop/experience-review.live.test.ts", + ); expect(selectLiveShardFiles("native-live-src-agents-zai-coding", allFiles)).toEqual([ "src/agents/zai.live.test.ts", ]);