diff --git a/CHANGELOG.md b/CHANGELOG.md index 22293cd4c..6ff27dca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changes - Plugins: add extension loader (tools/RPC/CLI/services), discovery paths, config schema, and Control UI labels; ship voice-call plugin stub + skill. - Docs: add plugins doc + cross-links from tools/skills/gateway config. +- Tests: add Docker plugin loader smoke test. - Build: set pnpm minimum release age to 2880 minutes (2 days). (#718) — thanks @dan-dr. - macOS: prompt to install the global `clawdbot` CLI when missing in local mode; install via `clawd.bot/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime. - Docs: add gog calendar event color IDs from `gog calendar colors`. (#715) — thanks @mjrussell. diff --git a/docs/testing.md b/docs/testing.md index 7a883ab02..5d7c4029f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -298,6 +298,7 @@ These run `pnpm test:live` inside the repo Docker image, mounting your local con - Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`) - Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`) - Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`) +- Plugins (custom extension load + registry smoke): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`) Useful env vars: diff --git a/package.json b/package.json index 9f724d622..e302e61e7 100644 --- a/package.json +++ b/package.json @@ -104,8 +104,9 @@ "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh", "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh", "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh", + "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh", "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh", - "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:cleanup", + "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup", "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all", "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh", "test:install:e2e:openai": "CLAWDBOT_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh", diff --git a/scripts/e2e/plugins-docker.sh b/scripts/e2e/plugins-docker.sh new file mode 100755 index 000000000..fabb5bb75 --- /dev/null +++ b/scripts/e2e/plugins-docker.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +IMAGE_NAME="clawdbot-plugins-e2e" + +echo "Building Docker image..." +docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" + +echo "Running plugins Docker E2E..." +docker run --rm -t "$IMAGE_NAME" bash -lc ' + set -euo pipefail + + home_dir=$(mktemp -d "/tmp/clawdbot-plugins-e2e.XXXXXX") + export HOME="$home_dir" + mkdir -p "$HOME/.clawdbot/extensions" + + cat > "$HOME/.clawdbot/extensions/demo-plugin.js" <<'"'"'JS'"'"' +module.exports = { + id: "demo-plugin", + name: "Demo Plugin", + description: "Docker E2E demo plugin", + register(api) { + api.registerTool(() => null, { name: "demo_tool" }); + api.registerGatewayMethod("demo.ping", async () => ({ ok: true })); + api.registerCli(() => {}, { commands: ["demo"] }); + api.registerService({ id: "demo-service", start: () => {} }); + }, +}; +JS + + node dist/index.js plugins list --json > /tmp/plugins.json + + node - <<'"'"'NODE'"'"' +const fs = require("node:fs"); + +const data = JSON.parse(fs.readFileSync("/tmp/plugins.json", "utf8")); +const plugin = (data.plugins || []).find((entry) => entry.id === "demo-plugin"); +if (!plugin) throw new Error("plugin not found"); +if (plugin.status !== "loaded") { + throw new Error(`unexpected status: ${plugin.status}`); +} + +const assertIncludes = (list, value, label) => { + if (!Array.isArray(list) || !list.includes(value)) { + throw new Error(`${label} missing: ${value}`); + } +}; + +assertIncludes(plugin.toolNames, "demo_tool", "tool"); +assertIncludes(plugin.gatewayMethods, "demo.ping", "gateway method"); +assertIncludes(plugin.cliCommands, "demo", "cli command"); +assertIncludes(plugin.services, "demo-service", "service"); + +const diagErrors = (data.diagnostics || []).filter((diag) => diag.level === "error"); +if (diagErrors.length > 0) { + throw new Error(`diagnostics errors: ${diagErrors.map((diag) => diag.message).join("; ")}`); +} + +console.log("ok"); +NODE +' + +echo "OK" diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 628d46421..7143d9e38 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -153,7 +153,7 @@ export function registerPluginsCli(program: Command) { plugins: { ...cfg.plugins, entries: { - ...(cfg.plugins?.entries ?? {}), + ...cfg.plugins?.entries, [id]: { ...( cfg.plugins?.entries as @@ -182,7 +182,7 @@ export function registerPluginsCli(program: Command) { plugins: { ...cfg.plugins, entries: { - ...(cfg.plugins?.entries ?? {}), + ...cfg.plugins?.entries, [id]: { ...( cfg.plugins?.entries as