test(plugins): add kitchen sink rpc docker lane

This commit is contained in:
Vincent Koc
2026-05-22 13:02:20 +02:00
parent 2b396131e4
commit 6f6da5f5ba
7 changed files with 163 additions and 17 deletions

View File

@@ -1622,6 +1622,7 @@
"test:docker:e2e-build": "bash scripts/e2e/build-image.sh",
"test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
"test:docker:kitchen-sink-plugin": "bash scripts/e2e/kitchen-sink-plugin-docker.sh",
"test:docker:kitchen-sink-rpc": "bash scripts/e2e/kitchen-sink-rpc-docker.sh",
"test:docker:live-acp-bind": "bash scripts/test-live-acp-bind-docker.sh",
"test:docker:live-acp-bind:claude": "OPENCLAW_LIVE_ACP_BIND_AGENT=claude bash scripts/test-live-acp-bind-docker.sh",
"test:docker:live-acp-bind:codex": "OPENCLAW_LIVE_ACP_BIND_AGENT=codex bash scripts/test-live-acp-bind-docker.sh",

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-kitchen-sink-rpc-e2e" OPENCLAW_KITCHEN_SINK_RPC_E2E_IMAGE)"
MAX_MEMORY_MIB="${OPENCLAW_KITCHEN_SINK_MAX_MEMORY_MIB:-2048}"
MAX_CPU_PERCENT="${OPENCLAW_KITCHEN_SINK_MAX_CPU_PERCENT:-1200}"
CONTAINER_NAME="openclaw-kitchen-sink-rpc-e2e-$$"
RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-kitchen-sink-rpc.XXXXXX")"
STATS_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-kitchen-sink-rpc-stats.XXXXXX")"
cleanup() {
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
rm -f "$RUN_LOG" "$STATS_LOG"
}
trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" kitchen-sink-rpc
DOCKER_ENV_ARGS=(
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0
-e OPENCLAW_ENTRY=/app/openclaw.mjs
)
for env_name in \
OPENCLAW_KITCHEN_SINK_NPM_SPEC \
OPENCLAW_KITCHEN_SINK_PLUGIN_ID \
OPENCLAW_KITCHEN_SINK_PERSONALITY \
OPENCLAW_KITCHEN_SINK_RPC_READY_MS \
OPENCLAW_KITCHEN_SINK_RPC_COMMAND_MS \
OPENCLAW_KITCHEN_SINK_RPC_INSTALL_MS \
OPENCLAW_KITCHEN_SINK_RPC_CALL_MS \
OPENCLAW_KITCHEN_SINK_MAX_RSS_MIB; do
env_value="${!env_name:-}"
if [[ -n "$env_value" && "$env_value" != "undefined" && "$env_value" != "null" ]]; then
DOCKER_ENV_ARGS+=(-e "$env_name")
fi
done
echo "Running kitchen-sink RPC Docker E2E..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker_e2e_harness_mount_args
docker run --name "$CONTAINER_NAME" "${DOCKER_E2E_HARNESS_ARGS[@]}" "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" \
node --import tsx scripts/e2e/kitchen-sink-rpc-walk.mjs >"$RUN_LOG" 2>&1 &
docker_pid="$!"
while kill -0 "$docker_pid" 2>/dev/null; do
if docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
docker stats --no-stream --format '{{json .}}' "$CONTAINER_NAME" >>"$STATS_LOG" 2>/dev/null || true
fi
sleep 2
done
set +e
wait "$docker_pid"
run_status="$?"
set -e
cat "$RUN_LOG"
node scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs "$STATS_LOG" "$MAX_MEMORY_MIB" "$MAX_CPU_PERCENT" kitchen-sink-rpc
exit "$run_status"

View File

