feat: add rule dispatch, cross-plugin API, and Docker integration test
Wire rule registry and authenticated callbacks into both client and server runtimes; expose __yonexusClient / __yonexusServer on globalThis for cross-plugin communication. Add Docker-based integration test with server-test-plugin (test_ping echo) and client-test-plugin (test_pong receiver), plus docker-compose setup. Fix transport race condition where a stale _connections entry caused promoteToAuthenticated to silently fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.idea/
|
||||||
|
tests/docker/.env
|
||||||
Submodule Yonexus.Client updated: 57b53fc122...8824e768fb
Submodule Yonexus.Server updated: 31f41cb49b...59d5b26aff
33
tests/docker/client-test-plugin/index.mjs
Normal file
33
tests/docker/client-test-plugin/index.mjs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Singleton guard — openclaw calls register() twice per process
|
||||||
|
let _registered = false;
|
||||||
|
|
||||||
|
export default function register(_api) {
|
||||||
|
if (_registered) return;
|
||||||
|
_registered = true;
|
||||||
|
|
||||||
|
const client = globalThis.__yonexusClient;
|
||||||
|
if (!client) {
|
||||||
|
console.error('[client-test] __yonexusClient not on globalThis — ensure Yonexus.Client loads first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[client-test] __yonexusClient available, keys:', Object.keys(client));
|
||||||
|
|
||||||
|
// Register test_pong rule
|
||||||
|
// Received format (plain rule message from server): test_pong::<content>
|
||||||
|
client.ruleRegistry.registerRule('test_pong', (raw) => {
|
||||||
|
const sep = raw.indexOf('::');
|
||||||
|
const content = raw.slice(sep + 2);
|
||||||
|
console.log(`[client-test] MATCH test_pong content="${content}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When authenticated, send one matching and one non-matching rule message to server
|
||||||
|
client.onAuthenticated.push(() => {
|
||||||
|
console.log('[client-test] Authenticated — sending test_ping + other_rule to server');
|
||||||
|
const s1 = client.sendRule('test_ping', 'hello-from-client');
|
||||||
|
const s2 = client.sendRule('other_rule', 'other-from-client');
|
||||||
|
console.log(`[client-test] sendRule results: test_ping=${s1} other_rule=${s2}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[client-test] registered test_pong rule and onAuthenticated callback');
|
||||||
|
}
|
||||||
13
tests/docker/client-test-plugin/openclaw.plugin.json
Normal file
13
tests/docker/client-test-plugin/openclaw.plugin.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "yonexus-client-test",
|
||||||
|
"name": "Yonexus Client Test Plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Test plugin for Yonexus.Client rule routing",
|
||||||
|
"entry": "./index.mjs",
|
||||||
|
"permissions": [],
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
tests/docker/client/Dockerfile
Normal file
37
tests/docker/client/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Build context: repo root (Yonexus/)
|
||||||
|
# ── Stage 1: compile ──────────────────────────────────────────────────────────
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Client imports Yonexus.Protocol only
|
||||||
|
COPY Yonexus.Protocol/src ./Yonexus.Protocol/src
|
||||||
|
|
||||||
|
COPY Yonexus.Client/package.json ./Yonexus.Client/
|
||||||
|
COPY Yonexus.Client/package-lock.json ./Yonexus.Client/
|
||||||
|
COPY Yonexus.Client/tsconfig.json ./Yonexus.Client/
|
||||||
|
COPY Yonexus.Client/plugin ./Yonexus.Client/plugin
|
||||||
|
|
||||||
|
WORKDIR /build/Yonexus.Client
|
||||||
|
RUN npm ci
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Stage 2: runtime ─────────────────────────────────────────────────────────
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
|
||||||
|
RUN npm install -g openclaw@2026.4.9
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Layout expected by install.mjs: repoRoot = /app, sourceDist = /app/dist
|
||||||
|
COPY --from=builder /build/Yonexus.Client/dist ./dist
|
||||||
|
COPY --from=builder /build/Yonexus.Client/node_modules ./node_modules
|
||||||
|
COPY Yonexus.Client/package.json ./package.json
|
||||||
|
COPY Yonexus.Client/plugin/openclaw.plugin.json ./plugin/openclaw.plugin.json
|
||||||
|
COPY Yonexus.Client/scripts/install.mjs ./scripts/install.mjs
|
||||||
|
|
||||||
|
COPY tests/docker/client-test-plugin /app/client-test-plugin
|
||||||
|
COPY tests/docker/client/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
69
tests/docker/client/entrypoint.sh
Normal file
69
tests/docker/client/entrypoint.sh
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
: "${IDENTIFIER:?IDENTIFIER is required}"
|
||||||
|
: "${NOTIFY_BOT_TOKEN:?NOTIFY_BOT_TOKEN is required}"
|
||||||
|
: "${ADMIN_USER_ID:?ADMIN_USER_ID is required}"
|
||||||
|
: "${YONEXUS_SERVER_URL:?YONEXUS_SERVER_URL is required}"
|
||||||
|
|
||||||
|
STATE_DIR=/app/.openclaw-state
|
||||||
|
PLUGIN_DIR="$STATE_DIR/plugins/Yonexus.Client"
|
||||||
|
TEST_PLUGIN_DIR="$STATE_DIR/plugins/yonexus-client-test"
|
||||||
|
|
||||||
|
# Install plugin dist + manifest into isolated state directory
|
||||||
|
node /app/scripts/install.mjs --install --openclaw-profile-path "$STATE_DIR"
|
||||||
|
# Symlink node_modules so bare-module imports (e.g. ws) resolve from plugin dir
|
||||||
|
ln -sf /app/node_modules "$PLUGIN_DIR/node_modules"
|
||||||
|
|
||||||
|
# Install test plugin (plain .mjs, no compilation needed)
|
||||||
|
mkdir -p "$TEST_PLUGIN_DIR"
|
||||||
|
cp /app/client-test-plugin/index.mjs "$TEST_PLUGIN_DIR/"
|
||||||
|
cp /app/client-test-plugin/openclaw.plugin.json "$TEST_PLUGIN_DIR/"
|
||||||
|
|
||||||
|
# Write openclaw config — plugin id is "yonexus-client" per openclaw.plugin.json
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
cat > "$STATE_DIR/openclaw.json" << EOF
|
||||||
|
{
|
||||||
|
"meta": { "lastTouchedVersion": "2026.4.9" },
|
||||||
|
"gateway": { "bind": "loopback" },
|
||||||
|
"agents": { "defaults": { "workspace": "$STATE_DIR/workspace" } },
|
||||||
|
"plugins": {
|
||||||
|
"allow": ["yonexus-client", "yonexus-client-test"],
|
||||||
|
"load": { "paths": ["$PLUGIN_DIR", "$TEST_PLUGIN_DIR"] },
|
||||||
|
"installs": {
|
||||||
|
"yonexus-client": {
|
||||||
|
"source": "path",
|
||||||
|
"sourcePath": "$PLUGIN_DIR",
|
||||||
|
"installPath": "$PLUGIN_DIR",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"installedAt": "2026-04-10T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
"yonexus-client-test": {
|
||||||
|
"source": "path",
|
||||||
|
"sourcePath": "$TEST_PLUGIN_DIR",
|
||||||
|
"installPath": "$TEST_PLUGIN_DIR",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"installedAt": "2026-04-10T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entries": {
|
||||||
|
"yonexus-client": {
|
||||||
|
"enabled": true,
|
||||||
|
"config": {
|
||||||
|
"mainHost": "$YONEXUS_SERVER_URL",
|
||||||
|
"identifier": "$IDENTIFIER",
|
||||||
|
"notifyBotToken": "$NOTIFY_BOT_TOKEN",
|
||||||
|
"adminUserId": "$ADMIN_USER_ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yonexus-client-test": {
|
||||||
|
"enabled": true,
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
export OPENCLAW_STATE_DIR="$STATE_DIR"
|
||||||
|
exec openclaw gateway run --allow-unconfigured
|
||||||
46
tests/docker/docker-compose.yml
Normal file
46
tests/docker/docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
yonexus-server:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: tests/docker/server/Dockerfile
|
||||||
|
environment:
|
||||||
|
# Identifier the client will use — must match IDENTIFIER on the client side
|
||||||
|
CLIENT_IDENTIFIER: test-client
|
||||||
|
# Required: Discord bot token and admin user ID for pairing notifications
|
||||||
|
NOTIFY_BOT_TOKEN: ${NOTIFY_BOT_TOKEN}
|
||||||
|
ADMIN_USER_ID: ${ADMIN_USER_ID}
|
||||||
|
# Optional: override the publicWsUrl advertised to clients
|
||||||
|
# PUBLIC_WS_URL: ws://yonexus-server:8787
|
||||||
|
networks:
|
||||||
|
- yonexus-net
|
||||||
|
healthcheck:
|
||||||
|
# Wait until the Yonexus WebSocket port is accepting connections
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- node
|
||||||
|
- -e
|
||||||
|
- "require('net').createConnection({port:8787,host:'127.0.0.1'}).on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))"
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
yonexus-client:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: tests/docker/client/Dockerfile
|
||||||
|
environment:
|
||||||
|
# Must match CLIENT_IDENTIFIER on the server side
|
||||||
|
IDENTIFIER: test-client
|
||||||
|
NOTIFY_BOT_TOKEN: ${NOTIFY_BOT_TOKEN}
|
||||||
|
ADMIN_USER_ID: ${ADMIN_USER_ID}
|
||||||
|
YONEXUS_SERVER_URL: ws://yonexus-server:8787
|
||||||
|
networks:
|
||||||
|
- yonexus-net
|
||||||
|
depends_on:
|
||||||
|
yonexus-server:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
yonexus-net:
|
||||||
|
driver: bridge
|
||||||
39
tests/docker/server-test-plugin/index.mjs
Normal file
39
tests/docker/server-test-plugin/index.mjs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Singleton guard — openclaw calls register() twice per process
|
||||||
|
let _registered = false;
|
||||||
|
|
||||||
|
export default function register(_api) {
|
||||||
|
if (_registered) return;
|
||||||
|
_registered = true;
|
||||||
|
|
||||||
|
const server = globalThis.__yonexusServer;
|
||||||
|
if (!server) {
|
||||||
|
console.error('[server-test] __yonexusServer not on globalThis — ensure Yonexus.Server loads first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[server-test] __yonexusServer available, keys:', Object.keys(server));
|
||||||
|
|
||||||
|
// Register test_ping rule
|
||||||
|
// Received format (rewritten by server): test_ping::<senderIdentifier>::<content>
|
||||||
|
server.ruleRegistry.registerRule('test_ping', (raw) => {
|
||||||
|
const firstSep = raw.indexOf('::');
|
||||||
|
const rest = raw.slice(firstSep + 2);
|
||||||
|
const secondSep = rest.indexOf('::');
|
||||||
|
const sender = rest.slice(0, secondSep);
|
||||||
|
const content = rest.slice(secondSep + 2);
|
||||||
|
console.log(`[server-test] MATCH test_ping from="${sender}" content="${content}"`);
|
||||||
|
// Echo back to sender via test_pong
|
||||||
|
const sent = server.sendRule(sender, 'test_pong', `echo-${content}`);
|
||||||
|
console.log(`[server-test] echo sent=${sent}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a client authenticates, send one matching and one non-matching rule message
|
||||||
|
server.onClientAuthenticated.push((identifier) => {
|
||||||
|
console.log(`[server-test] Client "${identifier}" authenticated — sending test_pong + other_rule`);
|
||||||
|
const s1 = server.sendRule(identifier, 'test_pong', 'welcome-from-server');
|
||||||
|
const s2 = server.sendRule(identifier, 'other_rule', 'other-from-server');
|
||||||
|
console.log(`[server-test] sendRule results: test_pong=${s1} other_rule=${s2}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[server-test] registered test_ping rule and onClientAuthenticated callback');
|
||||||
|
}
|
||||||
13
tests/docker/server-test-plugin/openclaw.plugin.json
Normal file
13
tests/docker/server-test-plugin/openclaw.plugin.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "yonexus-server-test",
|
||||||
|
"name": "Yonexus Server Test Plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Test plugin for Yonexus.Server rule routing",
|
||||||
|
"entry": "./index.mjs",
|
||||||
|
"permissions": [],
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
tests/docker/server/Dockerfile
Normal file
40
tests/docker/server/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Build context: repo root (Yonexus/)
|
||||||
|
# ── Stage 1: compile ──────────────────────────────────────────────────────────
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Server imports Yonexus.Protocol and Yonexus.Client/crypto — all needed for tsc
|
||||||
|
COPY Yonexus.Protocol/src ./Yonexus.Protocol/src
|
||||||
|
COPY Yonexus.Client/plugin/crypto ./Yonexus.Client/plugin/crypto
|
||||||
|
|
||||||
|
COPY Yonexus.Server/package.json ./Yonexus.Server/
|
||||||
|
COPY Yonexus.Server/package-lock.json ./Yonexus.Server/
|
||||||
|
COPY Yonexus.Server/tsconfig.json ./Yonexus.Server/
|
||||||
|
COPY Yonexus.Server/plugin ./Yonexus.Server/plugin
|
||||||
|
|
||||||
|
WORKDIR /build/Yonexus.Server
|
||||||
|
RUN npm ci
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Stage 2: runtime ─────────────────────────────────────────────────────────
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
|
||||||
|
RUN npm install -g openclaw@2026.4.9
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Layout expected by install.mjs: repoRoot = /app, sourceDist = /app/dist
|
||||||
|
COPY --from=builder /build/Yonexus.Server/dist ./dist
|
||||||
|
COPY --from=builder /build/Yonexus.Server/node_modules ./node_modules
|
||||||
|
COPY Yonexus.Server/package.json ./package.json
|
||||||
|
COPY Yonexus.Server/plugin/openclaw.plugin.json ./plugin/openclaw.plugin.json
|
||||||
|
COPY Yonexus.Server/scripts/install.mjs ./scripts/install.mjs
|
||||||
|
|
||||||
|
COPY tests/docker/server-test-plugin /app/server-test-plugin
|
||||||
|
COPY tests/docker/server/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 8787
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
71
tests/docker/server/entrypoint.sh
Normal file
71
tests/docker/server/entrypoint.sh
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
: "${CLIENT_IDENTIFIER:?CLIENT_IDENTIFIER is required}"
|
||||||
|
: "${NOTIFY_BOT_TOKEN:?NOTIFY_BOT_TOKEN is required}"
|
||||||
|
: "${ADMIN_USER_ID:?ADMIN_USER_ID is required}"
|
||||||
|
|
||||||
|
STATE_DIR=/app/.openclaw-state
|
||||||
|
PLUGIN_DIR="$STATE_DIR/plugins/Yonexus.Server"
|
||||||
|
TEST_PLUGIN_DIR="$STATE_DIR/plugins/yonexus-server-test"
|
||||||
|
SERVER_WS_URL="${PUBLIC_WS_URL:-ws://yonexus-server:8787}"
|
||||||
|
|
||||||
|
# Install plugin dist + manifest into isolated state directory
|
||||||
|
node /app/scripts/install.mjs --install --openclaw-profile-path "$STATE_DIR"
|
||||||
|
# Symlink node_modules so bare-module imports (e.g. ws) resolve from plugin dir
|
||||||
|
ln -sf /app/node_modules "$PLUGIN_DIR/node_modules"
|
||||||
|
|
||||||
|
# Install test plugin (plain .mjs, no compilation needed)
|
||||||
|
mkdir -p "$TEST_PLUGIN_DIR"
|
||||||
|
cp /app/server-test-plugin/index.mjs "$TEST_PLUGIN_DIR/"
|
||||||
|
cp /app/server-test-plugin/openclaw.plugin.json "$TEST_PLUGIN_DIR/"
|
||||||
|
|
||||||
|
# Write openclaw config — plugin id is "yonexus-server" per openclaw.plugin.json
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
cat > "$STATE_DIR/openclaw.json" << EOF
|
||||||
|
{
|
||||||
|
"meta": { "lastTouchedVersion": "2026.4.9" },
|
||||||
|
"gateway": { "bind": "loopback" },
|
||||||
|
"agents": { "defaults": { "workspace": "$STATE_DIR/workspace" } },
|
||||||
|
"plugins": {
|
||||||
|
"allow": ["yonexus-server", "yonexus-server-test"],
|
||||||
|
"load": { "paths": ["$PLUGIN_DIR", "$TEST_PLUGIN_DIR"] },
|
||||||
|
"installs": {
|
||||||
|
"yonexus-server": {
|
||||||
|
"source": "path",
|
||||||
|
"sourcePath": "$PLUGIN_DIR",
|
||||||
|
"installPath": "$PLUGIN_DIR",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"installedAt": "2026-04-10T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
"yonexus-server-test": {
|
||||||
|
"source": "path",
|
||||||
|
"sourcePath": "$TEST_PLUGIN_DIR",
|
||||||
|
"installPath": "$TEST_PLUGIN_DIR",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"installedAt": "2026-04-10T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entries": {
|
||||||
|
"yonexus-server": {
|
||||||
|
"enabled": true,
|
||||||
|
"config": {
|
||||||
|
"followerIdentifiers": ["$CLIENT_IDENTIFIER"],
|
||||||
|
"notifyBotToken": "$NOTIFY_BOT_TOKEN",
|
||||||
|
"adminUserId": "$ADMIN_USER_ID",
|
||||||
|
"listenHost": "0.0.0.0",
|
||||||
|
"listenPort": 8787,
|
||||||
|
"publicWsUrl": "$SERVER_WS_URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yonexus-server-test": {
|
||||||
|
"enabled": true,
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
export OPENCLAW_STATE_DIR="$STATE_DIR"
|
||||||
|
exec openclaw gateway run --allow-unconfigured
|
||||||
Reference in New Issue
Block a user