refactor: consolidate e2e fixture helpers

This commit is contained in:
Peter Steinberger
2026-04-29 10:49:10 +01:00
parent 412434a450
commit b52197427c
11 changed files with 427 additions and 449 deletions

View File

@@ -48,74 +48,9 @@ output_file="$HOME/delete.json"
trap '\''rm -rf "$HOME"'\'' EXIT
mkdir -p "$OPENCLAW_STATE_DIR" "$SHARED_WORKSPACE"
node --input-type=module - <<'\''NODE'\''
import fs from "node:fs";
import path from "node:path";
const stateDir = process.env.OPENCLAW_STATE_DIR;
const sharedWorkspace = process.env.SHARED_WORKSPACE;
if (!stateDir || !sharedWorkspace) {
throw new Error("missing OPENCLAW_STATE_DIR or SHARED_WORKSPACE");
}
fs.mkdirSync(stateDir, { recursive: true });
fs.mkdirSync(sharedWorkspace, { recursive: true });
fs.writeFileSync(
path.join(stateDir, "openclaw.json"),
`${JSON.stringify(
{
agents: {
list: [
{ id: "main", workspace: sharedWorkspace },
{ id: "ops", workspace: sharedWorkspace },
],
},
},
null,
2,
)}\n`,
);
NODE
node scripts/e2e/lib/fixture.mjs agents-delete-config
run_openclaw agents delete ops --force --json > "$output_file"
node --input-type=module - "$output_file" <<'\''NODE'\''
import fs from "node:fs";
import path from "node:path";
const outputPath = process.argv[2];
const raw = fs.readFileSync(outputPath, "utf8").trim();
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
console.error("agents delete --json did not emit valid JSON:");
console.error(raw);
throw error;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
assert(parsed.agentId === "ops", `unexpected agentId: ${JSON.stringify(parsed.agentId)}`);
assert(parsed.workspace === process.env.SHARED_WORKSPACE, "deleted agent workspace mismatch");
assert(parsed.workspaceRetained === true, "shared workspace was not marked retained");
assert(parsed.workspaceRetainedReason === "shared", "missing shared retained reason");
assert(
Array.isArray(parsed.workspaceSharedWith) && parsed.workspaceSharedWith.includes("main"),
"missing shared-with main marker",
);
assert(fs.existsSync(process.env.SHARED_WORKSPACE), "shared workspace was removed");
const configPath = path.join(process.env.OPENCLAW_STATE_DIR, "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
const remaining = config?.agents?.list ?? [];
assert(Array.isArray(remaining), "agents list missing after delete");
assert(!remaining.some((entry) => entry?.id === "ops"), "deleted agent remained in config");
assert(remaining.some((entry) => entry?.id === "main"), "main agent missing after delete");
console.log("agents delete shared workspace smoke ok");
NODE
node scripts/e2e/lib/fixture.mjs agents-delete-assert "$output_file"
'

View File

@@ -61,31 +61,7 @@ openclaw_e2e_write_state_env
entry=\"\$(openclaw_e2e_resolve_entrypoint)\"
mkdir -p /tmp/openclaw-browser-cdp/chrome
find dist -maxdepth 1 -type f -name 'pw-ai-*.js' ! -name 'pw-ai-state-*' -exec mv {} /tmp/openclaw-browser-cdp/ \;
cat > \"\$OPENCLAW_CONFIG_PATH\" <<'JSON'
{
\"gateway\": {
\"port\": $PORT,
\"auth\": {
\"mode\": \"token\",
\"token\": \"$TOKEN\"
},
\"controlUi\": { \"enabled\": false }
},
\"browser\": {
\"enabled\": true,
\"defaultProfile\": \"docker-cdp\",
\"ssrfPolicy\": {
\"allowedHostnames\": [\"127.0.0.1\"]
},
\"profiles\": {
\"docker-cdp\": {
\"cdpUrl\": \"http://127.0.0.1:$CDP_PORT\",
\"color\": \"#FF4500\"
}
}
}
}
JSON
PORT=$PORT CDP_PORT=$CDP_PORT node scripts/e2e/lib/fixture.mjs browser-cdp
chromium --headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage \\
--remote-debugging-address=127.0.0.1 \\
--remote-debugging-port=$CDP_PORT \\

View File