@@ -5,7 +5,6 @@ import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { createPnpmRunnerSpawnSpec } from "../pnpm-runner.mjs";
const PLUGIN_SPEC =
process.env.OPENCLAW_KITCHEN_SINK_NPM_SPEC || "npm:@openclaw/kitchen-sink@latest";
@@ -130,7 +129,7 @@ function runCommand(command, args, options = {}) {
}
async function runOpenClaw(runner, args, env, options = {}) {
const command = resolveOpenClawCommand(runner, args, env, {
const command = await resolveOpenClawCommand(runner, args, env, {
stdio: ["ignore", "pipe", "pipe"],
});
return runCommand(command.command, command.args, {
@@ -140,8 +139,9 @@ async function runOpenClaw(runner, args, env, options = {}) {
});
}
function resolveOpenClawCommand(runner, args, env, options = {}) {
async function resolveOpenClawCommand(runner, args, env, options = {}) {
if (runner.pnpm) {
const { createPnpmRunnerSpawnSpec } = await import("../pnpm-runner.mjs");
return createPnpmRunnerSpawnSpec({
env,
pnpmArgs: [...runner.baseArgs, ...args],
@@ -242,12 +242,28 @@ async function rpcCall(method, params, options) {
}
async function loadCallGatewayModule() {
callGatewayModulePromise ??= import(
pathToFileURL(path.join(process.cwd(), "src/gateway/call.ts"))
);
callGatewayModulePromise ??= importCallGatewayModule();
return callGatewayModulePromise;
}
async function importCallGatewayModule() {
const distDir = path.join(process.cwd(), "dist");
if (fs.existsSync(distDir)) {
const candidates = fs
.readdirSync(distDir)
.filter((name) => /^call(?:\.runtime)?-[A-Za-z0-9_-]+\.js$/u.test(name))
.toSorted((left, right) => left.localeCompare(right));
for (const name of candidates) {
const module = await import(pathToFileURL(path.join(distDir, name)).href);
if (typeof module.callGateway === "function") {
return module;
}
}
throw new Error(`unable to find callGateway export in dist (${candidates.join(", ")})`);
}
return import(pathToFileURL(path.join(process.cwd(), "src/gateway/call.ts")).href);
}
async function retryRpcCall(method, params, options) {
const started = Date.now();
let lastError;
@@ -347,18 +363,11 @@ function configureKitchenSink(env, port) {
writeJson(configPath, config);
}
function startGateway(runner, port, env, logPath) {
async function startGateway(runner, port, env, logPath) {
const log = fs.openSync(logPath, "w");
const command = resolveOpenClawCommand(
const command = await resolveOpenClawCommand(
runner,
[
"gateway",
"--port",
String(port),
"--bind",
"loopback",
"--allow-unconfigured",
],
["gateway", "--port", String(port), "--bind", "loopback", "--allow-unconfigured"],
env,
{
stdio: ["ignore", log, log],
@@ -599,7 +608,7 @@ async function main() {
];
assertIncludesAny(inspectProviders, EXPECTED_PROVIDERS, "plugins inspect providers");
const child = startGateway(runner, port, env, logPath);
const child = await startGateway(runner, port, env, logPath);
try {
await waitForGatewayReady(child, port, logPath);
const initialSample = await sampleProcess(child.pid);

View File

@@ -224,6 +224,19 @@ function liveCodexNpmPluginLane() {
);
}
function kitchenSinkRpcLane() {
return serviceLane(
"kitchen-sink-rpc",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:kitchen-sink-rpc",
{
resources: ["npm"],
stateScenario: "empty",
timeoutMs: 15 * 60 * 1000,
weight: 3,
},
);
}
export const mainLanes = [
liveLane("live-models", liveDockerScriptCommand("test-live-models-docker.sh"), {
providers: ["claude-cli", "google-gemini-cli"],
@@ -403,6 +416,7 @@ export const mainLanes = [
stateScenario: "empty",
weight: 3,
}),
kitchenSinkRpcLane(),
...bundledPluginInstallUninstallLanes,
lane(
"plugins-offline",
@@ -587,6 +601,7 @@ const releasePathPluginRuntimeLanes = [
weight: 3,
},
),
kitchenSinkRpcLane(),
serviceLane(
"openai-web-search-minimal",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal",
@@ -613,6 +628,7 @@ const releasePathPluginRuntimeServiceLanes = [
weight: 3,
},
),
kitchenSinkRpcLane(),
serviceLane(
"openai-web-search-minimal",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal",

View File

@@ -15,6 +15,7 @@ export const PLUGIN_PRERELEASE_REQUIRED_SURFACES = Object.freeze([
"npm-registry-plugin",
"clawhub-registry-plugin",
"resource-guardrails",
"plugin-gateway-rpc",
"live-ish-availability",
]);
@@ -70,6 +71,18 @@ const pluginPrereleaseDockerLanes = Object.freeze([
"resource-guardrails",
],
},
{
lane: "kitchen-sink-rpc",
surfaces: [
"external-plugins",
"sdk-compatibility",
"gateway-bootstrap",
"status-diagnostics",
"npm-registry-plugin",
"resource-guardrails",
"plugin-gateway-rpc",
],
},
{
lane: "plugin-update",
surfaces: ["package-artifact", "update-no-op"],

View File

@@ -365,6 +365,16 @@ describe("scripts/lib/docker-e2e-plan", () => {
stateScenario: "empty",
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:kitchen-sink-rpc",
imageKind: "functional",
live: false,
name: "kitchen-sink-rpc",
resources: ["docker", "service", "npm"],
stateScenario: "empty",
timeoutMs: 900_000,
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal",
imageKind: "functional",
@@ -504,6 +514,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
"plugins",
...bundledPluginSweepLanes,
"cron-mcp-cleanup",
"kitchen-sink-rpc",
"openai-web-search-minimal",
"live-plugin-tool",
"openwebui",
@@ -512,6 +523,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
"plugins",
...bundledPluginSweepLanes,
"cron-mcp-cleanup",
"kitchen-sink-rpc",
"openai-web-search-minimal",
"live-plugin-tool",
"plugin-update",
@@ -766,6 +778,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
"plugin-update",
"plugins",
"kitchen-sink-plugin",
"kitchen-sink-rpc",
"bundled-plugin-install-uninstall-0",
"commitments-safety",
"update-channel-switch",
@@ -792,6 +805,7 @@ describe("scripts/lib/docker-e2e-plan", () => {
{ name: "plugin-update", stateScenario: "empty" },
{ name: "plugins", stateScenario: "empty" },
{ name: "kitchen-sink-plugin", stateScenario: "empty" },
{ name: "kitchen-sink-rpc", stateScenario: "empty" },
{ name: "bundled-plugin-install-uninstall-0", stateScenario: "empty" },
{ name: "commitments-safety", stateScenario: "empty" },
{ name: "update-channel-switch", stateScenario: "update-stable" },

View File

@@ -51,6 +51,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
"plugins-offline",
"plugins",
"kitchen-sink-plugin",
"kitchen-sink-rpc",
"plugin-update",
"config-reload",
"gateway-network",
@@ -162,6 +163,33 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
expect(sweepScript).toContain("scan_logs_for_unexpected_errors");
});
it("keeps kitchen-sink RPC coverage package-backed and resource-guarded", () => {
const lane = getDockerLane("kitchen-sink-rpc");
const script = readFileSync("scripts/e2e/kitchen-sink-rpc-docker.sh", "utf8");
const walkScript = readFileSync("scripts/e2e/kitchen-sink-rpc-walk.mjs", "utf8");
expect(lane).toEqual({
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:kitchen-sink-rpc",
e2eImageKind: "functional",
live: false,
name: "kitchen-sink-rpc",
resources: ["service", "npm"],
retryPatterns: [],
retries: 0,
stateScenario: "empty",
timeoutMs: 900000,
weight: 3,
});
expect(script).toContain("OPENCLAW_ENTRY=/app/openclaw.mjs");
expect(script).toContain("docker stats --no-stream");
expect(script).toContain("scripts/e2e/kitchen-sink-rpc-walk.mjs");
expect(walkScript).toContain("commands.list");
expect(walkScript).toContain("tools.invoke");
expect(walkScript).toContain("tts.providers");
expect(walkScript).toContain("plugins.uiDescriptors");
expect(walkScript).toContain("^call(?:\\.runtime)?");
});
it("keeps the generic plugin Docker lane as an external install contract canary", () => {
const lane = getDockerLane("plugins");
const sweepScript = readFileSync("scripts/e2e/lib/plugins/sweep.sh", "utf8");