mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
refactor(test): split e2e fixture helpers
This commit is contained in:
@@ -12,63 +12,9 @@ export OPENCLAW_DISABLE_BUNDLED_PLUGINS=1
|
||||
# Stub systemd/loginctl so doctor + daemon flows work in Docker.
|
||||
export PATH="/tmp/openclaw-bin:$PATH"
|
||||
mkdir -p /tmp/openclaw-bin
|
||||
|
||||
cat >/tmp/openclaw-bin/systemctl <<"SYSTEMCTL"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
args=("$@")
|
||||
if [[ "${args[0]:-}" == "--user" ]]; then
|
||||
args=("${args[@]:1}")
|
||||
fi
|
||||
cmd="${args[0]:-}"
|
||||
case "$cmd" in
|
||||
status)
|
||||
exit 0
|
||||
;;
|
||||
is-active)
|
||||
echo "inactive" >&2
|
||||
exit 3
|
||||
;;
|
||||
is-enabled)
|
||||
unit="${args[1]:-}"
|
||||
unit_path="$HOME/.config/systemd/user/${unit}"
|
||||
if [ -f "$unit_path" ]; then
|
||||
echo "enabled"
|
||||
exit 0
|
||||
fi
|
||||
echo "disabled" >&2
|
||||
exit 1
|
||||
;;
|
||||
show)
|
||||
echo "ActiveState=inactive"
|
||||
echo "SubState=dead"
|
||||
echo "MainPID=0"
|
||||
echo "ExecMainStatus=0"
|
||||
echo "ExecMainCode=0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
SYSTEMCTL
|
||||
chmod +x /tmp/openclaw-bin/systemctl
|
||||
|
||||
cat >/tmp/openclaw-bin/loginctl <<"LOGINCTL"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$*" == *"show-user"* ]]; then
|
||||
echo "Linger=yes"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$*" == *"enable-linger"* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
LOGINCTL
|
||||
chmod +x /tmp/openclaw-bin/loginctl
|
||||
cp scripts/e2e/lib/doctor-install-switch/shims/systemctl /tmp/openclaw-bin/systemctl
|
||||
cp scripts/e2e/lib/doctor-install-switch/shims/loginctl /tmp/openclaw-bin/loginctl
|
||||
chmod +x /tmp/openclaw-bin/systemctl /tmp/openclaw-bin/loginctl
|
||||
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
git_root="/tmp/openclaw-git"
|
||||
|
||||
7
scripts/e2e/lib/doctor-install-switch/shims/loginctl
Executable file
7
scripts/e2e/lib/doctor-install-switch/shims/loginctl
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
case "$*" in
|
||||
*show-user*) echo "Linger=yes" ;;
|
||||
*enable-linger*) ;;
|
||||
esac
|
||||
34
scripts/e2e/lib/doctor-install-switch/shims/systemctl
Executable file
34
scripts/e2e/lib/doctor-install-switch/shims/systemctl
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
args=("$@")
|
||||
if [[ "${args[0]:-}" == "--user" ]]; then
|
||||
args=("${args[@]:1}")
|
||||
fi
|
||||
cmd="${args[0]:-}"
|
||||
|
||||
case "$cmd" in
|
||||
status) ;;
|
||||
is-active)
|
||||
echo "inactive" >&2
|
||||
exit 3
|
||||
;;
|
||||
is-enabled)
|
||||
unit="${args[1]:-}"
|
||||
unit_path="$HOME/.config/systemd/user/${unit}"
|
||||
if [ -f "$unit_path" ]; then
|
||||
echo "enabled"
|
||||
exit 0
|
||||
fi
|
||||
echo "disabled" >&2
|
||||
exit 1
|
||||
;;
|
||||
show)
|
||||
printf "%s\n" \
|
||||
"ActiveState=inactive" \
|
||||
"SubState=dead" \
|
||||
"MainPID=0" \
|
||||
"ExecMainStatus=0" \
|
||||
"ExecMainCode=0"
|
||||
;;
|
||||
esac
|
||||
@@ -1,283 +1,16 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { configCommands } from "./fixtures/config.mjs";
|
||||
import { pluginCommands } from "./fixtures/plugins.mjs";
|
||||
import { workspaceCommands } from "./fixtures/workspace.mjs";
|
||||
|
||||
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: {} } });
|
||||
const handler = {
|
||||
...pluginCommands,
|
||||
...configCommands,
|
||||
...workspaceCommands,
|
||||
}[command];
|
||||
if (!handler) {
|
||||
throw new Error(`unknown fixture command: ${command}`);
|
||||
}
|
||||
|
||||
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);
|
||||
handler(args);
|
||||
|
||||
24
scripts/e2e/lib/fixtures/common.mjs
Normal file
24
scripts/e2e/lib/fixtures/common.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
||||
export const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
|
||||
export const write = (file, contents) => {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, contents);
|
||||
};
|
||||
export const writeJson = (file, value) => write(file, json(value));
|
||||
|
||||
export const requireArg = (value, name) => {
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const assert = (condition, message) => {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
115
scripts/e2e/lib/fixtures/config.mjs
Normal file
115
scripts/e2e/lib/fixtures/config.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
import path from "node:path";
|
||||
import { requireArg, writeJson } from "./common.mjs";
|
||||
|
||||
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 },
|
||||
]);
|
||||
}
|
||||
|
||||
export const configCommands = {
|
||||
"config-reload": () => writeConfig("config-reload"),
|
||||
"browser-cdp": () => writeConfig("browser-cdp"),
|
||||
"openai-web-search-minimal-config": writeOpenAiWebSearchMinimalConfig,
|
||||
"openwebui-config": writeOpenWebUiConfig,
|
||||
};
|
||||
82
scripts/e2e/lib/fixtures/plugins.mjs
Normal file
82
scripts/e2e/lib/fixtures/plugins.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
import path from "node:path";
|
||||
import { requireArg, write, writeJson } from "./common.mjs";
|
||||
|
||||
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" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const pluginCommands = {
|
||||
"plugin-demo": writePluginDemo,
|
||||
plugin: writePlugin,
|
||||
"plugin-manifest": ([file, id]) =>
|
||||
writePluginManifest(requireArg(file, "file"), requireArg(id, "id")),
|
||||
"claude-bundle": writeClaudeBundle,
|
||||
marketplace: writePluginMarketplace,
|
||||
};
|
||||
70
scripts/e2e/lib/fixtures/workspace.mjs
Normal file
70
scripts/e2e/lib/fixtures/workspace.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { assert, readJson, requireArg, write, writeJson } from "./common.mjs";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
export const workspaceCommands = {
|
||||
"openwebui-workspace": writeOpenWebUiWorkspace,
|
||||
"agents-delete-config": writeAgentsDeleteConfig,
|
||||
"agents-delete-assert": assertAgentsDeleteResult,
|
||||
};
|
||||
13
scripts/e2e/lib/npm-telegram-live/prepare-package.mjs
Normal file
13
scripts/e2e/lib/npm-telegram-live/prepare-package.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
for (const packageJsonPath of process.argv.slice(2)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
pkg.exports = pkg.exports && typeof pkg.exports === "object" ? pkg.exports : {};
|
||||
if (!pkg.exports["./plugin-sdk/gateway-runtime"]) {
|
||||
pkg.exports["./plugin-sdk/gateway-runtime"] = {
|
||||
types: "./dist/plugin-sdk/gateway-runtime.d.ts",
|
||||
default: "./dist/plugin-sdk/gateway-runtime.js",
|
||||
};
|
||||
}
|
||||
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
||||
}
|
||||
32
scripts/e2e/lib/onboard/log-contains.mjs
Normal file
32
scripts/e2e/lib/onboard/log-contains.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
const [file, needle] = process.argv.slice(2);
|
||||
if (!file || !needle) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let text = "";
|
||||
try {
|
||||
text = fs.readFileSync(file, "utf8");
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (text.length > 120000) {
|
||||
text = text.slice(-120000);
|
||||
}
|
||||
|
||||
const normalizeScriptOutput = (value) => value.replace(/\r?\n/g, "").replace(/\r/g, "");
|
||||
const oscPattern = new RegExp(String.raw`\u001b\][^\u0007]*(?:\u0007|\u001b\\)`, "g");
|
||||
const csiPattern = new RegExp(String.raw`\u001b\[[0-?]*[ -/]*[@-~]`, "g");
|
||||
|
||||
const stripAnsi = (value) =>
|
||||
normalizeScriptOutput(value).replace(oscPattern, "").replace(csiPattern, "");
|
||||
|
||||
const compact = (value) =>
|
||||
stripAnsi(value)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z]+/g, "");
|
||||
const compactNeedle = compact(needle);
|
||||
|
||||
process.exit(compactNeedle && compact(text).includes(compactNeedle) ? 0 : 1);
|
||||
@@ -23,8 +23,6 @@ wait_for_log() {
|
||||
local needle="$1"
|
||||
local timeout_s="${2:-45}"
|
||||
local quiet_on_timeout="${3:-false}"
|
||||
local needle_compact
|
||||
needle_compact="$(printf "%s" "$needle" | tr -cd "[:alpha:]")"
|
||||
local start_s
|
||||
start_s="$(date +%s)"
|
||||
while true; do
|
||||
@@ -32,34 +30,7 @@ wait_for_log() {
|
||||
if grep -a -F -q "$needle" "$WIZARD_LOG_PATH"; then
|
||||
return 0
|
||||
fi
|
||||
if NEEDLE=\"$needle_compact\" node --input-type=module -e "
|
||||
import fs from \"node:fs\";
|
||||
const file = process.env.WIZARD_LOG_PATH;
|
||||
const needle = process.env.NEEDLE ?? \"\";
|
||||
let text = \"\";
|
||||
try { text = fs.readFileSync(file, \"utf8\"); } catch { process.exit(1); }
|
||||
// Clack/script output can include lots of control sequences; keep a larger tail and strip ANSI more robustly.
|
||||
if (text.length > 120000) text = text.slice(-120000);
|
||||
const normalizeScriptOutput = (value) =>
|
||||
value
|
||||
// util-linux script can emit each byte on its own CRLF-delimited line.
|
||||
// Collapse those first so ANSI/control stripping works on real sequences.
|
||||
.replace(/\\r?\\n/g, \"\")
|
||||
.replace(/\\r/g, \"\");
|
||||
const stripAnsi = (value) =>
|
||||
normalizeScriptOutput(value)
|
||||
// OSC: ESC ] ... BEL or ESC \\
|
||||
.replace(/\\x1b\\][^\\x07]*(?:\\x07|\\x1b\\\\)/g, \"\")
|
||||
// CSI: ESC [ ... cmd
|
||||
.replace(/\\x1b\\[[0-?]*[ -/]*[@-~]/g, \"\");
|
||||
// Letters-only: script output sometimes fragments ANSI sequences into digits/letters that
|
||||
// can otherwise break substring matching.
|
||||
const compact = (value) => stripAnsi(value).toLowerCase().replace(/[^a-z]+/g, \"\");
|
||||
const haystack = compact(text);
|
||||
const compactNeedle = compact(needle);
|
||||
if (!compactNeedle) process.exit(1);
|
||||
process.exit(haystack.includes(compactNeedle) ? 0 : 1);
|
||||
"; then
|
||||
if node scripts/e2e/lib/onboard/log-contains.mjs "$WIZARD_LOG_PATH" "$needle"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
parallels_package_current_build_commit() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
path = pathlib.Path("dist/build-info.json")
|
||||
if not path.exists():
|
||||
print("")
|
||||
else:
|
||||
print(json.loads(path.read_text()).get("commit", ""))
|
||||
PY
|
||||
node scripts/e2e/lib/parallels-package/build-info-commit.mjs
|
||||
}
|
||||
|
||||
parallels_package_acquire_build_lock() {
|
||||
@@ -66,35 +57,9 @@ parallels_package_assert_no_generated_drift() {
|
||||
}
|
||||
|
||||
parallels_log_progress_extract() {
|
||||
local python_bin="$1"
|
||||
local _python_bin="$1"
|
||||
local log_path="$2"
|
||||
"$python_bin" - "$log_path" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
if not path.exists():
|
||||
print("")
|
||||
raise SystemExit(0)
|
||||
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
|
||||
for line in reversed(lines):
|
||||
if line.startswith("==> "):
|
||||
print(line[4:].strip())
|
||||
raise SystemExit(0)
|
||||
|
||||
for line in reversed(lines):
|
||||
if line.startswith("warn:") or line.startswith("error:"):
|
||||
print(line)
|
||||
raise SystemExit(0)
|
||||
|
||||
if lines:
|
||||
print(lines[-1][:240])
|
||||
else:
|
||||
print("")
|
||||
PY
|
||||
node scripts/e2e/lib/parallels-package/log-progress-extract.mjs "$log_path"
|
||||
}
|
||||
|
||||
parallels_child_job_running() {
|
||||
|
||||
9
scripts/e2e/lib/parallels-package/build-info-commit.mjs
Normal file
9
scripts/e2e/lib/parallels-package/build-info-commit.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
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 ?? "");
|
||||
}
|
||||
18
scripts/e2e/lib/parallels-package/log-progress-extract.mjs
Normal file
18
scripts/e2e/lib/parallels-package/log-progress-extract.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
const [logPath] = process.argv.slice(2);
|
||||
if (!logPath || !fs.existsSync(logPath)) {
|
||||
console.log("");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const text = fs.readFileSync(logPath, "utf8");
|
||||
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) ?? "");
|
||||
@@ -233,24 +233,9 @@ ln -sfnT /app/extensions "$openclaw_package_dir/extensions"
|
||||
mkdir -p /app/node_modules/@openclaw
|
||||
rm -rf /app/node_modules/@openclaw/qa-channel
|
||||
ln -sfnT /app/extensions/qa-channel /app/node_modules/@openclaw/qa-channel
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
|
||||
for (const packageJsonPath of [
|
||||
"/app/package.json",
|
||||
"/app/node_modules/openclaw/package.json",
|
||||
]) {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
pkg.exports = pkg.exports && typeof pkg.exports === "object" ? pkg.exports : {};
|
||||
if (!pkg.exports["./plugin-sdk/gateway-runtime"]) {
|
||||
pkg.exports["./plugin-sdk/gateway-runtime"] = {
|
||||
types: "./dist/plugin-sdk/gateway-runtime.d.ts",
|
||||
default: "./dist/plugin-sdk/gateway-runtime.js",
|
||||
};
|
||||
}
|
||||
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
||||
}
|
||||
NODE
|
||||
node scripts/e2e/lib/npm-telegram-live/prepare-package.mjs \
|
||||
/app/package.json \
|
||||
/app/node_modules/openclaw/package.json
|
||||
for deps_dir in "$openclaw_package_dir/node_modules" /npm-global/lib/node_modules; do
|
||||
[ -d "$deps_dir" ] || continue
|
||||
for dependency_dir in "$deps_dir"/*; do
|
||||
|
||||
Reference in New Issue
Block a user