@@ -35,29 +35,7 @@ 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}\"
openclaw_e2e_write_state_env
entry=\"\$(openclaw_e2e_resolve_entrypoint)\"
cat > \"\$OPENCLAW_CONFIG_PATH\" <<'JSON'
{
\"gateway\": {
\"port\": $PORT,
\"auth\": {
\"mode\": \"token\",
\"token\": {
\"source\": \"env\",
\"provider\": \"default\",
\"id\": \"GATEWAY_AUTH_TOKEN_REF\"
}
},
\"channelHealthCheckMinutes\": 1,
\"controlUi\": {
\"enabled\": false
},
\"reload\": {
\"mode\": \"hybrid\",
\"debounceMs\": 0
}
}
}
JSON
PORT=$PORT node scripts/e2e/lib/fixture.mjs config-reload
openclaw_e2e_exec_gateway \"\$entry\" $PORT loopback /tmp/config-reload-e2e.log" >/dev/null
echo "Waiting for gateway..."

283
scripts/e2e/lib/fixture.mjs Normal file
View File

@@ -0,0 +1,283 @@
import fs from "node:fs";
import path from "node:path";
const [command, ...args] = process.argv.slice(2);
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const write = (file, contents) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, contents);
};
const writeJson = (file, value) => write(file, json(value));
const requireArg = (value, name) => {
if (!value) {
throw new Error(`${name} is required`);
}
return value;
};
const assert = (condition, message) => {
if (!condition) {
throw new Error(message);
}
};
function writePluginManifest(file, id) {
writeJson(file, { id, configSchema: { type: "object", properties: {} } });
}
function writePluginDemo([dir]) {
write(
path.join(requireArg(dir, "dir"), "index.js"),
'module.exports = { id: "demo-plugin", name: "Demo Plugin", description: "Docker E2E demo plugin", register(api) { api.registerTool(() => null, { name: "demo_tool" }); api.registerGatewayMethod("demo.ping", async () => ({ ok: true })); api.registerCli(() => {}, { commands: ["demo"] }); api.registerService({ id: "demo-service", start: () => {} }); }, };\n',
);
writePluginManifest(path.join(dir, "openclaw.plugin.json"), "demo-plugin");
}
function writePlugin([dir, id, version, method, name]) {
for (const [value, label] of [
[dir, "dir"],
[id, "id"],
[version, "version"],
[method, "method"],
[name, "name"],
]) {
requireArg(value, label);
}
writeJson(path.join(dir, "package.json"), {
name: `@openclaw/${id}`,
version,
openclaw: { extensions: ["./index.js"] },
});
write(
path.join(dir, "index.js"),
`module.exports = { id: ${JSON.stringify(id)}, name: ${JSON.stringify(name)}, register(api) { api.registerGatewayMethod(${JSON.stringify(method)}, async () => ({ ok: true })); }, };\n`,
);
writePluginManifest(path.join(dir, "openclaw.plugin.json"), id);
}
function writeClaudeBundle([root]) {
root = requireArg(root, "root");
writeJson(path.join(root, ".claude-plugin", "plugin.json"), { name: "claude-bundle-e2e" });
write(
path.join(root, "commands", "office-hours.md"),
"---\ndescription: Help with architecture and rollout planning\n---\nAct as an engineering advisor.\n\nFocus on:\n$ARGUMENTS\n",
);
}
function writePluginMarketplace([root]) {
root = requireArg(root, "root");
writeJson(path.join(root, ".claude-plugin", "marketplace.json"), {
name: "Fixture Marketplace",
version: "1.0.0",
plugins: [
{
name: "marketplace-shortcut",
version: "0.0.1",
description: "Shortcut install fixture",
source: "./plugins/marketplace-shortcut",
},
{
name: "marketplace-direct",
version: "0.0.1",
description: "Explicit marketplace fixture",
source: { type: "path", path: "./plugins/marketplace-direct" },
},
],
});
writeJson(path.join(process.env.HOME, ".claude", "plugins", "known_marketplaces.json"), {
"claude-fixtures": {
installLocation: root,
source: { type: "github", repo: "openclaw/fixture-marketplace" },
},
});
}
function writeConfig(kind) {
const configPath = requireArg(process.env.OPENCLAW_CONFIG_PATH, "OPENCLAW_CONFIG_PATH");
const port = Number(process.env.PORT ?? 18789);
const config =
kind === "config-reload"
? {
gateway: {
port,
auth: {
mode: "token",
token: { source: "env", provider: "default", id: "GATEWAY_AUTH_TOKEN_REF" },
},
channelHealthCheckMinutes: 1,
controlUi: { enabled: false },
reload: { mode: "hybrid", debounceMs: 0 },
},
}
: kind === "browser-cdp"
? {
gateway: {
port,
auth: {
mode: "token",
token: requireArg(process.env.OPENCLAW_GATEWAY_TOKEN, "OPENCLAW_GATEWAY_TOKEN"),
},
controlUi: { enabled: false },
},
browser: {
enabled: true,
defaultProfile: "docker-cdp",
ssrfPolicy: { allowedHostnames: ["127.0.0.1"] },
profiles: {
"docker-cdp": {
cdpUrl: `http://127.0.0.1:${Number(process.env.CDP_PORT ?? 19222)}`,
color: "#FF4500",
},
},
},
}
: null;
writeJson(configPath, requireArg(config, "known config kind"));
}
function writeOpenAiWebSearchMinimalConfig() {
writeJson(path.join(process.env.OPENCLAW_STATE_DIR, "openclaw.json"), {
agents: {
defaults: {
model: { primary: "openai/gpt-5" },
models: {
"openai/gpt-5": {
params: { transport: "sse", openaiWsWarmup: false },
},
},
},
},
models: {
providers: {
openai: {
api: "openai-responses",
baseUrl: "http://api.openai.com/v1",
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
request: { allowPrivateNetwork: true },
models: [
{
id: "gpt-5",
name: "gpt-5",
api: "openai-responses",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
contextTokens: 96000,
maxTokens: 4096,
},
],
},
},
},
tools: { web: { search: { enabled: true, maxResults: 3 } } },
plugins: { enabled: true, allow: ["openai"], entries: { openai: { enabled: true } } },
gateway: { auth: { mode: "token", token: process.env.OPENCLAW_GATEWAY_TOKEN } },
});
}
function writeOpenWebUiConfig([openaiApiKey]) {
const batchPath = requireArg(
process.env.OPENCLAW_CONFIG_BATCH_PATH,
"OPENCLAW_CONFIG_BATCH_PATH",
);
writeJson(batchPath, [
{ path: "models.providers.openai.apiKey", value: requireArg(openaiApiKey, "OpenAI API key") },
{
path: "models.providers.openai.baseUrl",
value: (process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").trim(),
},
{ path: "models.providers.openai.models", value: [] },
{ path: "gateway.controlUi.enabled", value: false },
{ path: "gateway.mode", value: "local" },
{ path: "gateway.bind", value: "lan" },
{ path: "gateway.auth.mode", value: "token" },
{ path: "gateway.auth.token", value: process.env.OPENCLAW_GATEWAY_TOKEN },
{ path: "gateway.http.endpoints.chatCompletions.enabled", value: true },
{ path: "agents.defaults.model.primary", value: process.env.OPENCLAW_OPENWEBUI_MODEL },
]);
}
function writeOpenWebUiWorkspace() {
const workspace =
process.env.OPENCLAW_WORKSPACE_DIR || path.join(process.env.HOME, ".openclaw", "workspace");
write(
path.join(workspace, "IDENTITY.md"),
"# Identity\n\n- Name: OpenClaw\n- Purpose: Open WebUI Docker compatibility smoke test assistant.\n",
);
writeJson(path.join(workspace, ".openclaw", "workspace-state.json"), {
version: 1,
setupCompletedAt: "2026-01-01T00:00:00.000Z",
});
fs.rmSync(path.join(workspace, "BOOTSTRAP.md"), { force: true });
}
function writeAgentsDeleteConfig() {
const stateDir = requireArg(process.env.OPENCLAW_STATE_DIR, "OPENCLAW_STATE_DIR");
const sharedWorkspace = requireArg(process.env.SHARED_WORKSPACE, "SHARED_WORKSPACE");
fs.mkdirSync(sharedWorkspace, { recursive: true });
writeJson(path.join(stateDir, "openclaw.json"), {
agents: {
list: [
{ id: "main", workspace: sharedWorkspace },
{ id: "ops", workspace: sharedWorkspace },
],
},
});
}
function assertAgentsDeleteResult([outputPath]) {
let parsed;
try {
parsed = readJson(requireArg(outputPath, "outputPath"));
} catch (error) {
console.error("agents delete --json did not emit valid JSON:");
console.error(fs.readFileSync(outputPath, "utf8").trim());
throw error;
}
for (const [actual, expected, label] of [
[parsed.agentId, "ops", "agentId"],
[parsed.workspace, process.env.SHARED_WORKSPACE, "workspace"],
[parsed.workspaceRetained, true, "workspaceRetained"],
[parsed.workspaceRetainedReason, "shared", "workspaceRetainedReason"],
]) {
assert(actual === expected, `${label} mismatch: ${JSON.stringify(actual)}`);
}
assert(
Array.isArray(parsed.workspaceSharedWith) && parsed.workspaceSharedWith.includes("main"),
"missing shared-with main marker",
);
assert(fs.existsSync(process.env.SHARED_WORKSPACE), "shared workspace was removed");
const remaining =
readJson(path.join(process.env.OPENCLAW_STATE_DIR, "openclaw.json"))?.agents?.list ?? [];
assert(Array.isArray(remaining), "agents list missing after delete");
assert(!remaining.some((entry) => entry?.id === "ops"), "deleted agent remained in config");
assert(
remaining.some((entry) => entry?.id === "main"),
"main agent missing after delete",
);
console.log("agents delete shared workspace smoke ok");
}
const commands = {
"plugin-demo": writePluginDemo,
plugin: writePlugin,
"plugin-manifest": ([file, id]) =>
writePluginManifest(requireArg(file, "file"), requireArg(id, "id")),
"claude-bundle": writeClaudeBundle,
marketplace: writePluginMarketplace,
"config-reload": () => writeConfig("config-reload"),
"browser-cdp": () => writeConfig("browser-cdp"),
"openai-web-search-minimal-config": writeOpenAiWebSearchMinimalConfig,
"openwebui-config": writeOpenWebUiConfig,
"openwebui-workspace": writeOpenWebUiWorkspace,
"agents-delete-config": writeAgentsDeleteConfig,
"agents-delete-assert": assertAgentsDeleteResult,
};
(
commands[command] ??
(() => {
throw new Error(`unknown fixture command: ${command}`);
})
)(args);

View File

@@ -56,67 +56,7 @@ mkdir -p "$OPENCLAW_STATE_DIR"
node scripts/e2e/lib/openai-web-search-minimal/assertions.mjs assert-patch-behavior
cat >"$OPENCLAW_STATE_DIR/openclaw.json" <<JSON
{
"agents": {
"defaults": {
"model": { "primary": "openai/gpt-5" },
"models": {
"openai/gpt-5": {
"params": {
"transport": "sse",
"openaiWsWarmup": false
}
}
}
}
},
"models": {
"providers": {
"openai": {
"api": "openai-responses",
"baseUrl": "http://api.openai.com/v1",
"apiKey": { "source": "env", "provider": "default", "id": "OPENAI_API_KEY" },
"request": { "allowPrivateNetwork": true },
"models": [
{
"id": "gpt-5",
"name": "gpt-5",
"api": "openai-responses",
"reasoning": true,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 128000,
"contextTokens": 96000,
"maxTokens": 4096
}
]
}
}
},
"tools": {
"web": {
"search": {
"enabled": true,
"maxResults": 3
}
}
},
"plugins": {
"enabled": true,
"allow": ["openai"],
"entries": {
"openai": { "enabled": true }
}
},
"gateway": {
"auth": {
"mode": "token",
"token": "${TOKEN}"
}
}
}
JSON
node scripts/e2e/lib/fixture.mjs openai-web-search-minimal-config
MOCK_PORT="$MOCK_PORT" \
MOCK_REQUEST_LOG="$MOCK_REQUEST_LOG" \

View File

@@ -4,14 +4,6 @@ import path from "node:path";
const command = process.argv[2];
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
function setManifestId() {
const file = process.argv[3];
const id = process.argv[4];
const parsed = readJson(file);
parsed.id = id;
fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
}
function recordFixturePluginTrust() {
const pluginId = process.argv[3];
const pluginRoot = process.argv[4];
@@ -396,7 +388,6 @@ function assertClawHubRemoved() {
}
const commands = {
"set-manifest-id": setManifestId,
"record-fixture-plugin-trust": recordFixturePluginTrust,
"demo-plugin": assertDemoPlugin,
"plugin-tgz": () =>

View File

@@ -7,22 +7,7 @@ record_fixture_plugin_trust() {
write_demo_fixture_plugin() {
local dir="$1"
mkdir -p "$dir"
cat >"$dir/index.js" <<'JS'
module.exports = {
id: "demo-plugin",
name: "Demo Plugin",
description: "Docker E2E demo plugin",
register(api) {
api.registerTool(() => null, { name: "demo_tool" });
api.registerGatewayMethod("demo.ping", async () => ({ ok: true }));
api.registerCli(() => {}, { commands: ["demo"] });
api.registerService({ id: "demo-service", start: () => {} });
},
};
JS
write_fixture_manifest "$dir/openclaw.plugin.json" demo-plugin
node scripts/e2e/lib/fixture.mjs plugin-demo "$dir"
}
write_fixture_plugin() {
@@ -32,40 +17,14 @@ write_fixture_plugin() {
local method="$4"
local name="$5"
mkdir -p "$dir"
cat >"$dir/package.json" <<JSON
{
"name": "@openclaw/$id",
"version": "$version",
"openclaw": { "extensions": ["./index.js"] }
}
JSON
cat >"$dir/index.js" <<JS
module.exports = {
id: "$id",
name: "$name",
register(api) {
api.registerGatewayMethod("$method", async () => ({ ok: true }));
},
};
JS
write_fixture_manifest "$dir/openclaw.plugin.json" "$id"
node scripts/e2e/lib/fixture.mjs plugin "$dir" "$id" "$version" "$method" "$name"
}
write_fixture_manifest() {
local file="$1"
local id="$2"
cat >"$file" <<'JSON'
{
"id": "placeholder",
"configSchema": {
"type": "object",
"properties": {}
}
}
JSON
node scripts/e2e/lib/plugins/assertions.mjs set-manifest-id "$file" "$id"
node scripts/e2e/lib/fixture.mjs plugin-manifest "$file" "$id"
}
pack_fixture_plugin() {
@@ -84,19 +43,5 @@ pack_fixture_plugin() {
write_claude_bundle_fixture() {
local bundle_root="$1"
mkdir -p "$bundle_root/.claude-plugin" "$bundle_root/commands"
cat >"$bundle_root/.claude-plugin/plugin.json" <<'JSON'
{
"name": "claude-bundle-e2e"
}
JSON
cat >"$bundle_root/commands/office-hours.md" <<'MD'
---
description: Help with architecture and rollout planning
---
Act as an engineering advisor.
Focus on:
$ARGUMENTS
MD
node scripts/e2e/lib/fixture.mjs claude-bundle "$bundle_root"
}

View File

@@ -14,40 +14,7 @@ run_plugins_marketplace_scenario() {
"0.0.1" \
"demo.marketplace.direct.v1" \
"Marketplace Direct"
cat >"$marketplace_root/.claude-plugin/marketplace.json" <<'JSON'
{
"name": "Fixture Marketplace",
"version": "1.0.0",
"plugins": [
{
"name": "marketplace-shortcut",
"version": "0.0.1",
"description": "Shortcut install fixture",
"source": "./plugins/marketplace-shortcut"
},
{
"name": "marketplace-direct",
"version": "0.0.1",
"description": "Explicit marketplace fixture",
"source": {
"type": "path",
"path": "./plugins/marketplace-direct"
}
}
]
}
JSON
cat >"$HOME/.claude/plugins/known_marketplaces.json" <<JSON
{
"claude-fixtures": {
"installLocation": "$marketplace_root",
"source": {
"type": "github",
"repo": "openclaw/fixture-marketplace"
}
}
}
JSON
node scripts/e2e/lib/fixture.mjs marketplace "$marketplace_root"
node "$OPENCLAW_ENTRY" plugins marketplace list claude-fixtures --json >/tmp/marketplace-list.json

View File

@@ -0,0 +1,124 @@
import fs from "node:fs";
import path from "node:path";
import { legacyPackageAcceptanceCompat } from "../package-compat.mjs";
const [command, ...args] = process.argv.slice(2);
const controlUiHtml = "<!doctype html><title>fixture</title>\n";
function usage() {
console.error(
"usage: assertions.mjs <prepare-git-fixture|write-control-ui|assert-update|assert-config-channel|assert-status-kind> [...]",
);
process.exit(2);
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function writeControlUi(root) {
const file = path.join(root, "dist", "control-ui", "index.html");
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, controlUiHtml);
}
function prepareGitFixture(root) {
const packageJsonPath = path.join(root, "package.json");
const packageJson = readJson(packageJsonPath);
packageJson.pnpm = { ...packageJson.pnpm, allowUnusedPatches: true };
const patches = packageJson.pnpm.patchedDependencies;
if (patches && typeof patches === "object" && !Array.isArray(patches)) {
const kept = {};
const missing = [];
for (const [dependency, patchFile] of Object.entries(patches)) {
const exists =
typeof patchFile === "string" &&
fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile));
if (exists) {
kept[dependency] = patchFile;
} else {
missing.push(`${dependency} -> ${String(patchFile)}`);
}
}
if (missing.length > 0 && !legacyPackageAcceptanceCompat(packageJson.version)) {
throw new Error(
`package ${packageJson.version} has missing pnpm.patchedDependencies in package fixture: ${missing.join(", ")}`,
);
}
if (Object.keys(kept).length > 0) {
packageJson.pnpm.patchedDependencies = kept;
} else {
delete packageJson.pnpm.patchedDependencies;
}
}
const fixtureUiBuildSource = `const fs=require("node:fs");fs.mkdirSync("dist/control-ui",{recursive:true});fs.writeFileSync("dist/control-ui/index.html",${JSON.stringify(controlUiHtml)})`;
packageJson.scripts = {
...packageJson.scripts,
build: 'node -e "console.log(\\"fixture build skipped\\")"',
lint: 'node -e "console.log(\\"fixture lint skipped\\")"',
"ui:build": `node -e ${JSON.stringify(fixtureUiBuildSource)}`,
};
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
writeControlUi(root);
}
function assertUpdate(channel) {
const payload = JSON.parse(process.env.UPDATE_JSON ?? "");
if (payload.status !== "ok") {
throw new Error(`expected ${channel} update status ok, got ${payload.status}`);
}
if (channel === "dev" && payload.mode !== "git") {
throw new Error(`expected dev update mode git, got ${payload.mode}`);
}
if (channel === "stable" && !["npm", "pnpm", "bun"].includes(payload.mode)) {
throw new Error(`expected package-manager mode after stable switch, got ${payload.mode}`);
}
if (payload.postUpdate?.plugins && payload.postUpdate.plugins.status !== "ok") {
throw new Error(
`expected plugin post-update ok, got ${JSON.stringify(payload.postUpdate?.plugins)}`,
);
}
}
function assertConfigChannel(channel) {
const config = readJson(path.join(process.env.HOME, ".openclaw", "openclaw.json"));
if (config.update?.channel === channel) {
return;
}
if (process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1") {
console.log(
`legacy package did not persist update.channel ${channel}; got ${JSON.stringify(config.update?.channel)}`,
);
return;
}
throw new Error(
`expected persisted update.channel ${channel}, got ${JSON.stringify(config.update?.channel)}`,
);
}
function assertStatusKind(kind) {
const payload = JSON.parse(process.env.STATUS_JSON ?? "");
if (payload.update?.installKind !== kind) {
throw new Error(`expected ${kind} install after switch, got ${payload.update?.installKind}`);
}
}
switch (command) {
case "prepare-git-fixture":
prepareGitFixture(args[0] ?? "/tmp/openclaw-git");
break;
case "write-control-ui":
writeControlUi(args[0] ?? "/tmp/openclaw-git");
break;
case "assert-update":
assertUpdate(args[0]);
break;
case "assert-config-channel":
assertConfigChannel(args[0]);
break;
case "assert-status-kind":
assertStatusKind(args[0]);
break;
default:
usage();
}

View File

@@ -75,46 +75,10 @@ docker_e2e_docker_cmd run -d \
openai_api_key="${OPENAI_API_KEY:?OPENAI_API_KEY required}"
batch_file="$(mktemp /tmp/openclaw-openwebui-config.XXXXXX.json)"
OPENCLAW_CONFIG_BATCH_PATH="$batch_file" node - <<'"'"'NODE'"'"' "$openai_api_key"
const fs = require("node:fs");
const openaiApiKey = process.argv[2];
const batchPath = process.env.OPENCLAW_CONFIG_BATCH_PATH;
const entries = [
{ path: "models.providers.openai.apiKey", value: openaiApiKey },
{
path: "models.providers.openai.baseUrl",
value: (process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").trim(),
},
{ path: "models.providers.openai.models", value: [] },
{ path: "gateway.controlUi.enabled", value: false },
{ path: "gateway.mode", value: "local" },
{ path: "gateway.bind", value: "lan" },
{ path: "gateway.auth.mode", value: "token" },
{ path: "gateway.auth.token", value: process.env.OPENCLAW_GATEWAY_TOKEN },
{ path: "gateway.http.endpoints.chatCompletions.enabled", value: true },
{ path: "agents.defaults.model.primary", value: process.env.OPENCLAW_OPENWEBUI_MODEL },
];
fs.writeFileSync(batchPath, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
NODE
OPENCLAW_CONFIG_BATCH_PATH="$batch_file" node scripts/e2e/lib/fixture.mjs openwebui-config "$openai_api_key"
node "$entry" config set --batch-file "$batch_file" >/dev/null
rm -f "$batch_file"
workspace="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
mkdir -p "$workspace/.openclaw"
cat > "$workspace/IDENTITY.md" <<'"'"'EOF'"'"'
# Identity
- Name: OpenClaw
- Purpose: Open WebUI Docker compatibility smoke test assistant.
EOF
cat > "$workspace/.openclaw/workspace-state.json" <<'"'"'EOF'"'"'
{
"version": 1,
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
EOF
rm -f "$workspace/BOOTSTRAP.md"
node scripts/e2e/lib/fixture.mjs openwebui-workspace
openclaw_e2e_exec_gateway "$entry" '"$PORT"' lan /tmp/openwebui-gateway.log
' >/dev/null

View File

@@ -52,79 +52,12 @@ mkdir -p "$git_root"
tar -xzf "$package_tgz" -C "$git_root" --strip-components=1
# The package-derived fixture can carry patchedDependencies whose targets are
# absent from the trimmed tarball install; that should not block update preflight.
node - <<'"'"'NODE'"'"'
const fs = require("node:fs");
const path = require("node:path");
const packageJsonPath = "/tmp/openclaw-git/package.json";
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const isLegacyPackageAcceptanceCompat = (version) => {
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/.exec(version || "");
if (!match) return false;
const value = [Number(match[1]), Number(match[2]), Number(match[3])];
const max = [2026, 4, 25];
for (let i = 0; i < value.length; i += 1) {
if (value[i] < max[i]) return true;
if (value[i] > max[i]) return false;
}
return true;
};
const fixtureUiBuildSource = `const fs=require("node:fs");fs.mkdirSync("dist/control-ui",{recursive:true});fs.writeFileSync("dist/control-ui/index.html","<!doctype html><title>fixture</title>\\n")`;
const fixtureUiBuildCommand = `node -e ${JSON.stringify(fixtureUiBuildSource)}`;
const nextPnpm = { ...packageJson.pnpm, allowUnusedPatches: true };
const patchedDependencies = nextPnpm.patchedDependencies;
if (
patchedDependencies &&
typeof patchedDependencies === "object" &&
!Array.isArray(patchedDependencies)
) {
const patchEntries = Object.entries(patchedDependencies);
const keptPatches = Object.fromEntries(
patchEntries.filter(([, patchFile]) => {
return (
typeof patchFile === "string" &&
fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile))
);
}),
);
const missingPatches = patchEntries.filter(([dependency, patchFile]) => {
return (
typeof patchFile !== "string" ||
!fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile))
);
});
if (missingPatches.length > 0 && !isLegacyPackageAcceptanceCompat(packageJson.version)) {
throw new Error(
`package ${packageJson.version} has missing pnpm.patchedDependencies in package fixture: ${missingPatches
.map(([dependency, patchFile]) => `${dependency} -> ${patchFile}`)
.join(", ")}`,
);
}
if (Object.keys(keptPatches).length > 0) {
nextPnpm.patchedDependencies = keptPatches;
} else {
delete nextPnpm.patchedDependencies;
}
}
packageJson.pnpm = nextPnpm;
packageJson.scripts = {
...packageJson.scripts,
build: "node -e \"console.log(\\\"fixture build skipped\\\")\"",
lint: "node -e \"console.log(\\\"fixture lint skipped\\\")\"",
"ui:build": fixtureUiBuildCommand,
};
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
fs.mkdirSync("/tmp/openclaw-git/dist/control-ui", { recursive: true });
fs.writeFileSync("/tmp/openclaw-git/dist/control-ui/index.html", "<!doctype html><title>fixture</title>\n");
NODE
node scripts/e2e/lib/update-channel-switch/assertions.mjs prepare-git-fixture "$git_root"
(
cd "$git_root"
npm install --omit=optional --no-fund --no-audit >/tmp/openclaw-git-install.log 2>&1
)
node - <<'"'"'NODE'"'"'
const fs = require("node:fs");
fs.mkdirSync("/tmp/openclaw-git/dist/control-ui", { recursive: true });
fs.writeFileSync("/tmp/openclaw-git/dist/control-ui/index.html", "<!doctype html><title>fixture</title>\n");
NODE
node scripts/e2e/lib/update-channel-switch/assertions.mjs write-control-ui "$git_root"
git config --global user.email "docker-e2e@openclaw.local"
git config --global user.name "OpenClaw Docker E2E"
@@ -159,41 +92,12 @@ printf "%s\n" "$dev_json"
if [ "$dev_status" -ne 0 ]; then
exit "$dev_status"
fi
DEV_JSON="$dev_json" node - <<'"'"'NODE'"'"'
const payload = JSON.parse(process.env.DEV_JSON);
if (payload.status !== "ok") {
throw new Error(`expected dev update status ok, got ${payload.status}`);
}
if (payload.mode !== "git") {
throw new Error(`expected dev update mode git, got ${payload.mode}`);
}
if (payload.postUpdate?.plugins && payload.postUpdate.plugins.status !== "ok") {
throw new Error(`expected plugin post-update ok, got ${JSON.stringify(payload.postUpdate?.plugins)}`);
}
NODE
node - <<'"'"'NODE'"'"'
const fs = require("node:fs");
const path = require("node:path");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (config.update?.channel !== "dev") {
if (process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1") {
console.log(`legacy package did not persist update.channel dev; got ${JSON.stringify(config.update?.channel)}`);
} else {
throw new Error(`expected persisted update.channel dev, got ${JSON.stringify(config.update?.channel)}`);
}
}
NODE
UPDATE_JSON="$dev_json" node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-update dev
node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-config-channel dev
status_json="$(openclaw update status --json)"
printf "%s\n" "$status_json"
STATUS_JSON="$status_json" node - <<'"'"'NODE'"'"'
const payload = JSON.parse(process.env.STATUS_JSON);
if (payload.update?.installKind !== "git") {
throw new Error(`expected git install after dev switch, got ${payload.update?.installKind}`);
}
NODE
STATUS_JSON="$status_json" node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-status-kind git
echo "==> git -> package stable channel"
set +e
@@ -204,41 +108,12 @@ printf "%s\n" "$stable_json"
if [ "$stable_status" -ne 0 ]; then
exit "$stable_status"
fi
STABLE_JSON="$stable_json" node - <<'"'"'NODE'"'"'
const payload = JSON.parse(process.env.STABLE_JSON);
if (payload.status !== "ok") {
throw new Error(`expected stable update status ok, got ${payload.status}`);
}
if (!["npm", "pnpm", "bun"].includes(payload.mode)) {
throw new Error(`expected package-manager mode after stable switch, got ${payload.mode}`);
}
if (payload.postUpdate?.plugins && payload.postUpdate.plugins.status !== "ok") {
throw new Error(`expected plugin post-update ok, got ${JSON.stringify(payload.postUpdate?.plugins)}`);
}
NODE
node - <<'"'"'NODE'"'"'
const fs = require("node:fs");
const path = require("node:path");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (config.update?.channel !== "stable") {
if (process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1") {
console.log(`legacy package did not persist update.channel stable; got ${JSON.stringify(config.update?.channel)}`);
} else {
throw new Error(`expected persisted update.channel stable, got ${JSON.stringify(config.update?.channel)}`);
}
}
NODE
UPDATE_JSON="$stable_json" node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-update stable
node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-config-channel stable
status_json="$(openclaw update status --json)"
printf "%s\n" "$status_json"
STATUS_JSON="$status_json" node - <<'"'"'NODE'"'"'
const payload = JSON.parse(process.env.STATUS_JSON);
if (payload.update?.installKind !== "package") {
throw new Error(`expected package install after stable switch, got ${payload.update?.installKind}`);
}
NODE
STATUS_JSON="$status_json" node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-status-kind package
echo "OK"
'