diff --git a/scripts/e2e/agents-delete-shared-workspace-docker.sh b/scripts/e2e/agents-delete-shared-workspace-docker.sh index cf64e95ea71..6a8f0e988bc 100644 --- a/scripts/e2e/agents-delete-shared-workspace-docker.sh +++ b/scripts/e2e/agents-delete-shared-workspace-docker.sh @@ -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" ' diff --git a/scripts/e2e/browser-cdp-snapshot-docker.sh b/scripts/e2e/browser-cdp-snapshot-docker.sh index 6b1d4beda9f..62bc9174c6e 100755 --- a/scripts/e2e/browser-cdp-snapshot-docker.sh +++ b/scripts/e2e/browser-cdp-snapshot-docker.sh @@ -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 \\ diff --git a/scripts/e2e/config-reload-source-docker.sh b/scripts/e2e/config-reload-source-docker.sh index 72c915b3ec1..a893ba114aa 100755 --- a/scripts/e2e/config-reload-source-docker.sh +++ b/scripts/e2e/config-reload-source-docker.sh @@ -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..." diff --git a/scripts/e2e/lib/fixture.mjs b/scripts/e2e/lib/fixture.mjs new file mode 100644 index 00000000000..f2a1622143b --- /dev/null +++ b/scripts/e2e/lib/fixture.mjs @@ -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); diff --git a/scripts/e2e/lib/openai-web-search-minimal/scenario.sh b/scripts/e2e/lib/openai-web-search-minimal/scenario.sh index cb7aead4308..aff18bec491 100644 --- a/scripts/e2e/lib/openai-web-search-minimal/scenario.sh +++ b/scripts/e2e/lib/openai-web-search-minimal/scenario.sh @@ -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.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": () => diff --git a/scripts/e2e/lib/plugins/fixtures.sh b/scripts/e2e/lib/plugins/fixtures.sh index 6b50e7d2cc4..7320a8c9797 100644 --- a/scripts/e2e/lib/plugins/fixtures.sh +++ b/scripts/e2e/lib/plugins/fixtures.sh @@ -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" <"$dir/index.js" < ({ 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" } diff --git a/scripts/e2e/lib/plugins/marketplace.sh b/scripts/e2e/lib/plugins/marketplace.sh index 9f2defc86f1..c42e9a3a81e 100644 --- a/scripts/e2e/lib/plugins/marketplace.sh +++ b/scripts/e2e/lib/plugins/marketplace.sh @@ -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" </tmp/marketplace-list.json diff --git a/scripts/e2e/lib/update-channel-switch/assertions.mjs b/scripts/e2e/lib/update-channel-switch/assertions.mjs new file mode 100644 index 00000000000..5e9c938fcce --- /dev/null +++ b/scripts/e2e/lib/update-channel-switch/assertions.mjs @@ -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 = "fixture\n"; + +function usage() { + console.error( + "usage: assertions.mjs [...]", + ); + 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(); +} diff --git a/scripts/e2e/openwebui-docker.sh b/scripts/e2e/openwebui-docker.sh index c7dfbcd68b4..26623a74f42 100755 --- a/scripts/e2e/openwebui-docker.sh +++ b/scripts/e2e/openwebui-docker.sh @@ -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 diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh index 64d0a6c4e3c..cd358c66c87 100755 --- a/scripts/e2e/update-channel-switch-docker.sh +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -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","fixture\\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", "fixture\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", "fixture\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" '