mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 23:40:20 +00:00
test(plugins): add kitchen sink rpc docker lane
This commit is contained in:
@@ -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",
|
||||
|
||||
65
scripts/e2e/kitchen-sink-rpc-docker.sh
Executable file
65
scripts/e2e/kitchen-sink-rpc-docker.sh
Executable 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"
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user