mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
test: add shared OpenClaw test-state harness
This commit is contained in:
@@ -4,14 +4,22 @@ 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-onboard-e2e" OPENCLAW_ONBOARD_E2E_IMAGE)"
|
||||
OPENCLAW_TEST_STATE_FUNCTION_B64="$(
|
||||
node "$ROOT_DIR/scripts/lib/openclaw-test-state.mjs" shell-function \
|
||||
| base64 \
|
||||
| tr -d '\n'
|
||||
)"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" onboard
|
||||
|
||||
echo "Running onboarding E2E..."
|
||||
docker run --rm -t "$IMAGE_NAME" bash -lc '
|
||||
docker run --rm -t \
|
||||
-e "OPENCLAW_TEST_STATE_FUNCTION_B64=$OPENCLAW_TEST_STATE_FUNCTION_B64" \
|
||||
"$IMAGE_NAME" bash -lc '
|
||||
set -euo pipefail
|
||||
trap "" PIPE
|
||||
export TERM=xterm-256color
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_FUNCTION_B64:?missing OPENCLAW_TEST_STATE_FUNCTION_B64}" | base64 -d)"
|
||||
ONBOARD_FLAGS="--flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-daemon --skip-ui"
|
||||
# tsdown may emit dist/index.js or dist/index.mjs depending on runtime/bundler.
|
||||
if [ -f dist/index.mjs ]; then
|
||||
@@ -221,12 +229,8 @@ TRASH
|
||||
}
|
||||
|
||||
set_isolated_openclaw_env() {
|
||||
local home_dir="$1"
|
||||
export HOME="$home_dir"
|
||||
export OPENCLAW_HOME="$home_dir"
|
||||
export OPENCLAW_STATE_DIR="$home_dir/.openclaw"
|
||||
export OPENCLAW_CONFIG_PATH="$OPENCLAW_STATE_DIR/openclaw.json"
|
||||
mkdir -p "$OPENCLAW_STATE_DIR"
|
||||
local label="$1"
|
||||
openclaw_test_state_create "$label" empty
|
||||
}
|
||||
|
||||
assert_file() {
|
||||
|
||||
@@ -12,6 +12,13 @@ SKIP_BUILD="${OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_SKIP_BUILD:-0}"
|
||||
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz update-channel-switch "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
||||
# Bare lanes mount the package artifact instead of baking app sources into the image.
|
||||
docker_e2e_package_mount_args "$PACKAGE_TGZ"
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(
|
||||
node "$ROOT_DIR/scripts/lib/openclaw-test-state.mjs" shell \
|
||||
--label update-channel-switch \
|
||||
--scenario update-stable \
|
||||
| base64 \
|
||||
| tr -d '\n'
|
||||
)"
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" update-channel-switch "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
|
||||
|
||||
@@ -20,6 +27,7 @@ docker run --rm \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENCLAW_SKIP_CHANNELS=1 \
|
||||
-e OPENCLAW_SKIP_PROVIDERS=1 \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc 'set -euo pipefail
|
||||
@@ -156,17 +164,7 @@ NODE
|
||||
)"
|
||||
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
|
||||
|
||||
home_dir="$(mktemp -d /tmp/openclaw-update-channel-switch-home.XXXXXX)"
|
||||
export HOME="$home_dir"
|
||||
mkdir -p "$HOME/.openclaw"
|
||||
cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"'
|
||||
{
|
||||
"update": {
|
||||
"channel": "stable"
|
||||
},
|
||||
"plugins": {}
|
||||
}
|
||||
JSON
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
|
||||
|
||||
export OPENCLAW_GIT_DIR="$git_root"
|
||||
export OPENCLAW_UPDATE_DEV_TARGET_REF="$fixture_sha"
|
||||
|
||||
324
scripts/lib/openclaw-test-state.mjs
Normal file
324
scripts/lib/openclaw-test-state.mjs
Normal file
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DEFAULT_LABEL = "state";
|
||||
const DEFAULT_SCENARIO = "empty";
|
||||
const SCENARIOS = new Set([
|
||||
"empty",
|
||||
"minimal",
|
||||
"update-stable",
|
||||
"gateway-loopback",
|
||||
"external-service",
|
||||
]);
|
||||
|
||||
function usage() {
|
||||
return `Usage:
|
||||
node scripts/lib/openclaw-test-state.mjs -- create [--label <name>] [--scenario <name>] [--env-file <path>] [--json]
|
||||
node scripts/lib/openclaw-test-state.mjs shell [--label <name>] [--scenario <name>]
|
||||
node scripts/lib/openclaw-test-state.mjs shell-function
|
||||
|
||||
Scenarios: ${[...SCENARIOS].join(", ")}
|
||||
`;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv[0] === "--" ? argv.slice(1) : argv;
|
||||
const [command, ...rest] = args;
|
||||
if (!command || command === "--help" || command === "-h") {
|
||||
return { command: "help", options: {} };
|
||||
}
|
||||
const options = {};
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const arg = rest[index];
|
||||
if (arg === "--json") {
|
||||
options.json = true;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
arg === "--label" ||
|
||||
arg === "--scenario" ||
|
||||
arg === "--env-file" ||
|
||||
arg === "--port" ||
|
||||
arg === "--token"
|
||||
) {
|
||||
const value = rest[index + 1];
|
||||
if (!value) {
|
||||
throw new Error(`missing value for ${arg}`);
|
||||
}
|
||||
index += 1;
|
||||
options[arg.slice(2)] = value;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
return { command, options };
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
return (
|
||||
String(value || DEFAULT_LABEL)
|
||||
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
|
||||
.replace(/^-+|-+$/gu, "") || DEFAULT_LABEL
|
||||
);
|
||||
}
|
||||
|
||||
function requireScenario(value) {
|
||||
const scenario = value || DEFAULT_SCENARIO;
|
||||
if (!SCENARIOS.has(scenario)) {
|
||||
throw new Error(`unknown scenario: ${scenario}`);
|
||||
}
|
||||
return scenario;
|
||||
}
|
||||
|
||||
function scenarioConfig(scenario, options = {}) {
|
||||
if (scenario === "minimal" || scenario === "external-service") {
|
||||
return {};
|
||||
}
|
||||
if (scenario === "update-stable") {
|
||||
return {
|
||||
update: {
|
||||
channel: "stable",
|
||||
},
|
||||
plugins: {},
|
||||
};
|
||||
}
|
||||
if (scenario === "gateway-loopback") {
|
||||
return {
|
||||
gateway: {
|
||||
port: Number(options.port || 18789),
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: options.token || "openclaw-test-token",
|
||||
},
|
||||
controlUi: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function scenarioEnv(scenario) {
|
||||
if (scenario === "external-service") {
|
||||
return {
|
||||
OPENCLAW_SERVICE_REPAIR_POLICY: "external",
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replace(/'/gu, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function renderExports(env) {
|
||||
return Object.entries(env)
|
||||
.map(([key, value]) => `export ${key}=${shellQuote(value)}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function renderConfigWrite(configPathExpression, config) {
|
||||
if (config === undefined) {
|
||||
return "";
|
||||
}
|
||||
const json = JSON.stringify(config, null, 2);
|
||||
return [
|
||||
`cat > ${configPathExpression} <<'OPENCLAW_TEST_STATE_JSON'`,
|
||||
json,
|
||||
"OPENCLAW_TEST_STATE_JSON",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildCreatePlan(options = {}) {
|
||||
const label = normalizeLabel(options.label);
|
||||
const scenario = requireScenario(options.scenario);
|
||||
if (!options.root) {
|
||||
throw new Error("buildCreatePlan requires root");
|
||||
}
|
||||
const root = options.root;
|
||||
const home = path.join(root, "home");
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
const workspaceDir = path.join(home, "workspace");
|
||||
const config = scenarioConfig(scenario, options);
|
||||
const env = {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
OPENCLAW_HOME: home,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
...scenarioEnv(scenario),
|
||||
};
|
||||
return {
|
||||
label,
|
||||
scenario,
|
||||
root,
|
||||
home,
|
||||
stateDir,
|
||||
configPath,
|
||||
workspaceDir,
|
||||
env,
|
||||
hasConfig: config !== undefined,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createState(options = {}) {
|
||||
const label = normalizeLabel(options.label);
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-${label}-`));
|
||||
const plan = buildCreatePlan({ ...options, root });
|
||||
await fs.mkdir(plan.stateDir, { recursive: true });
|
||||
await fs.mkdir(plan.workspaceDir, { recursive: true });
|
||||
if (plan.config !== undefined) {
|
||||
await fs.writeFile(plan.configPath, `${JSON.stringify(plan.config, null, 2)}\n`, "utf8");
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function renderEnvFile(plan) {
|
||||
return `${renderExports(plan.env)}\n`;
|
||||
}
|
||||
|
||||
export function renderShellSnippet(options = {}) {
|
||||
const label = normalizeLabel(options.label);
|
||||
const scenario = requireScenario(options.scenario);
|
||||
const config = scenarioConfig(scenario, options);
|
||||
const env = scenarioEnv(scenario);
|
||||
const template = `/tmp/openclaw-${label}-${scenario}-home.XXXXXX`;
|
||||
const lines = [
|
||||
`OPENCLAW_TEST_STATE_HOME="$(mktemp -d ${shellQuote(template)})"`,
|
||||
'export HOME="$OPENCLAW_TEST_STATE_HOME"',
|
||||
'export USERPROFILE="$OPENCLAW_TEST_STATE_HOME"',
|
||||
'export OPENCLAW_HOME="$OPENCLAW_TEST_STATE_HOME"',
|
||||
'export OPENCLAW_STATE_DIR="$OPENCLAW_TEST_STATE_HOME/.openclaw"',
|
||||
'export OPENCLAW_CONFIG_PATH="$OPENCLAW_STATE_DIR/openclaw.json"',
|
||||
'export OPENCLAW_TEST_WORKSPACE_DIR="$OPENCLAW_TEST_STATE_HOME/workspace"',
|
||||
'mkdir -p "$OPENCLAW_STATE_DIR" "$OPENCLAW_TEST_WORKSPACE_DIR"',
|
||||
];
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
lines.push(`export ${key}=${shellQuote(value)}`);
|
||||
}
|
||||
const configWrite = renderConfigWrite('"$OPENCLAW_CONFIG_PATH"', config);
|
||||
if (configWrite) {
|
||||
lines.push(configWrite);
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export function renderShellFunction() {
|
||||
return `openclaw_test_state_create() {
|
||||
local raw_label="\${1:-state}"
|
||||
local label="$raw_label"
|
||||
local scenario="\${2:-empty}"
|
||||
case "$scenario" in
|
||||
empty|minimal|update-stable|gateway-loopback|external-service) ;;
|
||||
*)
|
||||
echo "unknown OpenClaw test-state scenario: $scenario" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "$raw_label" in
|
||||
/*)
|
||||
OPENCLAW_TEST_STATE_HOME="$raw_label"
|
||||
mkdir -p "$OPENCLAW_TEST_STATE_HOME"
|
||||
;;
|
||||
*)
|
||||
label="$(printf "%s" "$label" | tr -cs "A-Za-z0-9_.-" "-" | sed -e "s/^-*//" -e "s/-*$//")"
|
||||
[ -n "$label" ] || label="state"
|
||||
OPENCLAW_TEST_STATE_HOME="$(mktemp -d "/tmp/openclaw-$label-$scenario-home.XXXXXX")"
|
||||
;;
|
||||
esac
|
||||
export HOME="$OPENCLAW_TEST_STATE_HOME"
|
||||
export USERPROFILE="$OPENCLAW_TEST_STATE_HOME"
|
||||
export OPENCLAW_HOME="$OPENCLAW_TEST_STATE_HOME"
|
||||
export OPENCLAW_STATE_DIR="$OPENCLAW_TEST_STATE_HOME/.openclaw"
|
||||
export OPENCLAW_CONFIG_PATH="$OPENCLAW_STATE_DIR/openclaw.json"
|
||||
export OPENCLAW_TEST_WORKSPACE_DIR="$OPENCLAW_TEST_STATE_HOME/workspace"
|
||||
unset OPENCLAW_AGENT_DIR
|
||||
unset PI_CODING_AGENT_DIR
|
||||
unset OPENCLAW_SERVICE_REPAIR_POLICY
|
||||
mkdir -p "$OPENCLAW_STATE_DIR" "$OPENCLAW_TEST_WORKSPACE_DIR"
|
||||
case "$scenario" in
|
||||
minimal)
|
||||
cat > "$OPENCLAW_CONFIG_PATH" <<'OPENCLAW_TEST_STATE_JSON'
|
||||
{}
|
||||
OPENCLAW_TEST_STATE_JSON
|
||||
;;
|
||||
update-stable)
|
||||
cat > "$OPENCLAW_CONFIG_PATH" <<'OPENCLAW_TEST_STATE_JSON'
|
||||
{
|
||||
"update": {
|
||||
"channel": "stable"
|
||||
},
|
||||
"plugins": {}
|
||||
}
|
||||
OPENCLAW_TEST_STATE_JSON
|
||||
;;
|
||||
gateway-loopback)
|
||||
cat > "$OPENCLAW_CONFIG_PATH" <<'OPENCLAW_TEST_STATE_JSON'
|
||||
{
|
||||
"gateway": {
|
||||
"port": 18789,
|
||||
"auth": {
|
||||
"mode": "token",
|
||||
"token": "openclaw-test-token"
|
||||
},
|
||||
"controlUi": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
OPENCLAW_TEST_STATE_JSON
|
||||
;;
|
||||
external-service)
|
||||
export OPENCLAW_SERVICE_REPAIR_POLICY="external"
|
||||
cat > "$OPENCLAW_CONFIG_PATH" <<'OPENCLAW_TEST_STATE_JSON'
|
||||
{}
|
||||
OPENCLAW_TEST_STATE_JSON
|
||||
;;
|
||||
esac
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
async function main(argv = process.argv.slice(2)) {
|
||||
const { command, options } = parseArgs(argv);
|
||||
if (command === "help") {
|
||||
process.stdout.write(usage());
|
||||
return;
|
||||
}
|
||||
if (command === "shell") {
|
||||
process.stdout.write(renderShellSnippet(options));
|
||||
return;
|
||||
}
|
||||
if (command === "shell-function") {
|
||||
process.stdout.write(renderShellFunction());
|
||||
return;
|
||||
}
|
||||
if (command === "create") {
|
||||
const plan = await createState(options);
|
||||
if (options["env-file"]) {
|
||||
await fs.writeFile(options["env-file"], renderEnvFile(plan), "utf8");
|
||||
}
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new Error(`unknown command: ${command}`);
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||
|
||||
if (isMain) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.stderr.write(usage());
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -231,6 +231,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["scripts/changed-lanes.mjs", ["test/scripts/changed-lanes.test.ts"]],
|
||||
["scripts/check-changed.mjs", ["test/scripts/changed-lanes.test.ts"]],
|
||||
["scripts/lib/live-docker-stage.sh", ["test/scripts/live-docker-stage.test.ts"]],
|
||||
["scripts/lib/openclaw-test-state.mjs", ["test/scripts/openclaw-test-state.test.ts"]],
|
||||
["scripts/lib/vitest-local-scheduling.mjs", ["test/scripts/vitest-local-scheduling.test.ts"]],
|
||||
[
|
||||
"scripts/run-vitest.mjs",
|
||||
@@ -256,6 +257,7 @@ const TOOLING_TEST_TARGETS = new Map([
|
||||
["test/scripts/barnacle-auto-response.test.ts", ["test/scripts/barnacle-auto-response.test.ts"]],
|
||||
["test/scripts/changed-lanes.test.ts", ["test/scripts/changed-lanes.test.ts"]],
|
||||
["test/scripts/live-docker-stage.test.ts", ["test/scripts/live-docker-stage.test.ts"]],
|
||||
["test/scripts/openclaw-test-state.test.ts", ["test/scripts/openclaw-test-state.test.ts"]],
|
||||
["test/scripts/test-projects.test.ts", ["test/scripts/test-projects.test.ts"]],
|
||||
["test/scripts/testbox-sync-sanity.test.ts", ["test/scripts/testbox-sync-sanity.test.ts"]],
|
||||
[
|
||||
@@ -277,6 +279,7 @@ const GROUP_VISIBLE_REPLY_PROMPT_TEST_TARGETS = [
|
||||
];
|
||||
const SOURCE_TEST_TARGETS = new Map([
|
||||
...PRECISE_SOURCE_TEST_TARGETS,
|
||||
["src/test-utils/openclaw-test-state.ts", ["src/test-utils/openclaw-test-state.test.ts"]],
|
||||
[
|
||||
"src/plugin-sdk/test-helpers/directory-ids.ts",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user