mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:50:50 +00:00
refactor: consolidate e2e fixture helpers
This commit is contained in:
@@ -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"
|
||||
'
|
||||
|
||||
@@ -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 \\
|
||||
|
||||
@@ -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
283
scripts/e2e/lib/fixture.mjs
Normal 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);
|
||||
@@ -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" \
|
||||
|
||||
@@ -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": () =>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
124
scripts/e2e/lib/update-channel-switch/assertions.mjs
Normal file
124
scripts/e2e/lib/update-channel-switch/assertions.mjs
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
'
|
||||
|
||||
Reference in New Issue
Block a user