mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(e2e): remove stale proof scripts (#112776)
This commit is contained in:
committed by
GitHub
parent
c4bd3f5b2e
commit
62c148d7d0
@@ -44,8 +44,6 @@ const repositoryScriptEntries = [
|
||||
"scripts/e2e/lib/openai-chat-tools/client.mjs!",
|
||||
"scripts/e2e/lib/openai-chat-tools/write-config.mjs!",
|
||||
"scripts/e2e/lib/package-git-fixture.mjs!",
|
||||
"scripts/e2e/lib/parallels-package/build-info-commit.mjs!",
|
||||
"scripts/e2e/lib/parallels-package/log-progress-extract.mjs!",
|
||||
"scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs!",
|
||||
"scripts/e2e/lib/plugin-update/registry-server.mjs!",
|
||||
"scripts/e2e/lib/plugins/npm-registry-server.mjs!",
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Definition:
|
||||
# Docker/package E2E proof for local channel plugin trust gating. The host
|
||||
# mode builds or reuses the functional Docker image, then runs the container
|
||||
# mode against the installed OpenClaw package.
|
||||
#
|
||||
# Parameters:
|
||||
# --container: run the in-container scenario. Host mode is the default.
|
||||
# OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_IMAGE: override the Docker image name.
|
||||
# OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_SKIP_BUILD=1: reuse/pull the image.
|
||||
#
|
||||
# Outputs:
|
||||
# stdout logs each case and prints "Channel plugin trust Docker E2E passed."
|
||||
# Exit 0 means both representative package-environment cases passed.
|
||||
# Exit non-zero means the package build, Docker run, or trust assertion failed.
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/e2e/channel-plugin-trust-docker.sh [--container]
|
||||
|
||||
Description:
|
||||
Proves the packaged OpenClaw CLI enforces local channel plugin trust for
|
||||
plugins.load.paths entries in a clean Docker/package environment.
|
||||
|
||||
Options:
|
||||
--container Run the in-container scenario. Used by the host wrapper.
|
||||
-h, --help Show this help.
|
||||
|
||||
Environment:
|
||||
OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_IMAGE Override Docker image name.
|
||||
OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_SKIP_BUILD Reuse/pull image instead of building.
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64 Required in --container mode.
|
||||
|
||||
Outputs:
|
||||
Prints case progress and PASS lines to stdout. Exits non-zero on assertion
|
||||
failure and leaves the failing command output in the container log.
|
||||
|
||||
Examples:
|
||||
bash scripts/e2e/channel-plugin-trust-docker.sh
|
||||
OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_SKIP_BUILD=1 bash scripts/e2e/channel-plugin-trust-docker.sh
|
||||
EOF
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
run_openclaw() {
|
||||
if command -v openclaw >/dev/null 2>&1; then
|
||||
openclaw "$@"
|
||||
return
|
||||
fi
|
||||
if [ -f /app/openclaw.mjs ]; then
|
||||
node /app/openclaw.mjs "$@"
|
||||
return
|
||||
fi
|
||||
echo "openclaw CLI not found in Docker image" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
write_load_paths_fixture() {
|
||||
local plugin_dir="${1:?missing plugin dir}"
|
||||
local origin="${2:?missing origin}"
|
||||
local plugin_id="e2e-load-paths-shadow"
|
||||
local channel_id="e2e-load-paths"
|
||||
mkdir -p "$plugin_dir"
|
||||
|
||||
cat >"$plugin_dir/package.json" <<EOF
|
||||
{
|
||||
"name": "@openclaw-e2e/$plugin_id",
|
||||
"version": "0.0.0-e2e",
|
||||
"private": true,
|
||||
"openclaw": {
|
||||
"extensions": ["./index.cjs"],
|
||||
"setupEntry": "./setup-entry.cjs",
|
||||
"channel": {
|
||||
"id": "$channel_id",
|
||||
"label": "E2E Load Paths",
|
||||
"selectionLabel": "E2E Load Paths",
|
||||
"docsPath": "/channels/$channel_id",
|
||||
"blurb": "Docker E2E local trust fixture."
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat >"$plugin_dir/openclaw.plugin.json" <<EOF
|
||||
{
|
||||
"id": "$plugin_id",
|
||||
"name": "E2E load-paths Shadow",
|
||||
"description": "Docker E2E local trust fixture.",
|
||||
"activation": { "onStartup": false },
|
||||
"channels": ["$channel_id"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat >"$plugin_dir/index.cjs" <<EOF
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const importMarker = process.env.PLUGINTRUST_IMPORT_MARKER;
|
||||
const registerMarker = process.env.PLUGINTRUST_REGISTER_MARKER;
|
||||
const canary = process.env.PLUGINTRUST_CANARY ?? "<no-canary>";
|
||||
function writeMarker(target, payload) {
|
||||
if (!target) return;
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, payload, "utf8");
|
||||
}
|
||||
writeMarker(importMarker, "imported|origin=$origin|canary=" + canary + "\\n");
|
||||
module.exports = {
|
||||
id: "$plugin_id",
|
||||
register(api) {
|
||||
writeMarker(registerMarker, "registered|origin=$origin|canary=" + canary + "\\n");
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "$channel_id",
|
||||
meta: {
|
||||
id: "$channel_id",
|
||||
label: "E2E Load Paths",
|
||||
selectionLabel: "E2E Load Paths",
|
||||
docsPath: "/channels/$channel_id",
|
||||
blurb: "Docker E2E local trust fixture.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
EOF
|
||||
|
||||
cat >"$plugin_dir/setup-entry.cjs" <<EOF
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const importMarker = process.env.PLUGINTRUST_SETUP_IMPORT_MARKER;
|
||||
const registerMarker = process.env.PLUGINTRUST_SETUP_REGISTER_MARKER;
|
||||
const canary = process.env.PLUGINTRUST_CANARY ?? "<no-canary>";
|
||||
function writeMarker(target, payload) {
|
||||
if (!target) return;
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, payload, "utf8");
|
||||
}
|
||||
writeMarker(importMarker, "setup-imported|origin=$origin|canary=" + canary + "\\n");
|
||||
module.exports = {
|
||||
plugin: {
|
||||
id: "$channel_id",
|
||||
meta: {
|
||||
id: "$channel_id",
|
||||
label: "E2E Load Paths setup",
|
||||
selectionLabel: "E2E Load Paths setup",
|
||||
docsPath: "/channels/$channel_id",
|
||||
blurb: "Docker E2E local trust setup fixture.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
setup: {
|
||||
validateInput: ({ input }) => {
|
||||
writeMarker(
|
||||
registerMarker,
|
||||
"setup-registered|origin=$origin|canary=" + canary + "|token=" + (input?.token ?? "<no-token>") + "\\n",
|
||||
);
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg }) => cfg,
|
||||
},
|
||||
},
|
||||
};
|
||||
EOF
|
||||
}
|
||||
|
||||
write_case_config() {
|
||||
local plugin_dir="${1:?missing plugin dir}"
|
||||
local trusted="${2:?missing trusted flag}"
|
||||
local plugin_id="e2e-load-paths-shadow"
|
||||
mkdir -p "$(dirname "$OPENCLAW_CONFIG_PATH")"
|
||||
if [ "$trusted" = "1" ]; then
|
||||
cat >"$OPENCLAW_CONFIG_PATH" <<EOF
|
||||
{
|
||||
"plugins": {
|
||||
"enabled": true,
|
||||
"allow": ["$plugin_id"],
|
||||
"load": {
|
||||
"paths": ["$plugin_dir"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat >"$OPENCLAW_CONFIG_PATH" <<EOF
|
||||
{
|
||||
"plugins": {
|
||||
"enabled": true,
|
||||
"load": {
|
||||
"paths": ["$plugin_dir"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local case_id="${1:?missing case id}"
|
||||
local trusted="${2:?missing trusted flag}"
|
||||
local scratch
|
||||
scratch="$(mktemp -d "/tmp/openclaw-channel-plugin-trust-$case_id.XXXXXX")"
|
||||
local plugin_dir="$scratch/e2e-load-paths-shadow"
|
||||
local marker_dir="$scratch/markers"
|
||||
local stdout_file="$scratch/stdout.log"
|
||||
local stderr_file="$scratch/stderr.log"
|
||||
local canary="$case_id-canary"
|
||||
mkdir -p "$marker_dir"
|
||||
|
||||
write_load_paths_fixture "$plugin_dir" "config"
|
||||
write_case_config "$plugin_dir" "$trusted"
|
||||
|
||||
echo "[CASE $case_id] plugins.load.paths trusted=$trusted"
|
||||
set +e
|
||||
PLUGINTRUST_IMPORT_MARKER="$marker_dir/import.marker" \
|
||||
PLUGINTRUST_REGISTER_MARKER="$marker_dir/register.marker" \
|
||||
PLUGINTRUST_SETUP_IMPORT_MARKER="$marker_dir/setup-import.marker" \
|
||||
PLUGINTRUST_SETUP_REGISTER_MARKER="$marker_dir/setup-register.marker" \
|
||||
PLUGINTRUST_CANARY="$canary" \
|
||||
run_openclaw channels add --channel e2e-load-paths --token "$canary" \
|
||||
>"$stdout_file" 2>"$stderr_file"
|
||||
local status=$?
|
||||
set -e
|
||||
|
||||
if [ "$trusted" = "1" ] && [ "$status" -ne 0 ]; then
|
||||
echo "Expected trusted case to succeed; exit=$status" >&2
|
||||
cat "$stderr_file" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$trusted" = "1" ]; then
|
||||
for marker in setup-import setup-register; do
|
||||
local marker_path="$marker_dir/$marker.marker"
|
||||
if [ ! -f "$marker_path" ]; then
|
||||
echo "Expected $marker marker for trusted case" >&2
|
||||
cat "$stderr_file" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -qF "canary=$canary" "$marker_path"; then
|
||||
echo "$marker marker did not include canary $canary" >&2
|
||||
cat "$marker_path" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "PASS: $case_id trusted load-paths setup entry executed"
|
||||
else
|
||||
for marker in setup-import setup-register import register; do
|
||||
if [ -e "$marker_dir/$marker.marker" ]; then
|
||||
echo "Expected $marker marker to be absent for untrusted case" >&2
|
||||
cat "$marker_dir/$marker.marker" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "PASS: $case_id untrusted load-paths setup entry blocked"
|
||||
fi
|
||||
}
|
||||
|
||||
run_container() {
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
|
||||
export OPENCLAW_WORKSPACE_DIR="$HOME/.openclaw/workspace"
|
||||
|
||||
run_openclaw --version
|
||||
run_case untrusted-load-paths 0
|
||||
run_case trusted-load-paths 1
|
||||
echo "Channel plugin trust Docker E2E passed."
|
||||
}
|
||||
|
||||
run_host() {
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
local image_name
|
||||
image_name="$(
|
||||
docker_e2e_resolve_image \
|
||||
"openclaw-channel-plugin-trust-e2e:local" \
|
||||
OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_IMAGE
|
||||
)"
|
||||
local skip_build="${OPENCLAW_CHANNEL_PLUGIN_TRUST_E2E_SKIP_BUILD:-0}"
|
||||
docker_e2e_build_or_reuse "$image_name" channel-plugin-trust "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$skip_build"
|
||||
|
||||
local state_script_b64
|
||||
state_script_b64="$(docker_e2e_test_state_shell_b64 channel-plugin-trust minimal)"
|
||||
echo "Running channel plugin trust Docker E2E..."
|
||||
docker_e2e_run_logged_print_with_harness \
|
||||
channel-plugin-trust \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$state_script_b64" \
|
||||
"$image_name" \
|
||||
bash scripts/e2e/channel-plugin-trust-docker.sh --container
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h | --help)
|
||||
usage
|
||||
;;
|
||||
--container)
|
||||
run_container
|
||||
;;
|
||||
"")
|
||||
run_host
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
echo >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
parallels_macos_resolve_desktop_user() {
|
||||
local vm_name="$1"
|
||||
local user
|
||||
user="$(prlctl exec "$vm_name" /usr/bin/stat -f '%Su' /dev/console 2>/dev/null | tr -d '\r' | tail -n 1 || true)"
|
||||
if [[ "$user" =~ ^[A-Za-z0-9._-]+$ && "$user" != "root" && "$user" != "loginwindow" ]]; then
|
||||
printf '%s\n' "$user"
|
||||
return 0
|
||||
fi
|
||||
prlctl exec "$vm_name" /usr/bin/dscl . -list /Users NFSHomeDirectory 2>/dev/null \
|
||||
| tr -d '\r' \
|
||||
| awk '$2 ~ /^\/Users\// && $1 !~ /^_/ && $1 != "Shared" && $1 != ".localized" { print $1; exit }'
|
||||
}
|
||||
|
||||
parallels_macos_resolve_desktop_home() {
|
||||
local vm_name="$1"
|
||||
local user="$2"
|
||||
local home
|
||||
home="$(
|
||||
prlctl exec "$vm_name" /usr/bin/dscl . -read "/Users/$user" NFSHomeDirectory 2>/dev/null \
|
||||
| tr -d '\r' \
|
||||
| awk '/NFSHomeDirectory:/ { print $2; exit }'
|
||||
)"
|
||||
if [[ -n "$home" ]]; then
|
||||
printf '%s\n' "$home"
|
||||
else
|
||||
printf '/Users/%s\n' "$user"
|
||||
fi
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
parallels_package_acquire_build_lock() {
|
||||
local lock_dir="$1"
|
||||
local owner_pid=""
|
||||
while ! mkdir "$lock_dir" 2>/dev/null; do
|
||||
if [[ -f "$lock_dir/pid" ]]; then
|
||||
owner_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
|
||||
if [[ -n "$owner_pid" ]] && ! kill -0 "$owner_pid" >/dev/null 2>&1; then
|
||||
printf 'warn: Removing stale Parallels build lock\n' >&2
|
||||
rm -rf "$lock_dir"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
printf '%s\n' "$$" >"$lock_dir/pid"
|
||||
}
|
||||
|
||||
parallels_package_release_build_lock() {
|
||||
local lock_dir="$1"
|
||||
if [[ -d "$lock_dir" ]]; then
|
||||
rm -rf "$lock_dir"
|
||||
fi
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Validates build-info commit metadata for Parallels package E2E scenarios.
|
||||
import fs from "node:fs";
|
||||
|
||||
const path = "dist/build-info.json";
|
||||
if (!fs.existsSync(path)) {
|
||||
console.log("");
|
||||
} else {
|
||||
const buildInfo = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
console.log(buildInfo.commit ?? "");
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Extracts progress markers from Parallels package E2E logs.
|
||||
import fs from "node:fs";
|
||||
import { readTextFileTail } from "../text-file-utils.mjs";
|
||||
|
||||
const LOG_PROGRESS_TAIL_BYTES = 256 * 1024;
|
||||
|
||||
const [logPath] = process.argv.slice(2);
|
||||
if (!logPath || !fs.existsSync(logPath)) {
|
||||
console.log("");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const text = readTextFileTail(logPath, LOG_PROGRESS_TAIL_BYTES);
|
||||
const lines = text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const reversed = lines.toReversed();
|
||||
|
||||
const progress = reversed.find((line) => line.startsWith("==> "));
|
||||
const warning = reversed.find((line) => line.startsWith("warn:") || line.startsWith("error:"));
|
||||
console.log(progress?.slice(4).trim() ?? warning ?? lines.at(-1)?.slice(0, 240) ?? "");
|
||||
@@ -1,23 +0,0 @@
|
||||
export type PondGatewayRpcOptions = {
|
||||
url: string;
|
||||
token: string;
|
||||
scopes: string[];
|
||||
openTimeoutMs?: number;
|
||||
webSocketFactory?: (target: string) => unknown;
|
||||
};
|
||||
|
||||
export type PondGatewayRpcRequestOptions = {
|
||||
expectFinal?: boolean;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export declare class PondGatewayRpc {
|
||||
constructor(options: PondGatewayRpcOptions);
|
||||
connect(): Promise<void>;
|
||||
request(
|
||||
method: string,
|
||||
params?: unknown,
|
||||
options?: PondGatewayRpcRequestOptions,
|
||||
): Promise<unknown>;
|
||||
close(): void;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// Gateway RPC client used by the Pond E2E verifier.
|
||||
import { WebSocket } from "ws";
|
||||
import { waitForWebSocketOpen } from "./websocket-open.mjs";
|
||||
|
||||
function asError(value) {
|
||||
return value instanceof Error ? value : new Error(String(value));
|
||||
}
|
||||
|
||||
function formatCloseReason(code, reason) {
|
||||
const text = reason instanceof Uint8Array ? Buffer.from(reason).toString("utf8") : String(reason);
|
||||
return text ? ` (${code}): ${text}` : ` (${code})`;
|
||||
}
|
||||
|
||||
export class PondGatewayRpc {
|
||||
constructor({
|
||||
url,
|
||||
token,
|
||||
scopes,
|
||||
openTimeoutMs = 15_000,
|
||||
webSocketFactory = (target) => new WebSocket(target),
|
||||
}) {
|
||||
this.url = url;
|
||||
this.token = token;
|
||||
this.scopes = scopes;
|
||||
this.openTimeoutMs = openTimeoutMs;
|
||||
this.webSocketFactory = webSocketFactory;
|
||||
this.pending = new Map();
|
||||
this.nextId = 1;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this.ws = this.webSocketFactory(this.url);
|
||||
this.ws.on("message", (data) => this.onMessage(data));
|
||||
// These listeners outlive the open wait so post-handshake failures reject RPCs
|
||||
// instead of surfacing as uncaught EventEmitter errors.
|
||||
this.ws.on("error", (error) => this.rejectPending(asError(error)));
|
||||
this.ws.on("close", (code, reason) => {
|
||||
this.rejectPending(new Error(`Gateway socket closed${formatCloseReason(code, reason)}`));
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForWebSocketOpen(
|
||||
this.ws,
|
||||
this.openTimeoutMs,
|
||||
`Gateway connect timeout: ${this.url}`,
|
||||
);
|
||||
await this.request("connect", {
|
||||
minProtocol: 1,
|
||||
maxProtocol: 99,
|
||||
client: {
|
||||
id: "gateway-client",
|
||||
displayName: "Pond proof verifier",
|
||||
version: "0.0.0",
|
||||
platform: process.platform,
|
||||
mode: "backend",
|
||||
},
|
||||
auth: { token: this.token },
|
||||
role: "operator",
|
||||
scopes: this.scopes,
|
||||
});
|
||||
} catch (error) {
|
||||
// Callers only receive the client after connect succeeds, so failed setup
|
||||
// must close its socket here or the E2E process can remain alive.
|
||||
this.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
rejectPending(error) {
|
||||
this.terminalError ??= error;
|
||||
for (const pending of this.pending.values()) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(this.terminalError);
|
||||
}
|
||||
this.pending.clear();
|
||||
}
|
||||
|
||||
onMessage(data) {
|
||||
let frame;
|
||||
try {
|
||||
frame = JSON.parse(String(data));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (frame?.type !== "res" || typeof frame.id !== "string") {
|
||||
return;
|
||||
}
|
||||
const pending = this.pending.get(frame.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
if (pending.expectFinal && frame.payload?.status === "accepted") {
|
||||
return;
|
||||
}
|
||||
this.pending.delete(frame.id);
|
||||
clearTimeout(pending.timer);
|
||||
if (frame.ok) {
|
||||
pending.resolve(frame.payload);
|
||||
return;
|
||||
}
|
||||
pending.reject(new Error(frame.error?.message ?? `Gateway RPC failed: ${pending.method}`));
|
||||
}
|
||||
|
||||
request(method, params = {}, options = {}) {
|
||||
if (this.terminalError) {
|
||||
return Promise.reject(asError(this.terminalError));
|
||||
}
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return Promise.reject(new Error(`Gateway socket is not open for RPC: ${method}`));
|
||||
}
|
||||
|
||||
const id = `pond-proof-${this.nextId}`;
|
||||
this.nextId += 1;
|
||||
const timeoutMs = options.timeoutMs ?? 30_000;
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`Gateway RPC timeout: ${method}`));
|
||||
}, timeoutMs);
|
||||
this.pending.set(id, {
|
||||
method,
|
||||
expectFinal: options.expectFinal === true,
|
||||
resolve,
|
||||
reject,
|
||||
timer,
|
||||
});
|
||||
try {
|
||||
this.ws.send(JSON.stringify({ type: "req", id, method, params }), (error) => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
const pending = this.pending.get(id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
this.pending.delete(id);
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(asError(error));
|
||||
});
|
||||
} catch (error) {
|
||||
this.pending.delete(id);
|
||||
clearTimeout(timer);
|
||||
reject(asError(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.rejectPending(new Error("Gateway RPC client closed"));
|
||||
if (this.ws?.readyState === WebSocket.CONNECTING) {
|
||||
this.ws.terminate();
|
||||
return;
|
||||
}
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1737,10 +1737,6 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
"test/scripts/e2e-helper-env-limits.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/channel-plugin-trust-docker.sh",
|
||||
["test/scripts/docker-build-helper.test.ts", "test/scripts/test-projects.test.ts"],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/config-reload-source-docker.sh",
|
||||
[
|
||||
@@ -3671,18 +3667,10 @@ function resolveK8sManifestTargets(changedPath) {
|
||||
function resolveParallelsToolingTestTargets(changedPath) {
|
||||
if (
|
||||
!/^scripts\/e2e\/parallels\/[^/]+\.ts$/u.test(changedPath) &&
|
||||
!/^scripts\/e2e\/parallels-(?:linux|macos|npm-update|windows)-smoke\.sh$/u.test(changedPath) &&
|
||||
!/^scripts\/e2e\/lib\/parallels-package\/build-info-commit\.mjs$/u.test(changedPath) &&
|
||||
!/^scripts\/e2e\/lib\/parallels-(?:macos|package)-common\.sh$/u.test(changedPath)
|
||||
!/^scripts\/e2e\/parallels-(?:linux|macos|npm-update|windows)-smoke\.sh$/u.test(changedPath)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
/^scripts\/e2e\/lib\/parallels-package\/build-info-commit\.mjs$/u.test(changedPath) ||
|
||||
/^scripts\/e2e\/lib\/parallels-(?:macos|package)-common\.sh$/u.test(changedPath)
|
||||
) {
|
||||
return ["test/scripts/parallels-lib-helpers.test.ts"];
|
||||
}
|
||||
const targets = ["test/scripts/parallels-smoke-model.test.ts"];
|
||||
if (
|
||||
[
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
|
||||
|
||||
const BUILD_INFO_COMMIT_SCRIPT = path.resolve(
|
||||
"scripts/e2e/lib/parallels-package/build-info-commit.mjs",
|
||||
);
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function runBash(script: string, env: NodeJS.ProcessEnv = {}) {
|
||||
return spawnSync("/bin/bash", ["-c", script], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ...env },
|
||||
});
|
||||
}
|
||||
|
||||
describe("Parallels lib helpers", () => {
|
||||
it("reads build-info commit metadata from the current package cwd", () => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-parallels-build-info-");
|
||||
|
||||
const missingResult = spawnSync(process.execPath, [BUILD_INFO_COMMIT_SCRIPT], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env },
|
||||
});
|
||||
expect(missingResult.status).toBe(0);
|
||||
expect(missingResult.stdout).toBe("\n");
|
||||
|
||||
mkdirSync(path.join(root, "dist"));
|
||||
writeFileSync(
|
||||
path.join(root, "dist", "build-info.json"),
|
||||
`${JSON.stringify({ commit: "abc123" })}\n`,
|
||||
);
|
||||
const result = spawnSync(process.execPath, [BUILD_INFO_COMMIT_SCRIPT], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env },
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("abc123\n");
|
||||
});
|
||||
|
||||
it("reclaims stale package shell locks and releases current locks", () => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-parallels-package-lock-");
|
||||
const lockDir = path.join(root, "build.lock");
|
||||
const result = runBash(`
|
||||
set -euo pipefail
|
||||
source scripts/e2e/lib/parallels-package-common.sh
|
||||
lock_dir=${shellQuote(lockDir)}
|
||||
mkdir -p "$lock_dir"
|
||||
printf '%s\\n' 999999999 >"$lock_dir/pid"
|
||||
parallels_package_acquire_build_lock "$lock_dir"
|
||||
owner="$(cat "$lock_dir/pid")"
|
||||
parallels_package_release_build_lock "$lock_dir"
|
||||
printf 'owner=%s exists=%s\\n' "$owner" "$([[ -e "$lock_dir" ]] && echo yes || echo no)"
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toContain("warn: Removing stale Parallels build lock");
|
||||
expect(result.stdout).toMatch(/^owner=\d+ exists=no\n$/u);
|
||||
expect(result.stdout).not.toContain("owner=999999999");
|
||||
});
|
||||
|
||||
it("resolves macOS desktop users through prlctl fallbacks", () => {
|
||||
const root = makeTempDir(tempDirs, "openclaw-parallels-macos-common-");
|
||||
const binDir = path.join(root, "bin");
|
||||
const macHome = `${"/"}Users/alice`;
|
||||
mkdirSync(binDir);
|
||||
const prlctlShim = path.join(binDir, "prlctl");
|
||||
writeFileSync(
|
||||
prlctlShim,
|
||||
`#!/usr/bin/env bash
|
||||
args="$*"
|
||||
if [[ "$args" == *"/usr/bin/stat -f %Su /dev/console"* ]]; then
|
||||
printf 'loginwindow\\r\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$args" == *"/usr/bin/dscl . -list /Users NFSHomeDirectory"* ]]; then
|
||||
printf '_daemon /var/root\\r\\nShared %s\\r\\nalice %s\\r\\n' "${`${"/"}Users/Shared`}" "${macHome}"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$args" == *"-read ${macHome} NFSHomeDirectory"* ]]; then
|
||||
printf 'NFSHomeDirectory: %s\\r\\n' "${macHome}"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
`,
|
||||
);
|
||||
chmodSync(prlctlShim, 0o755);
|
||||
|
||||
const result = runBash(
|
||||
`
|
||||
set -euo pipefail
|
||||
source scripts/e2e/lib/parallels-macos-common.sh
|
||||
printf 'user=%s\\n' "$(parallels_macos_resolve_desktop_user macos-vm)"
|
||||
printf 'home=%s\\n' "$(parallels_macos_resolve_desktop_home macos-vm alice)"
|
||||
`,
|
||||
{ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe(`user=alice\nhome=${macHome}\n`);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
// Parallels Package Log Progress Extract tests cover parallels package log progress extract script behavior.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const SCRIPT_PATH = "scripts/e2e/lib/parallels-package/log-progress-extract.mjs";
|
||||
const tempRoots = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
function makeTempRoot(): string {
|
||||
return tempRoots.make("openclaw-parallels-progress-");
|
||||
}
|
||||
|
||||
function runExtract(logPath?: string) {
|
||||
return spawnSync(process.execPath, [SCRIPT_PATH, ...(logPath ? [logPath] : [])], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
describe("parallels package log progress extractor", () => {
|
||||
it("prints a blank status when the log is absent", () => {
|
||||
const result = runExtract(path.join(makeTempRoot(), "missing.log"));
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("\n");
|
||||
});
|
||||
|
||||
it("extracts the latest progress line from recent log output", () => {
|
||||
const logPath = path.join(makeTempRoot(), "phase.log");
|
||||
writeFileSync(logPath, "==> Build package\nwarn: transient\n==> Copy artifact\n");
|
||||
|
||||
const result = runExtract(logPath);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("Copy artifact\n");
|
||||
});
|
||||
|
||||
it("does not let stale progress hide recent warnings in long logs", () => {
|
||||
const logPath = path.join(makeTempRoot(), "phase.log");
|
||||
writeFileSync(
|
||||
logPath,
|
||||
`==> Stale build step\n${"ordinary output\n".repeat(24 * 1024)}warn: recent package issue\n`,
|
||||
);
|
||||
|
||||
const result = runExtract(logPath);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("warn: recent package issue\n");
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { PondGatewayRpc } from "../../scripts/e2e/lib/pond-gateway-rpc.mjs";
|
||||
|
||||
const CONNECTING = 0;
|
||||
const OPEN = 1;
|
||||
const CLOSED = 3;
|
||||
|
||||
class FakeWebSocket extends EventEmitter {
|
||||
readyState = CONNECTING;
|
||||
sent: Array<{ id: string; method: string }> = [];
|
||||
terminated = false;
|
||||
sendError: Error | undefined;
|
||||
respondToConnect = true;
|
||||
|
||||
open(): void {
|
||||
this.readyState = OPEN;
|
||||
this.emit("open");
|
||||
}
|
||||
|
||||
send(payload: string, callback: (error?: Error) => void): void {
|
||||
const frame = JSON.parse(payload) as { id: string; method: string };
|
||||
this.sent.push(frame);
|
||||
callback(this.sendError);
|
||||
if (frame.method === "connect" && !this.sendError && this.respondToConnect) {
|
||||
queueMicrotask(() => {
|
||||
this.emit("message", JSON.stringify({ type: "res", id: frame.id, ok: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.readyState = CLOSED;
|
||||
this.emit("close", 1000, Buffer.alloc(0));
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.terminated = true;
|
||||
this.readyState = CLOSED;
|
||||
this.emit("close", 1006, Buffer.alloc(0));
|
||||
}
|
||||
}
|
||||
|
||||
function createRpc(socket: FakeWebSocket, openTimeoutMs = 100) {
|
||||
return new PondGatewayRpc({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: String(),
|
||||
scopes: ["operator.read"],
|
||||
openTimeoutMs,
|
||||
webSocketFactory: () => socket,
|
||||
});
|
||||
}
|
||||
|
||||
async function connect(rpc: PondGatewayRpc, socket: FakeWebSocket): Promise<void> {
|
||||
const connecting = rpc.connect();
|
||||
socket.open();
|
||||
await connecting;
|
||||
}
|
||||
|
||||
describe("Pond gateway RPC", () => {
|
||||
it("terminates a stalled websocket handshake at the connection deadline", async () => {
|
||||
const socket = new FakeWebSocket();
|
||||
const rpc = createRpc(socket, 1);
|
||||
const keepAlive = setTimeout(() => {}, 100);
|
||||
|
||||
try {
|
||||
await expect(rpc.connect()).rejects.toThrow("Gateway connect timeout: ws://127.0.0.1:18789");
|
||||
} finally {
|
||||
clearTimeout(keepAlive);
|
||||
}
|
||||
|
||||
expect(socket.terminated).toBe(true);
|
||||
});
|
||||
|
||||
it("closes the websocket when the gateway connect RPC stalls", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const socket = new FakeWebSocket();
|
||||
socket.respondToConnect = false;
|
||||
const rpc = createRpc(socket);
|
||||
const connecting = rpc.connect();
|
||||
const rejected = expect(connecting).rejects.toThrow("Gateway RPC timeout: connect");
|
||||
socket.open();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await rejected;
|
||||
expect(socket.readyState).toBe(CLOSED);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects pending RPCs when an open websocket emits an error", async () => {
|
||||
const socket = new FakeWebSocket();
|
||||
const rpc = createRpc(socket);
|
||||
await connect(rpc, socket);
|
||||
|
||||
const request = rpc.request("node.list");
|
||||
socket.emit("error", new Error("invalid websocket frame"));
|
||||
|
||||
await expect(request).rejects.toThrow("invalid websocket frame");
|
||||
await expect(rpc.request("node.list")).rejects.toThrow("invalid websocket frame");
|
||||
});
|
||||
|
||||
it("rejects pending RPCs when an open websocket closes", async () => {
|
||||
const socket = new FakeWebSocket();
|
||||
const rpc = createRpc(socket);
|
||||
await connect(rpc, socket);
|
||||
|
||||
const request = rpc.request("node.list");
|
||||
socket.readyState = CLOSED;
|
||||
socket.emit("close", 1006, Buffer.from("gateway stopped"));
|
||||
|
||||
await expect(request).rejects.toThrow("Gateway socket closed (1006): gateway stopped");
|
||||
});
|
||||
|
||||
it("rejects an RPC when the websocket send callback reports failure", async () => {
|
||||
const socket = new FakeWebSocket();
|
||||
const rpc = createRpc(socket);
|
||||
await connect(rpc, socket);
|
||||
socket.sendError = new Error("send failed");
|
||||
|
||||
await expect(rpc.request("node.list")).rejects.toThrow("send failed");
|
||||
});
|
||||
});
|
||||
@@ -818,10 +818,6 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
"test/scripts/e2e-helper-env-limits.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/channel-plugin-trust-docker.sh",
|
||||
["test/scripts/docker-build-helper.test.ts", "test/scripts/test-projects.test.ts"],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/config-reload-source-docker.sh",
|
||||
[
|
||||
@@ -2678,9 +2674,6 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
"scripts/e2e/parallels/update-job-timeout.ts",
|
||||
"scripts/e2e/parallels/windows-smoke.ts",
|
||||
"scripts/e2e/parallels-windows-smoke.sh",
|
||||
"scripts/e2e/lib/parallels-package/build-info-commit.mjs",
|
||||
"scripts/e2e/lib/parallels-macos-common.sh",
|
||||
"scripts/e2e/lib/parallels-package-common.sh",
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
@@ -2690,7 +2683,6 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
"test/scripts/parallels-smoke-model.test.ts",
|
||||
"test/scripts/parallels-npm-update-smoke.test.ts",
|
||||
"test/scripts/parallels-update-job-timeout.test.ts",
|
||||
"test/scripts/parallels-lib-helpers.test.ts",
|
||||
],
|
||||
watchMode: false,
|
||||
},
|
||||
@@ -2704,18 +2696,6 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("routes Parallels common shell helpers through lib helper owner tests", () => {
|
||||
for (const changedPath of [
|
||||
"scripts/e2e/lib/parallels-macos-common.sh",
|
||||
"scripts/e2e/lib/parallels-package-common.sh",
|
||||
]) {
|
||||
expect(resolveChangedTestTargetPlan([changedPath]), changedPath).toEqual({
|
||||
mode: "targets",
|
||||
targets: ["test/scripts/parallels-lib-helpers.test.ts"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("routes MCP and cron Docker E2E script targets instead of skipping changed tests", () => {
|
||||
const targets = [
|
||||
"scripts/e2e/mcp-channels-docker.sh",
|
||||
|
||||
Reference in New Issue
Block a user