mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
refactor: extract remaining docker e2e scenarios
This commit is contained in:
@@ -118,26 +118,13 @@ NODE
|
||||
openclaw_e2e_exec_gateway \"\$entry\" $PORT loopback /tmp/browser-cdp-gateway.log" >/dev/null
|
||||
|
||||
echo "Waiting for Chromium and Gateway..."
|
||||
ready=0
|
||||
for _ in $(seq 1 180); do
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)" != "true" ]; then
|
||||
break
|
||||
fi
|
||||
if docker_cmd docker exec "$CONTAINER_NAME" bash -lc "
|
||||
if ! docker_e2e_wait_container_bash "$CONTAINER_NAME" 180 0.5 "
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
openclaw_e2e_probe_http_status http://127.0.0.1:$CDP_PORT/json/version
|
||||
openclaw_e2e_probe_tcp 127.0.0.1 $PORT
|
||||
" >/dev/null 2>&1; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
"; then
|
||||
echo "Browser CDP snapshot container failed to become ready"
|
||||
docker_cmd docker logs "$CONTAINER_NAME" 2>&1 | tail -n 120 || true
|
||||
docker_cmd docker exec "$CONTAINER_NAME" bash -lc "tail -n 120 /tmp/browser-cdp-chromium.log /tmp/browser-cdp-gateway.log /tmp/browser-cdp-fixture.log" || true
|
||||
docker_e2e_tail_container_file_if_running "$CONTAINER_NAME" "/tmp/browser-cdp-chromium.log /tmp/browser-cdp-gateway.log /tmp/browser-cdp-fixture.log" 120
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -43,25 +43,9 @@ docker_cmd docker run -d \
|
||||
bash -lc "set -euo pipefail; source scripts/lib/openclaw-e2e-instance.sh; entry=\"\$(openclaw_e2e_resolve_entrypoint)\"; node \"\$entry\" config set gateway.controlUi.enabled false >/dev/null; openclaw_e2e_exec_gateway \"\$entry\" $PORT lan /tmp/gateway-net-e2e.log" >/dev/null
|
||||
|
||||
echo "Waiting for gateway to come up..."
|
||||
ready=0
|
||||
for _ in $(seq 1 180); do
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" != "true" ]; then
|
||||
break
|
||||
fi
|
||||
if docker_cmd docker exec "$GW_NAME" bash -lc "source scripts/lib/openclaw-e2e-instance.sh; openclaw_e2e_probe_tcp 127.0.0.1 $PORT || grep -q \"listening on ws://\" /tmp/gateway-net-e2e.log 2>/dev/null"; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 180 0.5 "source scripts/lib/openclaw-e2e-instance.sh; openclaw_e2e_probe_tcp 127.0.0.1 $PORT || grep -q \"listening on ws://\" /tmp/gateway-net-e2e.log 2>/dev/null"; then
|
||||
echo "Gateway failed to start"
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" = "true" ]; then
|
||||
docker_cmd docker exec "$GW_NAME" bash -lc "tail -n 80 /tmp/gateway-net-e2e.log" || true
|
||||
else
|
||||
docker_cmd docker logs "$GW_NAME" 2>&1 | tail -n 120 || true
|
||||
fi
|
||||
docker_e2e_tail_container_file_if_running "$GW_NAME" /tmp/gateway-net-e2e.log 120
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-kitchen-sink-plugin-e2e" OPENCL
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" kitchen-sink-plugin
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 kitchen-sink-plugin empty)"
|
||||
|
||||
DEFAULT_KITCHEN_SINK_SCENARIOS="$(cat <<'SCENARIOS'
|
||||
DEFAULT_KITCHEN_SINK_SCENARIOS="$(
|
||||
cat <<'SCENARIOS'
|
||||
npm-latest|npm:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|npm|success|full
|
||||
npm-beta|npm:@openclaw/kitchen-sink@beta|openclaw-kitchen-sink-fixture|npm|failure|none
|
||||
clawhub-latest|clawhub:openclaw-kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic
|
||||
@@ -21,660 +22,12 @@ MAX_CPU_PERCENT="${OPENCLAW_KITCHEN_SINK_MAX_CPU_PERCENT:-1200}"
|
||||
CONTAINER_NAME="openclaw-kitchen-sink-plugin-e2e-$$"
|
||||
RUN_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-kitchen-sink-plugin.XXXXXX")"
|
||||
STATS_LOG="$(mktemp "${TMPDIR:-/tmp}/openclaw-kitchen-sink-plugin-stats.XXXXXX")"
|
||||
SCRIPT_FILE="$(mktemp "${TMPDIR:-/tmp}/openclaw-kitchen-sink-plugin-script.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
rm -f "$SCRIPT_FILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat > "$SCRIPT_FILE" <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
if [ -f dist/index.mjs ]; then
|
||||
OPENCLAW_ENTRY="dist/index.mjs"
|
||||
elif [ -f dist/index.js ]; then
|
||||
OPENCLAW_ENTRY="dist/index.js"
|
||||
else
|
||||
echo "Missing dist/index.(m)js (build output):"
|
||||
ls -la dist || true
|
||||
exit 1
|
||||
fi
|
||||
export OPENCLAW_ENTRY
|
||||
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" | base64 -d)"
|
||||
|
||||
run_logged() {
|
||||
local label="$1"
|
||||
shift
|
||||
local log_file="/tmp/openclaw-kitchen-sink-${label}.log"
|
||||
if ! "$@" >"$log_file" 2>&1; then
|
||||
cat "$log_file"
|
||||
exit 1
|
||||
fi
|
||||
cat "$log_file"
|
||||
}
|
||||
|
||||
run_expect_failure() {
|
||||
local label="$1"
|
||||
shift
|
||||
local output_file="/tmp/kitchen-sink-expected-failure-${label}.txt"
|
||||
set +e
|
||||
"$@" >"$output_file" 2>&1
|
||||
local status="$?"
|
||||
set -e
|
||||
cat "$output_file"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "Expected ${label} to fail, but it succeeded." >&2
|
||||
exit 1
|
||||
fi
|
||||
node - "$output_file" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
|
||||
const output = fs.readFileSync(process.argv[2], "utf8");
|
||||
const source = process.env.KITCHEN_SINK_SOURCE;
|
||||
const spec = process.env.KITCHEN_SINK_SPEC;
|
||||
const displayedSpec = source === "npm" ? spec.replace(/^npm:/u, "") : spec;
|
||||
const expected =
|
||||
source === "clawhub"
|
||||
? /Version not found on ClawHub|ClawHub .* failed \(404\)|version.*not found/iu
|
||||
: /No matching version|ETARGET|notarget|npm (?:error|ERR!)/iu;
|
||||
if (!output.includes(displayedSpec)) {
|
||||
throw new Error(`expected failure output to mention ${displayedSpec}`);
|
||||
}
|
||||
if (!expected.test(output)) {
|
||||
throw new Error(`unexpected ${source} beta failure output:\n${output}`);
|
||||
}
|
||||
console.log("ok");
|
||||
NODE
|
||||
}
|
||||
|
||||
start_kitchen_sink_clawhub_fixture_server() {
|
||||
local fixture_dir="$1"
|
||||
local server_log="$fixture_dir/clawhub-fixture.log"
|
||||
local server_port_file="$fixture_dir/clawhub-fixture-port"
|
||||
local server_pid_file="$fixture_dir/clawhub-fixture-pid"
|
||||
|
||||
node - <<'NODE' "$server_port_file" >"$server_log" 2>&1 &
|
||||
const crypto = require("node:crypto");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
const portFile = process.argv[2];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const packageName = "openclaw-kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
const version = "0.1.3";
|
||||
|
||||
async function main() {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"package/package.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/index.js",
|
||||
`module.exports = {
|
||||
id: "${pluginId}",
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "kitchen-sink-provider",
|
||||
label: "Kitchen Sink Provider",
|
||||
docsPath: "/providers/kitchen-sink",
|
||||
auth: [],
|
||||
});
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "kitchen-sink-channel",
|
||||
meta: {
|
||||
id: "kitchen-sink-channel",
|
||||
label: "Kitchen Sink Channel",
|
||||
selectionLabel: "Kitchen Sink",
|
||||
docsPath: "/channels/kitchen-sink",
|
||||
blurb: "Kitchen sink ClawHub fixture channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/openclaw.plugin.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
const packageDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
runtimeId: pluginId,
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
summary: "Kitchen sink plugin fixture for prerelease CI.",
|
||||
ownerHandle: "openclaw",
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
latestVersion: version,
|
||||
tags: { latest: version },
|
||||
capabilityTags: ["test-fixture"],
|
||||
executesCode: true,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.11",
|
||||
minGatewayVersion: "2026.4.11",
|
||||
},
|
||||
capabilities: {
|
||||
executesCode: true,
|
||||
runtimeId: pluginId,
|
||||
capabilityTags: ["test-fixture"],
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
},
|
||||
verification: {
|
||||
tier: "source-linked",
|
||||
sourceRepo: "https://github.com/openclaw/kitchen-sink",
|
||||
hasProvenance: false,
|
||||
scanStatus: "passed",
|
||||
},
|
||||
},
|
||||
};
|
||||
const versionDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
},
|
||||
version: {
|
||||
version,
|
||||
createdAt: 0,
|
||||
changelog: "Fixture package for kitchen-sink plugin prerelease CI.",
|
||||
distTags: ["latest"],
|
||||
sha256hash,
|
||||
compatibility: packageDetail.package.compatibility,
|
||||
capabilities: packageDetail.package.capabilities,
|
||||
verification: packageDetail.package.verification,
|
||||
},
|
||||
};
|
||||
|
||||
const json = (response, value, status = 200) => {
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405);
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}`) {
|
||||
json(response, packageDetail);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}`
|
||||
) {
|
||||
json(response, versionDetail);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/beta`) {
|
||||
json(response, { error: "version not found" }, 404);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/download`) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
});
|
||||
response.end(archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
require("node:fs").writeFileSync(portFile, String(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
local server_pid="$!"
|
||||
echo "$server_pid" > "$server_pid_file"
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
trap 'if [[ -f "'"$server_pid_file"'" ]]; then kill "$(cat "'"$server_pid_file"'")" 2>/dev/null || true; fi' EXIT
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
cat "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
cat "$server_log"
|
||||
echo "Timed out waiting for kitchen-sink ClawHub fixture server." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
scan_logs_for_unexpected_errors() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const roots = ["/tmp", path.join(process.env.HOME, ".openclaw")];
|
||||
const files = [];
|
||||
const visit = (entry) => {
|
||||
if (!fs.existsSync(entry)) {
|
||||
return;
|
||||
}
|
||||
const stat = fs.statSync(entry);
|
||||
if (stat.isDirectory()) {
|
||||
for (const child of fs.readdirSync(entry)) {
|
||||
visit(path.join(entry, child));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (/\.(?:log|jsonl)$/u.test(entry) || /openclaw-kitchen-sink-/u.test(path.basename(entry))) {
|
||||
if (entry.includes("/.npm/_logs/")) {
|
||||
return;
|
||||
}
|
||||
files.push(entry);
|
||||
}
|
||||
};
|
||||
for (const root of roots) {
|
||||
visit(root);
|
||||
}
|
||||
|
||||
const deny = [
|
||||
/\buncaught exception\b/iu,
|
||||
/\bunhandled rejection\b/iu,
|
||||
/\bfatal\b/iu,
|
||||
/\bpanic\b/iu,
|
||||
/\blevel["']?\s*:\s*["']error["']/iu,
|
||||
/\[(?:error|ERROR)\]/u,
|
||||
];
|
||||
const allow = [
|
||||
/0 errors?/iu,
|
||||
/expected no diagnostics errors?/iu,
|
||||
/diagnostics errors?:\s*$/iu,
|
||||
];
|
||||
const findings = [];
|
||||
for (const file of files) {
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
const lines = text.split(/\r?\n/u);
|
||||
lines.forEach((line, index) => {
|
||||
if (allow.some((pattern) => pattern.test(line))) {
|
||||
return;
|
||||
}
|
||||
if (deny.some((pattern) => pattern.test(line))) {
|
||||
findings.push(`${file}:${index + 1}: ${line}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (findings.length > 0) {
|
||||
throw new Error(`unexpected error-like log lines:\n${findings.join("\n")}`);
|
||||
}
|
||||
console.log(`log scan passed (${files.length} file(s))`);
|
||||
NODE
|
||||
}
|
||||
|
||||
configure_kitchen_sink_runtime() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const pluginId = process.env.KITCHEN_SINK_ID;
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
config.plugins = config.plugins || {};
|
||||
config.plugins.entries = config.plugins.entries || {};
|
||||
config.plugins.entries[pluginId] = {
|
||||
...(config.plugins.entries[pluginId] || {}),
|
||||
hooks: {
|
||||
...(config.plugins.entries[pluginId]?.hooks || {}),
|
||||
allowConversationAccess: true,
|
||||
},
|
||||
};
|
||||
config.channels = {
|
||||
...(config.channels || {}),
|
||||
"kitchen-sink-channel": { enabled: true, token: "kitchen-sink-ci" },
|
||||
};
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
||||
NODE
|
||||
}
|
||||
|
||||
remove_kitchen_sink_channel_config() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
if (fs.existsSync(configPath)) {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
delete config.channels?.["kitchen-sink-channel"];
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_kitchen_sink_installed() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const pluginId = process.env.KITCHEN_SINK_ID;
|
||||
const spec = process.env.KITCHEN_SINK_SPEC;
|
||||
const source = process.env.KITCHEN_SINK_SOURCE;
|
||||
const surfaceMode = process.env.KITCHEN_SINK_SURFACE_MODE;
|
||||
const label = process.env.KITCHEN_SINK_LABEL;
|
||||
const list = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-plugins.json`, "utf8"));
|
||||
const inspect = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-inspect.json`, "utf8"));
|
||||
const allInspect = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-inspect-all.json`, "utf8"));
|
||||
const plugin = (list.plugins || []).find((entry) => entry.id === pluginId);
|
||||
if (!plugin) throw new Error(`kitchen-sink plugin not found after install: ${pluginId}`);
|
||||
if (plugin.status !== "loaded") {
|
||||
throw new Error(`unexpected kitchen-sink status after enable: ${plugin.status}`);
|
||||
}
|
||||
if (inspect.plugin?.id !== pluginId) {
|
||||
throw new Error(`unexpected inspected kitchen-sink plugin id: ${inspect.plugin?.id}`);
|
||||
}
|
||||
if (inspect.plugin?.enabled !== true || inspect.plugin?.status !== "loaded") {
|
||||
throw new Error(
|
||||
`expected enabled loaded kitchen-sink plugin, got enabled=${inspect.plugin?.enabled} status=${inspect.plugin?.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectIncludes = (listValue, expected, field) => {
|
||||
if (!Array.isArray(listValue) || !listValue.includes(expected)) {
|
||||
throw new Error(`${field} missing ${expected}: ${JSON.stringify(listValue)}`);
|
||||
}
|
||||
};
|
||||
expectIncludes(inspect.plugin?.channelIds, "kitchen-sink-channel", "channels");
|
||||
expectIncludes(inspect.plugin?.providerIds, "kitchen-sink-provider", "providers");
|
||||
|
||||
const diagnostics = [
|
||||
...(list.diagnostics || []),
|
||||
...(inspect.diagnostics || []),
|
||||
...(allInspect.diagnostics || []),
|
||||
];
|
||||
const errorMessages = new Set(
|
||||
diagnostics
|
||||
.filter((diag) => diag?.level === "error")
|
||||
.map((diag) => String(diag.message || "")),
|
||||
);
|
||||
|
||||
if (surfaceMode === "full") {
|
||||
const toolNames = Array.isArray(inspect.tools)
|
||||
? inspect.tools.flatMap((entry) => (Array.isArray(entry?.names) ? entry.names : []))
|
||||
: [];
|
||||
expectIncludes(inspect.plugin?.speechProviderIds, "kitchen-sink-speech-provider", "speech providers");
|
||||
expectIncludes(
|
||||
inspect.plugin?.realtimeTranscriptionProviderIds,
|
||||
"kitchen-sink-realtime-transcription-provider",
|
||||
"realtime transcription providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.realtimeVoiceProviderIds,
|
||||
"kitchen-sink-realtime-voice-provider",
|
||||
"realtime voice providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.mediaUnderstandingProviderIds,
|
||||
"kitchen-sink-media-understanding-provider",
|
||||
"media understanding providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.imageGenerationProviderIds,
|
||||
"kitchen-sink-image-generation-provider",
|
||||
"image generation providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.videoGenerationProviderIds,
|
||||
"kitchen-sink-video-generation-provider",
|
||||
"video generation providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.musicGenerationProviderIds,
|
||||
"kitchen-sink-music-generation-provider",
|
||||
"music generation providers",
|
||||
);
|
||||
expectIncludes(inspect.plugin?.webFetchProviderIds, "kitchen-sink-web-fetch-provider", "web fetch providers");
|
||||
expectIncludes(inspect.plugin?.webSearchProviderIds, "kitchen-sink-web-search-provider", "web search providers");
|
||||
expectIncludes(inspect.plugin?.migrationProviderIds, "kitchen-sink-migration-provider", "migration providers");
|
||||
expectIncludes(inspect.plugin?.agentHarnessIds, "kitchen-sink-agent-harness", "agent harnesses");
|
||||
expectIncludes(inspect.services, "kitchen-sink-service", "services");
|
||||
expectIncludes(inspect.commands, "kitchen-sink-command", "commands");
|
||||
expectIncludes(toolNames, "kitchen-sink-tool", "tools");
|
||||
if ((inspect.plugin?.hookCount || 0) < 30 || !Array.isArray(inspect.typedHooks) || inspect.typedHooks.length < 30) {
|
||||
throw new Error(
|
||||
`expected kitchen-sink typed hooks to load, got hookCount=${inspect.plugin?.hookCount} typedHooks=${inspect.typedHooks?.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectedErrorMessages = new Set([
|
||||
"only bundled plugins can register agent tool result middleware",
|
||||
"cli registration missing explicit commands metadata",
|
||||
"only bundled plugins can register Codex app-server extension factories",
|
||||
"http route registration missing or invalid auth: /kitchen-sink/http-route",
|
||||
"plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider",
|
||||
]);
|
||||
for (const message of errorMessages) {
|
||||
if (!expectedErrorMessages.has(message)) {
|
||||
throw new Error(`unexpected kitchen-sink diagnostic error: ${message}`);
|
||||
}
|
||||
}
|
||||
for (const message of expectedErrorMessages) {
|
||||
if (!errorMessages.has(message)) {
|
||||
throw new Error(`missing expected kitchen-sink diagnostic error: ${message}`);
|
||||
}
|
||||
}
|
||||
} else if (errorMessages.size > 0) {
|
||||
throw new Error(`unexpected kitchen-sink diagnostic errors: ${[...errorMessages].join(", ")}`);
|
||||
}
|
||||
|
||||
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
||||
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
||||
const record = (index.installRecords ?? index.records ?? {})[pluginId];
|
||||
if (!record) throw new Error(`missing kitchen-sink install record for ${pluginId}`);
|
||||
if (record.source !== source) {
|
||||
throw new Error(`expected kitchen-sink install source=${source}, got ${record.source}`);
|
||||
}
|
||||
if (source === "npm") {
|
||||
const expectedSpec = spec.replace(/^npm:/u, "");
|
||||
if (record.spec !== expectedSpec) {
|
||||
throw new Error(`expected kitchen-sink npm spec ${expectedSpec}, got ${record.spec}`);
|
||||
}
|
||||
if (!record.resolvedVersion || !record.resolvedSpec) {
|
||||
throw new Error(`missing npm resolution metadata: ${JSON.stringify(record)}`);
|
||||
}
|
||||
} else if (source === "clawhub") {
|
||||
const value = spec.slice("clawhub:".length).trim();
|
||||
const slashIndex = value.lastIndexOf("/");
|
||||
const atIndex = value.lastIndexOf("@");
|
||||
const packageName = atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value;
|
||||
if (record.spec !== spec) {
|
||||
throw new Error(`expected kitchen-sink ClawHub spec ${spec}, got ${record.spec}`);
|
||||
}
|
||||
if (record.clawhubPackage !== packageName) {
|
||||
throw new Error(`expected ClawHub package ${packageName}, got ${record.clawhubPackage}`);
|
||||
}
|
||||
if (record.clawhubFamily !== "code-plugin" && record.clawhubFamily !== "bundle-plugin") {
|
||||
throw new Error(`unexpected ClawHub family: ${record.clawhubFamily}`);
|
||||
}
|
||||
if (!record.version || !record.integrity || !record.resolvedAt) {
|
||||
throw new Error(`missing ClawHub resolution metadata: ${JSON.stringify(record)}`);
|
||||
}
|
||||
}
|
||||
if (typeof record.installPath !== "string" || record.installPath.length === 0) {
|
||||
throw new Error("missing kitchen-sink install path");
|
||||
}
|
||||
const installPath = record.installPath.replace(/^~(?=$|\/)/u, process.env.HOME);
|
||||
if (!fs.existsSync(installPath)) {
|
||||
throw new Error(`kitchen-sink install path missing: ${record.installPath}`);
|
||||
}
|
||||
fs.writeFileSync(`/tmp/kitchen-sink-${label}-install-path.txt`, installPath, "utf8");
|
||||
console.log("ok");
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_kitchen_sink_removed() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const pluginId = process.env.KITCHEN_SINK_ID;
|
||||
const label = process.env.KITCHEN_SINK_LABEL;
|
||||
const list = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-uninstalled.json`, "utf8"));
|
||||
if ((list.plugins || []).some((entry) => entry.id === pluginId)) {
|
||||
throw new Error(`kitchen-sink plugin still listed after uninstall: ${pluginId}`);
|
||||
}
|
||||
|
||||
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
||||
const index = fs.existsSync(indexPath) ? JSON.parse(fs.readFileSync(indexPath, "utf8")) : {};
|
||||
const records = index.installRecords ?? index.records ?? {};
|
||||
if (records[pluginId]) {
|
||||
throw new Error(`kitchen-sink install record still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
if (config.plugins?.entries?.[pluginId]) {
|
||||
throw new Error(`kitchen-sink config entry still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
if ((config.plugins?.allow || []).includes(pluginId)) {
|
||||
throw new Error(`kitchen-sink allowlist still contains ${pluginId}`);
|
||||
}
|
||||
if ((config.plugins?.deny || []).includes(pluginId)) {
|
||||
throw new Error(`kitchen-sink denylist still contains ${pluginId}`);
|
||||
}
|
||||
if (config.channels?.["kitchen-sink-channel"]) {
|
||||
throw new Error("kitchen-sink channel config still present after uninstall");
|
||||
}
|
||||
const installPathFile = `/tmp/kitchen-sink-${label}-install-path.txt`;
|
||||
if (fs.existsSync(installPathFile)) {
|
||||
const installPath = fs.readFileSync(installPathFile, "utf8").trim();
|
||||
if (installPath && fs.existsSync(installPath)) {
|
||||
throw new Error(`kitchen-sink managed install directory still exists: ${installPath}`);
|
||||
}
|
||||
}
|
||||
console.log("ok");
|
||||
NODE
|
||||
}
|
||||
|
||||
run_success_scenario() {
|
||||
echo "Testing ${KITCHEN_SINK_LABEL} install from ${KITCHEN_SINK_SPEC}..."
|
||||
run_logged "install-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins install "$KITCHEN_SINK_SPEC"
|
||||
run_logged "enable-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins enable "$KITCHEN_SINK_ID"
|
||||
configure_kitchen_sink_runtime
|
||||
node "$OPENCLAW_ENTRY" plugins list --json > "/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-plugins.json"
|
||||
node "$OPENCLAW_ENTRY" plugins inspect "$KITCHEN_SINK_ID" --json > "/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-inspect.json"
|
||||
node "$OPENCLAW_ENTRY" plugins inspect --all --json > "/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-inspect-all.json"
|
||||
assert_kitchen_sink_installed
|
||||
if [ "$KITCHEN_SINK_SOURCE" = "clawhub" ]; then
|
||||
run_logged "uninstall-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins uninstall "$KITCHEN_SINK_SPEC" --force
|
||||
else
|
||||
run_logged "uninstall-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins uninstall "$KITCHEN_SINK_ID" --force
|
||||
fi
|
||||
remove_kitchen_sink_channel_config
|
||||
node "$OPENCLAW_ENTRY" plugins list --json > "/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-uninstalled.json"
|
||||
assert_kitchen_sink_removed
|
||||
}
|
||||
|
||||
run_failure_scenario() {
|
||||
echo "Testing expected ${KITCHEN_SINK_LABEL} install failure from ${KITCHEN_SINK_SPEC}..."
|
||||
run_expect_failure "install-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins install "$KITCHEN_SINK_SPEC"
|
||||
remove_kitchen_sink_channel_config
|
||||
node "$OPENCLAW_ENTRY" plugins list --json > "/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-uninstalled.json"
|
||||
assert_kitchen_sink_removed
|
||||
}
|
||||
|
||||
if [[ "$KITCHEN_SINK_SCENARIOS" == *"clawhub:"* ]] &&
|
||||
[[ "${OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB:-0}" != "1" ]] &&
|
||||
[[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]; then
|
||||
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-kitchen-sink-clawhub.XXXXXX")"
|
||||
start_kitchen_sink_clawhub_fixture_server "$clawhub_fixture_dir"
|
||||
fi
|
||||
|
||||
scenario_count=0
|
||||
while IFS='|' read -r label spec plugin_id source expectation surface_mode; do
|
||||
if [ -z "${label:-}" ] || [[ "$label" == \#* ]]; then
|
||||
continue
|
||||
fi
|
||||
scenario_count=$((scenario_count + 1))
|
||||
export KITCHEN_SINK_LABEL="$label"
|
||||
export KITCHEN_SINK_SPEC="$spec"
|
||||
export KITCHEN_SINK_ID="$plugin_id"
|
||||
export KITCHEN_SINK_SOURCE="$source"
|
||||
export KITCHEN_SINK_SURFACE_MODE="$surface_mode"
|
||||
case "$expectation" in
|
||||
success)
|
||||
run_success_scenario
|
||||
;;
|
||||
failure)
|
||||
run_failure_scenario
|
||||
;;
|
||||
*)
|
||||
echo "Unknown kitchen-sink expectation for ${label}: ${expectation}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done <<< "$KITCHEN_SINK_SCENARIOS"
|
||||
|
||||
if [ "$scenario_count" -eq 0 ]; then
|
||||
echo "No kitchen-sink plugin scenarios configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
scan_logs_for_unexpected_errors
|
||||
echo "kitchen-sink plugin Docker E2E passed (${scenario_count} scenario(s))"
|
||||
EOF
|
||||
|
||||
DOCKER_ENV_ARGS=(
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64"
|
||||
@@ -695,8 +48,9 @@ done
|
||||
|
||||
echo "Running kitchen-sink plugin Docker E2E..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
docker run --name "$CONTAINER_NAME" "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash -s \
|
||||
>"$RUN_LOG" 2>&1 < "$SCRIPT_FILE" &
|
||||
docker_e2e_harness_mount_args
|
||||
docker run --name "$CONTAINER_NAME" "${DOCKER_E2E_HARNESS_ARGS[@]}" "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash scripts/e2e/lib/kitchen-sink-plugin/sweep.sh \
|
||||
>"$RUN_LOG" 2>&1 &
|
||||
docker_pid="$!"
|
||||
|
||||
while kill -0 "$docker_pid" 2>/dev/null; do
|
||||
|
||||
628
scripts/e2e/lib/kitchen-sink-plugin/sweep.sh
Normal file
628
scripts/e2e/lib/kitchen-sink-plugin/sweep.sh
Normal file
@@ -0,0 +1,628 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
source scripts/lib/docker-e2e-logs.sh
|
||||
|
||||
OPENCLAW_ENTRY="$(openclaw_e2e_resolve_entrypoint)"
|
||||
export OPENCLAW_ENTRY
|
||||
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}"
|
||||
|
||||
run_expect_failure() {
|
||||
local label="$1"
|
||||
shift
|
||||
local output_file="/tmp/kitchen-sink-expected-failure-${label}.txt"
|
||||
set +e
|
||||
"$@" >"$output_file" 2>&1
|
||||
local status="$?"
|
||||
set -e
|
||||
cat "$output_file"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "Expected ${label} to fail, but it succeeded." >&2
|
||||
exit 1
|
||||
fi
|
||||
node - "$output_file" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
|
||||
const output = fs.readFileSync(process.argv[2], "utf8");
|
||||
const source = process.env.KITCHEN_SINK_SOURCE;
|
||||
const spec = process.env.KITCHEN_SINK_SPEC;
|
||||
const displayedSpec = source === "npm" ? spec.replace(/^npm:/u, "") : spec;
|
||||
const expected =
|
||||
source === "clawhub"
|
||||
? /Version not found on ClawHub|ClawHub .* failed \(404\)|version.*not found/iu
|
||||
: /No matching version|ETARGET|notarget|npm (?:error|ERR!)/iu;
|
||||
if (!output.includes(displayedSpec)) {
|
||||
throw new Error(`expected failure output to mention ${displayedSpec}`);
|
||||
}
|
||||
if (!expected.test(output)) {
|
||||
throw new Error(`unexpected ${source} beta failure output:\n${output}`);
|
||||
}
|
||||
console.log("ok");
|
||||
NODE
|
||||
}
|
||||
|
||||
start_kitchen_sink_clawhub_fixture_server() {
|
||||
local fixture_dir="$1"
|
||||
local server_log="$fixture_dir/clawhub-fixture.log"
|
||||
local server_port_file="$fixture_dir/clawhub-fixture-port"
|
||||
local server_pid_file="$fixture_dir/clawhub-fixture-pid"
|
||||
|
||||
node - "$server_port_file" <<'NODE' >"$server_log" 2>&1 &
|
||||
const crypto = require("node:crypto");
|
||||
const http = require("node:http");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
const portFile = process.argv[2];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const packageName = "openclaw-kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
const version = "0.1.3";
|
||||
|
||||
async function main() {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"package/package.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
openclaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/index.js",
|
||||
`module.exports = {
|
||||
id: "${pluginId}",
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "kitchen-sink-provider",
|
||||
label: "Kitchen Sink Provider",
|
||||
docsPath: "/providers/kitchen-sink",
|
||||
auth: [],
|
||||
});
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "kitchen-sink-channel",
|
||||
meta: {
|
||||
id: "kitchen-sink-channel",
|
||||
label: "Kitchen Sink Channel",
|
||||
selectionLabel: "Kitchen Sink",
|
||||
docsPath: "/channels/kitchen-sink",
|
||||
blurb: "Kitchen sink ClawHub fixture channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
zip.file(
|
||||
"package/openclaw.plugin.json",
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
name: "OpenClaw Kitchen Sink",
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ date: new Date(0) },
|
||||
);
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
const packageDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
runtimeId: pluginId,
|
||||
channel: "official",
|
||||
isOfficial: true,
|
||||
summary: "Kitchen sink plugin fixture for prerelease CI.",
|
||||
ownerHandle: "openclaw",
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
latestVersion: version,
|
||||
tags: { latest: version },
|
||||
capabilityTags: ["test-fixture"],
|
||||
executesCode: true,
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.4.11",
|
||||
minGatewayVersion: "2026.4.11",
|
||||
},
|
||||
capabilities: {
|
||||
executesCode: true,
|
||||
runtimeId: pluginId,
|
||||
capabilityTags: ["test-fixture"],
|
||||
channels: ["kitchen-sink-channel"],
|
||||
providers: ["kitchen-sink-provider"],
|
||||
},
|
||||
verification: {
|
||||
tier: "source-linked",
|
||||
sourceRepo: "https://github.com/openclaw/kitchen-sink",
|
||||
hasProvenance: false,
|
||||
scanStatus: "passed",
|
||||
},
|
||||
},
|
||||
};
|
||||
const versionDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
displayName: "OpenClaw Kitchen Sink",
|
||||
family: "code-plugin",
|
||||
},
|
||||
version: {
|
||||
version,
|
||||
createdAt: 0,
|
||||
changelog: "Fixture package for kitchen-sink plugin prerelease CI.",
|
||||
distTags: ["latest"],
|
||||
sha256hash,
|
||||
compatibility: packageDetail.package.compatibility,
|
||||
capabilities: packageDetail.package.capabilities,
|
||||
verification: packageDetail.package.verification,
|
||||
},
|
||||
};
|
||||
|
||||
const json = (response, value, status = 200) => {
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405);
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}`) {
|
||||
json(response, packageDetail);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${version}`
|
||||
) {
|
||||
json(response, versionDetail);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/beta`) {
|
||||
json(response, { error: "version not found" }, 404);
|
||||
return;
|
||||
}
|
||||
if (url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/download`) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
});
|
||||
response.end(archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
require("node:fs").writeFileSync(portFile, String(server.address().port));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
local server_pid="$!"
|
||||
echo "$server_pid" >"$server_pid_file"
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
if [[ -s "$server_port_file" ]]; then
|
||||
export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")"
|
||||
trap 'if [[ -f "'"$server_pid_file"'" ]]; then kill "$(cat "'"$server_pid_file"'")" 2>/dev/null || true; fi' EXIT
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$server_pid" 2>/dev/null; then
|
||||
cat "$server_log"
|
||||
return 1
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
cat "$server_log"
|
||||
echo "Timed out waiting for kitchen-sink ClawHub fixture server." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
scan_logs_for_unexpected_errors() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const roots = ["/tmp", path.join(process.env.HOME, ".openclaw")];
|
||||
const files = [];
|
||||
const visit = (entry) => {
|
||||
if (!fs.existsSync(entry)) {
|
||||
return;
|
||||
}
|
||||
const stat = fs.statSync(entry);
|
||||
if (stat.isDirectory()) {
|
||||
for (const child of fs.readdirSync(entry)) {
|
||||
visit(path.join(entry, child));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (/\.(?:log|jsonl)$/u.test(entry) || /openclaw-kitchen-sink-/u.test(path.basename(entry))) {
|
||||
if (entry.includes("/.npm/_logs/")) {
|
||||
return;
|
||||
}
|
||||
files.push(entry);
|
||||
}
|
||||
};
|
||||
for (const root of roots) {
|
||||
visit(root);
|
||||
}
|
||||
|
||||
const deny = [
|
||||
/\buncaught exception\b/iu,
|
||||
/\bunhandled rejection\b/iu,
|
||||
/\bfatal\b/iu,
|
||||
/\bpanic\b/iu,
|
||||
/\blevel["']?\s*:\s*["']error["']/iu,
|
||||
/\[(?:error|ERROR)\]/u,
|
||||
];
|
||||
const allow = [
|
||||
/0 errors?/iu,
|
||||
/expected no diagnostics errors?/iu,
|
||||
/diagnostics errors?:\s*$/iu,
|
||||
];
|
||||
const findings = [];
|
||||
for (const file of files) {
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
const lines = text.split(/\r?\n/u);
|
||||
lines.forEach((line, index) => {
|
||||
if (allow.some((pattern) => pattern.test(line))) {
|
||||
return;
|
||||
}
|
||||
if (deny.some((pattern) => pattern.test(line))) {
|
||||
findings.push(`${file}:${index + 1}: ${line}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (findings.length > 0) {
|
||||
throw new Error(`unexpected error-like log lines:\n${findings.join("\n")}`);
|
||||
}
|
||||
console.log(`log scan passed (${files.length} file(s))`);
|
||||
NODE
|
||||
}
|
||||
|
||||
configure_kitchen_sink_runtime() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const pluginId = process.env.KITCHEN_SINK_ID;
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
config.plugins = config.plugins || {};
|
||||
config.plugins.entries = config.plugins.entries || {};
|
||||
config.plugins.entries[pluginId] = {
|
||||
...(config.plugins.entries[pluginId] || {}),
|
||||
hooks: {
|
||||
...(config.plugins.entries[pluginId]?.hooks || {}),
|
||||
allowConversationAccess: true,
|
||||
},
|
||||
};
|
||||
config.channels = {
|
||||
...(config.channels || {}),
|
||||
"kitchen-sink-channel": { enabled: true, token: "kitchen-sink-ci" },
|
||||
};
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
||||
NODE
|
||||
}
|
||||
|
||||
remove_kitchen_sink_channel_config() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
if (fs.existsSync(configPath)) {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
delete config.channels?.["kitchen-sink-channel"];
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_kitchen_sink_installed() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const pluginId = process.env.KITCHEN_SINK_ID;
|
||||
const spec = process.env.KITCHEN_SINK_SPEC;
|
||||
const source = process.env.KITCHEN_SINK_SOURCE;
|
||||
const surfaceMode = process.env.KITCHEN_SINK_SURFACE_MODE;
|
||||
const label = process.env.KITCHEN_SINK_LABEL;
|
||||
const list = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-plugins.json`, "utf8"));
|
||||
const inspect = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-inspect.json`, "utf8"));
|
||||
const allInspect = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-inspect-all.json`, "utf8"));
|
||||
const plugin = (list.plugins || []).find((entry) => entry.id === pluginId);
|
||||
if (!plugin) throw new Error(`kitchen-sink plugin not found after install: ${pluginId}`);
|
||||
if (plugin.status !== "loaded") {
|
||||
throw new Error(`unexpected kitchen-sink status after enable: ${plugin.status}`);
|
||||
}
|
||||
if (inspect.plugin?.id !== pluginId) {
|
||||
throw new Error(`unexpected inspected kitchen-sink plugin id: ${inspect.plugin?.id}`);
|
||||
}
|
||||
if (inspect.plugin?.enabled !== true || inspect.plugin?.status !== "loaded") {
|
||||
throw new Error(
|
||||
`expected enabled loaded kitchen-sink plugin, got enabled=${inspect.plugin?.enabled} status=${inspect.plugin?.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectIncludes = (listValue, expected, field) => {
|
||||
if (!Array.isArray(listValue) || !listValue.includes(expected)) {
|
||||
throw new Error(`${field} missing ${expected}: ${JSON.stringify(listValue)}`);
|
||||
}
|
||||
};
|
||||
expectIncludes(inspect.plugin?.channelIds, "kitchen-sink-channel", "channels");
|
||||
expectIncludes(inspect.plugin?.providerIds, "kitchen-sink-provider", "providers");
|
||||
|
||||
const diagnostics = [
|
||||
...(list.diagnostics || []),
|
||||
...(inspect.diagnostics || []),
|
||||
...(allInspect.diagnostics || []),
|
||||
];
|
||||
const errorMessages = new Set(
|
||||
diagnostics
|
||||
.filter((diag) => diag?.level === "error")
|
||||
.map((diag) => String(diag.message || "")),
|
||||
);
|
||||
|
||||
if (surfaceMode === "full") {
|
||||
const toolNames = Array.isArray(inspect.tools)
|
||||
? inspect.tools.flatMap((entry) => (Array.isArray(entry?.names) ? entry.names : []))
|
||||
: [];
|
||||
expectIncludes(inspect.plugin?.speechProviderIds, "kitchen-sink-speech-provider", "speech providers");
|
||||
expectIncludes(
|
||||
inspect.plugin?.realtimeTranscriptionProviderIds,
|
||||
"kitchen-sink-realtime-transcription-provider",
|
||||
"realtime transcription providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.realtimeVoiceProviderIds,
|
||||
"kitchen-sink-realtime-voice-provider",
|
||||
"realtime voice providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.mediaUnderstandingProviderIds,
|
||||
"kitchen-sink-media-understanding-provider",
|
||||
"media understanding providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.imageGenerationProviderIds,
|
||||
"kitchen-sink-image-generation-provider",
|
||||
"image generation providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.videoGenerationProviderIds,
|
||||
"kitchen-sink-video-generation-provider",
|
||||
"video generation providers",
|
||||
);
|
||||
expectIncludes(
|
||||
inspect.plugin?.musicGenerationProviderIds,
|
||||
"kitchen-sink-music-generation-provider",
|
||||
"music generation providers",
|
||||
);
|
||||
expectIncludes(inspect.plugin?.webFetchProviderIds, "kitchen-sink-web-fetch-provider", "web fetch providers");
|
||||
expectIncludes(inspect.plugin?.webSearchProviderIds, "kitchen-sink-web-search-provider", "web search providers");
|
||||
expectIncludes(inspect.plugin?.migrationProviderIds, "kitchen-sink-migration-provider", "migration providers");
|
||||
expectIncludes(inspect.plugin?.agentHarnessIds, "kitchen-sink-agent-harness", "agent harnesses");
|
||||
expectIncludes(inspect.services, "kitchen-sink-service", "services");
|
||||
expectIncludes(inspect.commands, "kitchen-sink-command", "commands");
|
||||
expectIncludes(toolNames, "kitchen-sink-tool", "tools");
|
||||
if ((inspect.plugin?.hookCount || 0) < 30 || !Array.isArray(inspect.typedHooks) || inspect.typedHooks.length < 30) {
|
||||
throw new Error(
|
||||
`expected kitchen-sink typed hooks to load, got hookCount=${inspect.plugin?.hookCount} typedHooks=${inspect.typedHooks?.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectedErrorMessages = new Set([
|
||||
"only bundled plugins can register agent tool result middleware",
|
||||
"cli registration missing explicit commands metadata",
|
||||
"only bundled plugins can register Codex app-server extension factories",
|
||||
"http route registration missing or invalid auth: /kitchen-sink/http-route",
|
||||
"plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider",
|
||||
]);
|
||||
for (const message of errorMessages) {
|
||||
if (!expectedErrorMessages.has(message)) {
|
||||
throw new Error(`unexpected kitchen-sink diagnostic error: ${message}`);
|
||||
}
|
||||
}
|
||||
for (const message of expectedErrorMessages) {
|
||||
if (!errorMessages.has(message)) {
|
||||
throw new Error(`missing expected kitchen-sink diagnostic error: ${message}`);
|
||||
}
|
||||
}
|
||||
} else if (errorMessages.size > 0) {
|
||||
throw new Error(`unexpected kitchen-sink diagnostic errors: ${[...errorMessages].join(", ")}`);
|
||||
}
|
||||
|
||||
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
||||
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
||||
const record = (index.installRecords ?? index.records ?? {})[pluginId];
|
||||
if (!record) throw new Error(`missing kitchen-sink install record for ${pluginId}`);
|
||||
if (record.source !== source) {
|
||||
throw new Error(`expected kitchen-sink install source=${source}, got ${record.source}`);
|
||||
}
|
||||
if (source === "npm") {
|
||||
const expectedSpec = spec.replace(/^npm:/u, "");
|
||||
if (record.spec !== expectedSpec) {
|
||||
throw new Error(`expected kitchen-sink npm spec ${expectedSpec}, got ${record.spec}`);
|
||||
}
|
||||
if (!record.resolvedVersion || !record.resolvedSpec) {
|
||||
throw new Error(`missing npm resolution metadata: ${JSON.stringify(record)}`);
|
||||
}
|
||||
} else if (source === "clawhub") {
|
||||
const value = spec.slice("clawhub:".length).trim();
|
||||
const slashIndex = value.lastIndexOf("/");
|
||||
const atIndex = value.lastIndexOf("@");
|
||||
const packageName = atIndex > 0 && atIndex > slashIndex ? value.slice(0, atIndex) : value;
|
||||
if (record.spec !== spec) {
|
||||
throw new Error(`expected kitchen-sink ClawHub spec ${spec}, got ${record.spec}`);
|
||||
}
|
||||
if (record.clawhubPackage !== packageName) {
|
||||
throw new Error(`expected ClawHub package ${packageName}, got ${record.clawhubPackage}`);
|
||||
}
|
||||
if (record.clawhubFamily !== "code-plugin" && record.clawhubFamily !== "bundle-plugin") {
|
||||
throw new Error(`unexpected ClawHub family: ${record.clawhubFamily}`);
|
||||
}
|
||||
if (!record.version || !record.integrity || !record.resolvedAt) {
|
||||
throw new Error(`missing ClawHub resolution metadata: ${JSON.stringify(record)}`);
|
||||
}
|
||||
}
|
||||
if (typeof record.installPath !== "string" || record.installPath.length === 0) {
|
||||
throw new Error("missing kitchen-sink install path");
|
||||
}
|
||||
const installPath = record.installPath.replace(/^~(?=$|\/)/u, process.env.HOME);
|
||||
if (!fs.existsSync(installPath)) {
|
||||
throw new Error(`kitchen-sink install path missing: ${record.installPath}`);
|
||||
}
|
||||
fs.writeFileSync(`/tmp/kitchen-sink-${label}-install-path.txt`, installPath, "utf8");
|
||||
console.log("ok");
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_kitchen_sink_removed() {
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const pluginId = process.env.KITCHEN_SINK_ID;
|
||||
const label = process.env.KITCHEN_SINK_LABEL;
|
||||
const list = JSON.parse(fs.readFileSync(`/tmp/kitchen-sink-${label}-uninstalled.json`, "utf8"));
|
||||
if ((list.plugins || []).some((entry) => entry.id === pluginId)) {
|
||||
throw new Error(`kitchen-sink plugin still listed after uninstall: ${pluginId}`);
|
||||
}
|
||||
|
||||
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
|
||||
const index = fs.existsSync(indexPath) ? JSON.parse(fs.readFileSync(indexPath, "utf8")) : {};
|
||||
const records = index.installRecords ?? index.records ?? {};
|
||||
if (records[pluginId]) {
|
||||
throw new Error(`kitchen-sink install record still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
|
||||
if (config.plugins?.entries?.[pluginId]) {
|
||||
throw new Error(`kitchen-sink config entry still present after uninstall: ${pluginId}`);
|
||||
}
|
||||
if ((config.plugins?.allow || []).includes(pluginId)) {
|
||||
throw new Error(`kitchen-sink allowlist still contains ${pluginId}`);
|
||||
}
|
||||
if ((config.plugins?.deny || []).includes(pluginId)) {
|
||||
throw new Error(`kitchen-sink denylist still contains ${pluginId}`);
|
||||
}
|
||||
if (config.channels?.["kitchen-sink-channel"]) {
|
||||
throw new Error("kitchen-sink channel config still present after uninstall");
|
||||
}
|
||||
const installPathFile = `/tmp/kitchen-sink-${label}-install-path.txt`;
|
||||
if (fs.existsSync(installPathFile)) {
|
||||
const installPath = fs.readFileSync(installPathFile, "utf8").trim();
|
||||
if (installPath && fs.existsSync(installPath)) {
|
||||
throw new Error(`kitchen-sink managed install directory still exists: ${installPath}`);
|
||||
}
|
||||
}
|
||||
console.log("ok");
|
||||
NODE
|
||||
}
|
||||
|
||||
run_success_scenario() {
|
||||
echo "Testing ${KITCHEN_SINK_LABEL} install from ${KITCHEN_SINK_SPEC}..."
|
||||
run_logged_print "kitchen-sink-install-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins install "$KITCHEN_SINK_SPEC"
|
||||
run_logged_print "kitchen-sink-enable-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins enable "$KITCHEN_SINK_ID"
|
||||
configure_kitchen_sink_runtime
|
||||
node "$OPENCLAW_ENTRY" plugins list --json >"/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-plugins.json"
|
||||
node "$OPENCLAW_ENTRY" plugins inspect "$KITCHEN_SINK_ID" --json >"/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-inspect.json"
|
||||
node "$OPENCLAW_ENTRY" plugins inspect --all --json >"/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-inspect-all.json"
|
||||
assert_kitchen_sink_installed
|
||||
if [ "$KITCHEN_SINK_SOURCE" = "clawhub" ]; then
|
||||
run_logged_print "kitchen-sink-uninstall-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins uninstall "$KITCHEN_SINK_SPEC" --force
|
||||
else
|
||||
run_logged_print "kitchen-sink-uninstall-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins uninstall "$KITCHEN_SINK_ID" --force
|
||||
fi
|
||||
remove_kitchen_sink_channel_config
|
||||
node "$OPENCLAW_ENTRY" plugins list --json >"/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-uninstalled.json"
|
||||
assert_kitchen_sink_removed
|
||||
}
|
||||
|
||||
run_failure_scenario() {
|
||||
echo "Testing expected ${KITCHEN_SINK_LABEL} install failure from ${KITCHEN_SINK_SPEC}..."
|
||||
run_expect_failure "install-${KITCHEN_SINK_LABEL}" node "$OPENCLAW_ENTRY" plugins install "$KITCHEN_SINK_SPEC"
|
||||
remove_kitchen_sink_channel_config
|
||||
node "$OPENCLAW_ENTRY" plugins list --json >"/tmp/kitchen-sink-${KITCHEN_SINK_LABEL}-uninstalled.json"
|
||||
assert_kitchen_sink_removed
|
||||
}
|
||||
|
||||
if [[ "$KITCHEN_SINK_SCENARIOS" == *"clawhub:"* ]] &&
|
||||
[[ "${OPENCLAW_KITCHEN_SINK_LIVE_CLAWHUB:-0}" != "1" ]] &&
|
||||
[[ -z "${OPENCLAW_CLAWHUB_URL:-}" && -z "${CLAWHUB_URL:-}" ]]; then
|
||||
clawhub_fixture_dir="$(mktemp -d "/tmp/openclaw-kitchen-sink-clawhub.XXXXXX")"
|
||||
start_kitchen_sink_clawhub_fixture_server "$clawhub_fixture_dir"
|
||||
fi
|
||||
|
||||
scenario_count=0
|
||||
while IFS='|' read -r label spec plugin_id source expectation surface_mode; do
|
||||
if [ -z "${label:-}" ] || [[ "$label" == \#* ]]; then
|
||||
continue
|
||||
fi
|
||||
scenario_count=$((scenario_count + 1))
|
||||
export KITCHEN_SINK_LABEL="$label"
|
||||
export KITCHEN_SINK_SPEC="$spec"
|
||||
export KITCHEN_SINK_ID="$plugin_id"
|
||||
export KITCHEN_SINK_SOURCE="$source"
|
||||
export KITCHEN_SINK_SURFACE_MODE="$surface_mode"
|
||||
case "$expectation" in
|
||||
success)
|
||||
run_success_scenario
|
||||
;;
|
||||
failure)
|
||||
run_failure_scenario
|
||||
;;
|
||||
*)
|
||||
echo "Unknown kitchen-sink expectation for ${label}: ${expectation}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done <<<"$KITCHEN_SINK_SCENARIOS"
|
||||
|
||||
if [ "$scenario_count" -eq 0 ]; then
|
||||
echo "No kitchen-sink plugin scenarios configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
scan_logs_for_unexpected_errors
|
||||
echo "kitchen-sink plugin Docker E2E passed (${scenario_count} scenario(s))"
|
||||
78
scripts/e2e/lib/onboard/assert-config.mjs
Normal file
78
scripts/e2e/lib/onboard/assert-config.mjs
Normal file
@@ -0,0 +1,78 @@
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
|
||||
const [scenario, configPath, expectedWorkspace] = process.argv.slice(2);
|
||||
if (!scenario || !configPath) {
|
||||
throw new Error("usage: assert-config.mjs <scenario> <config-path> [expected-workspace]");
|
||||
}
|
||||
|
||||
const cfg = JSON5.parse(fs.readFileSync(configPath, "utf8"));
|
||||
const errors = [];
|
||||
const got = (value) => value ?? "unset";
|
||||
const expectEqual = (label, actual, expected) => {
|
||||
if (actual !== expected) {
|
||||
errors.push(`${label} mismatch (got ${got(actual)})`);
|
||||
}
|
||||
};
|
||||
|
||||
const assertLocalWizard = () => {
|
||||
expectEqual("gateway.mode", cfg?.gateway?.mode, "local");
|
||||
expectEqual("wizard.lastRunMode", cfg?.wizard?.lastRunMode, "local");
|
||||
};
|
||||
|
||||
switch (scenario) {
|
||||
case "local-basic": {
|
||||
expectEqual("agents.defaults.workspace", cfg?.agents?.defaults?.workspace, expectedWorkspace);
|
||||
assertLocalWizard();
|
||||
expectEqual("gateway.bind", cfg?.gateway?.bind, "loopback");
|
||||
expectEqual("gateway.tailscale.mode", cfg?.gateway?.tailscale?.mode ?? "off", "off");
|
||||
if (!cfg?.wizard?.lastRunAt) {
|
||||
errors.push("wizard.lastRunAt missing");
|
||||
}
|
||||
if (!cfg?.wizard?.lastRunVersion) {
|
||||
errors.push("wizard.lastRunVersion missing");
|
||||
}
|
||||
expectEqual("wizard.lastRunCommand", cfg?.wizard?.lastRunCommand, "onboard");
|
||||
break;
|
||||
}
|
||||
case "remote-non-interactive":
|
||||
expectEqual("gateway.mode", cfg?.gateway?.mode, "remote");
|
||||
expectEqual("gateway.remote.url", cfg?.gateway?.remote?.url, "ws://gateway.local:18789");
|
||||
expectEqual("gateway.remote.token", cfg?.gateway?.remote?.token, "remote-token");
|
||||
expectEqual("wizard.lastRunMode", cfg?.wizard?.lastRunMode, "remote");
|
||||
break;
|
||||
case "reset":
|
||||
assertLocalWizard();
|
||||
if (cfg?.gateway?.remote?.url) {
|
||||
errors.push(`gateway.remote.url should be cleared (got ${cfg.gateway.remote.url})`);
|
||||
}
|
||||
break;
|
||||
case "channels":
|
||||
if (cfg?.telegram?.botToken) {
|
||||
errors.push(`telegram.botToken should be unset (got ${cfg.telegram.botToken})`);
|
||||
}
|
||||
if (cfg?.discord?.token) {
|
||||
errors.push(`discord.token should be unset (got ${cfg.discord.token})`);
|
||||
}
|
||||
if (cfg?.slack?.botToken || cfg?.slack?.appToken) {
|
||||
errors.push(
|
||||
`slack tokens should be unset (got bot=${got(cfg?.slack?.botToken)}, app=${got(cfg?.slack?.appToken)})`,
|
||||
);
|
||||
}
|
||||
expectEqual("wizard.lastRunCommand", cfg?.wizard?.lastRunCommand, "configure");
|
||||
break;
|
||||
case "skills":
|
||||
expectEqual("skills.install.nodeManager", cfg?.skills?.install?.nodeManager, "bun");
|
||||
if (!Array.isArray(cfg?.skills?.allowBundled) || cfg.skills.allowBundled[0] !== "__none__") {
|
||||
errors.push("skills.allowBundled missing");
|
||||
}
|
||||
assertLocalWizard();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown onboard assertion scenario: ${scenario}`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
336
scripts/e2e/lib/onboard/scenario.sh
Normal file
336
scripts/e2e/lib/onboard/scenario.sh
Normal file
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
trap "" PIPE
|
||||
export TERM=xterm-256color
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_FUNCTION_B64:?missing OPENCLAW_TEST_STATE_FUNCTION_B64}"
|
||||
ONBOARD_FLAGS="--flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-daemon --skip-ui"
|
||||
OPENCLAW_ENTRY="$(openclaw_e2e_resolve_entrypoint)"
|
||||
export OPENCLAW_ENTRY
|
||||
|
||||
# Provide a minimal trash shim to avoid noisy "missing trash" logs in containers.
|
||||
openclaw_e2e_install_trash_shim
|
||||
|
||||
send() {
|
||||
local payload="$1"
|
||||
local delay="${2:-0.4}"
|
||||
# Let prompts render before sending keystrokes.
|
||||
sleep "$delay"
|
||||
printf "%b" "$payload" >&3 2>/dev/null || true
|
||||
}
|
||||
|
||||
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
|
||||
if [ -n "${WIZARD_LOG_PATH:-}" ] && [ -f "$WIZARD_LOG_PATH" ]; then
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [ $(($(date +%s) - start_s)) -ge "$timeout_s" ]; then
|
||||
if [ "$quiet_on_timeout" = "true" ]; then
|
||||
return 1
|
||||
fi
|
||||
echo "Timeout waiting for log: $needle"
|
||||
if [ -n "${WIZARD_LOG_PATH:-}" ] && [ -f "$WIZARD_LOG_PATH" ]; then
|
||||
tail -n 140 "$WIZARD_LOG_PATH" || true
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
}
|
||||
|
||||
start_gateway() {
|
||||
GATEWAY_PID="$(openclaw_e2e_start_gateway "$OPENCLAW_ENTRY" 18789 /tmp/gateway-e2e.log)"
|
||||
}
|
||||
|
||||
wait_for_gateway() {
|
||||
for _ in $(seq 1 20); do
|
||||
if openclaw_e2e_probe_tcp 127.0.0.1 18789 500 >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [ -f /tmp/gateway-e2e.log ] && grep -E -q "listening on ws://[^ ]+:18789" /tmp/gateway-e2e.log; then
|
||||
if [ -n "${GATEWAY_PID:-}" ] && kill -0 "$GATEWAY_PID" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "Gateway failed to start"
|
||||
cat /tmp/gateway-e2e.log || true
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_gateway() {
|
||||
openclaw_e2e_stop_process "$1"
|
||||
}
|
||||
|
||||
run_wizard_cmd() {
|
||||
local case_name="$1"
|
||||
local state_ref="$2"
|
||||
local command="$3"
|
||||
local send_fn="$4"
|
||||
local with_gateway="${5:-false}"
|
||||
local validate_fn="${6:-}"
|
||||
|
||||
echo "== Wizard case: $case_name =="
|
||||
set_isolated_openclaw_env "$state_ref"
|
||||
|
||||
input_fifo="$(mktemp -u "/tmp/openclaw-onboard-${case_name}.XXXXXX")"
|
||||
mkfifo "$input_fifo"
|
||||
local log_path="/tmp/openclaw-onboard-${case_name}.log"
|
||||
WIZARD_LOG_PATH="$log_path"
|
||||
export WIZARD_LOG_PATH
|
||||
# Run under script to keep an interactive TTY for clack prompts.
|
||||
script -q -f -c "$command" "$log_path" <"$input_fifo" >/dev/null 2>&1 &
|
||||
wizard_pid=$!
|
||||
exec 3>"$input_fifo"
|
||||
|
||||
local gw_pid=""
|
||||
if [ "$with_gateway" = "true" ]; then
|
||||
start_gateway
|
||||
gw_pid="$GATEWAY_PID"
|
||||
wait_for_gateway
|
||||
fi
|
||||
|
||||
"$send_fn"
|
||||
|
||||
if ! wait "$wizard_pid"; then
|
||||
wizard_status=$?
|
||||
exec 3>&-
|
||||
rm -f "$input_fifo"
|
||||
stop_gateway "$gw_pid"
|
||||
echo "Wizard exited with status $wizard_status"
|
||||
if [ -f "$log_path" ]; then
|
||||
tail -n 160 "$log_path" || true
|
||||
fi
|
||||
exit "$wizard_status"
|
||||
fi
|
||||
exec 3>&-
|
||||
rm -f "$input_fifo"
|
||||
stop_gateway "$gw_pid"
|
||||
if [ -n "$validate_fn" ]; then
|
||||
"$validate_fn" "$log_path"
|
||||
fi
|
||||
}
|
||||
|
||||
run_wizard() {
|
||||
local case_name="$1"
|
||||
local state_ref="$2"
|
||||
local send_fn="$3"
|
||||
local validate_fn="${4:-}"
|
||||
|
||||
# Default onboarding command wrapper.
|
||||
run_wizard_cmd "$case_name" "$state_ref" "node \"$OPENCLAW_ENTRY\" onboard $ONBOARD_FLAGS" "$send_fn" true "$validate_fn"
|
||||
}
|
||||
|
||||
set_isolated_openclaw_env() {
|
||||
local state_ref="$1"
|
||||
openclaw_test_state_create "$state_ref" empty
|
||||
}
|
||||
|
||||
select_skip_hooks() {
|
||||
# Hooks multiselect: pick "Skip for now".
|
||||
wait_for_log "Enable hooks?" 60
|
||||
send $' \r' 0.6
|
||||
}
|
||||
|
||||
send_local_basic() {
|
||||
# Risk acknowledgement (default is "No").
|
||||
wait_for_log "Continue?" 60
|
||||
send $'y\r' 0.6
|
||||
# Non-interactive flow; no gateway-location prompt.
|
||||
select_skip_hooks
|
||||
}
|
||||
|
||||
send_reset_config_only() {
|
||||
# Risk acknowledgement (default is "No").
|
||||
wait_for_log "Continue?" 40
|
||||
send $'y\r' 0.8
|
||||
# Select reset flow for existing config.
|
||||
wait_for_log "Config handling" 40
|
||||
send $'\e[B' 0.3
|
||||
send $'\e[B' 0.3
|
||||
send $'\r' 0.4
|
||||
# Reset scope -> Config only (default).
|
||||
wait_for_log "Reset scope" 40
|
||||
send $'\r' 0.4
|
||||
select_skip_hooks
|
||||
}
|
||||
|
||||
send_channels_flow() {
|
||||
# Configure channels via configure wizard. Use the remove-config branch for
|
||||
# a stable no-op smoke path when the config starts empty.
|
||||
wait_for_log "Where will the Gateway run?" 120
|
||||
send $'\r' 0.6
|
||||
wait_for_log "Configure/link" 120
|
||||
send $'\e[B\r' 0.8
|
||||
# Keep stdin open until wizard exits.
|
||||
send "" 2.0
|
||||
}
|
||||
|
||||
send_skills_flow() {
|
||||
# configure --section skills still runs the configure wizard.
|
||||
wait_for_log "Where will the Gateway run?" 120
|
||||
send $'\r' 0.6
|
||||
wait_for_log "Configure skills now?" 120
|
||||
send $'n\r' 0.8
|
||||
send "" 2.0
|
||||
}
|
||||
|
||||
run_case_local_basic() {
|
||||
set_isolated_openclaw_env local-basic
|
||||
openclaw_e2e_run_logged local-basic node "$OPENCLAW_ENTRY" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-health
|
||||
|
||||
# Assert config + workspace scaffolding.
|
||||
workspace_dir="$OPENCLAW_STATE_DIR/workspace"
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
sessions_dir="$OPENCLAW_STATE_DIR/agents/main/sessions"
|
||||
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
openclaw_e2e_assert_dir "$sessions_dir"
|
||||
for file in AGENTS.md BOOTSTRAP.md IDENTITY.md SOUL.md TOOLS.md USER.md; do
|
||||
openclaw_e2e_assert_file "$workspace_dir/$file"
|
||||
done
|
||||
|
||||
node scripts/e2e/lib/onboard/assert-config.mjs local-basic "$config_path" "$workspace_dir"
|
||||
|
||||
}
|
||||
|
||||
run_case_remote_non_interactive() {
|
||||
set_isolated_openclaw_env remote-non-interactive
|
||||
# Smoke test non-interactive remote config write.
|
||||
openclaw_e2e_run_logged remote-non-interactive node "$OPENCLAW_ENTRY" onboard --non-interactive --accept-risk \
|
||||
--mode remote \
|
||||
--remote-url ws://gateway.local:18789 \
|
||||
--remote-token remote-token \
|
||||
--skip-skills \
|
||||
--skip-health
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
node scripts/e2e/lib/onboard/assert-config.mjs remote-non-interactive "$config_path"
|
||||
}
|
||||
|
||||
run_case_reset() {
|
||||
set_isolated_openclaw_env reset-config
|
||||
# Seed a remote config to exercise reset path.
|
||||
cat >"$OPENCLAW_CONFIG_PATH" <<'JSON'
|
||||
{
|
||||
"meta": {},
|
||||
"agents": { "defaults": { "workspace": "/root/old" } },
|
||||
"gateway": {
|
||||
"mode": "remote",
|
||||
"remote": { "url": "ws://old.example:18789", "token": "old-token" }
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
openclaw_e2e_run_logged reset-config node "$OPENCLAW_ENTRY" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--reset \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-health
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
node scripts/e2e/lib/onboard/assert-config.mjs reset "$config_path"
|
||||
}
|
||||
|
||||
run_case_channels() {
|
||||
# Channels-only configure flow.
|
||||
run_wizard_cmd channels channels "node \"$OPENCLAW_ENTRY\" configure --section channels" send_channels_flow
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
node scripts/e2e/lib/onboard/assert-config.mjs channels "$config_path"
|
||||
}
|
||||
|
||||
run_case_skills() {
|
||||
local home_dir
|
||||
set_isolated_openclaw_env skills
|
||||
home_dir="$HOME"
|
||||
# Seed skills config to ensure it survives the wizard.
|
||||
cat >"$OPENCLAW_CONFIG_PATH" <<'JSON'
|
||||
{
|
||||
"meta": {},
|
||||
"skills": {
|
||||
"allowBundled": ["__none__"],
|
||||
"install": { "nodeManager": "bun" }
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
run_wizard_cmd skills "$home_dir" "node \"$OPENCLAW_ENTRY\" configure --section skills" send_skills_flow
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
node scripts/e2e/lib/onboard/assert-config.mjs skills "$config_path"
|
||||
}
|
||||
|
||||
validate_local_basic_log() {
|
||||
local log_path="$1"
|
||||
openclaw_e2e_assert_log_not_contains "$log_path" "systemctl --user unavailable"
|
||||
}
|
||||
|
||||
run_case_local_basic
|
||||
run_case_remote_non_interactive
|
||||
run_case_reset
|
||||
run_case_channels
|
||||
run_case_skills
|
||||
@@ -11,481 +11,6 @@ docker_e2e_build_or_reuse "$IMAGE_NAME" onboard
|
||||
echo "Running onboarding E2E..."
|
||||
docker_e2e_run_with_harness -t \
|
||||
-e "OPENCLAW_TEST_STATE_FUNCTION_B64=$OPENCLAW_TEST_STATE_FUNCTION_B64" \
|
||||
"$IMAGE_NAME" bash -lc '
|
||||
set -euo pipefail
|
||||
trap "" PIPE
|
||||
export TERM=xterm-256color
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
eval "$(printf "%s" "${OPENCLAW_TEST_STATE_FUNCTION_B64:?missing OPENCLAW_TEST_STATE_FUNCTION_B64}" | base64 -d)"
|
||||
ONBOARD_FLAGS="--flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-daemon --skip-ui"
|
||||
OPENCLAW_ENTRY="$(openclaw_e2e_resolve_entrypoint)"
|
||||
export OPENCLAW_ENTRY
|
||||
|
||||
# Provide a minimal trash shim to avoid noisy "missing trash" logs in containers.
|
||||
openclaw_e2e_install_trash_shim
|
||||
|
||||
send() {
|
||||
local payload="$1"
|
||||
local delay="${2:-0.4}"
|
||||
# Let prompts render before sending keystrokes.
|
||||
sleep "$delay"
|
||||
printf "%b" "$payload" >&3 2>/dev/null || true
|
||||
}
|
||||
|
||||
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
|
||||
if [ -n "${WIZARD_LOG_PATH:-}" ] && [ -f "$WIZARD_LOG_PATH" ]; then
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [ $(( $(date +%s) - start_s )) -ge "$timeout_s" ]; then
|
||||
if [ "$quiet_on_timeout" = "true" ]; then
|
||||
return 1
|
||||
fi
|
||||
echo "Timeout waiting for log: $needle"
|
||||
if [ -n "${WIZARD_LOG_PATH:-}" ] && [ -f "$WIZARD_LOG_PATH" ]; then
|
||||
tail -n 140 "$WIZARD_LOG_PATH" || true
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
}
|
||||
|
||||
start_gateway() {
|
||||
GATEWAY_PID="$(openclaw_e2e_start_gateway "$OPENCLAW_ENTRY" 18789 /tmp/gateway-e2e.log)"
|
||||
}
|
||||
|
||||
wait_for_gateway() {
|
||||
for _ in $(seq 1 20); do
|
||||
if openclaw_e2e_probe_tcp 127.0.0.1 18789 500 >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [ -f /tmp/gateway-e2e.log ] && grep -E -q "listening on ws://[^ ]+:18789" /tmp/gateway-e2e.log; then
|
||||
if [ -n "${GATEWAY_PID:-}" ] && kill -0 "$GATEWAY_PID" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "Gateway failed to start"
|
||||
cat /tmp/gateway-e2e.log || true
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_gateway() {
|
||||
openclaw_e2e_stop_process "$1"
|
||||
}
|
||||
|
||||
run_wizard_cmd() {
|
||||
local case_name="$1"
|
||||
local state_ref="$2"
|
||||
local command="$3"
|
||||
local send_fn="$4"
|
||||
local with_gateway="${5:-false}"
|
||||
local validate_fn="${6:-}"
|
||||
|
||||
echo "== Wizard case: $case_name =="
|
||||
set_isolated_openclaw_env "$state_ref"
|
||||
|
||||
input_fifo="$(mktemp -u "/tmp/openclaw-onboard-${case_name}.XXXXXX")"
|
||||
mkfifo "$input_fifo"
|
||||
local log_path="/tmp/openclaw-onboard-${case_name}.log"
|
||||
WIZARD_LOG_PATH="$log_path"
|
||||
export WIZARD_LOG_PATH
|
||||
# Run under script to keep an interactive TTY for clack prompts.
|
||||
script -q -f -c "$command" "$log_path" < "$input_fifo" >/dev/null 2>&1 &
|
||||
wizard_pid=$!
|
||||
exec 3> "$input_fifo"
|
||||
|
||||
local gw_pid=""
|
||||
if [ "$with_gateway" = "true" ]; then
|
||||
start_gateway
|
||||
gw_pid="$GATEWAY_PID"
|
||||
wait_for_gateway
|
||||
fi
|
||||
|
||||
"$send_fn"
|
||||
|
||||
if ! wait "$wizard_pid"; then
|
||||
wizard_status=$?
|
||||
exec 3>&-
|
||||
rm -f "$input_fifo"
|
||||
stop_gateway "$gw_pid"
|
||||
echo "Wizard exited with status $wizard_status"
|
||||
if [ -f "$log_path" ]; then
|
||||
tail -n 160 "$log_path" || true
|
||||
fi
|
||||
exit "$wizard_status"
|
||||
fi
|
||||
exec 3>&-
|
||||
rm -f "$input_fifo"
|
||||
stop_gateway "$gw_pid"
|
||||
if [ -n "$validate_fn" ]; then
|
||||
"$validate_fn" "$log_path"
|
||||
fi
|
||||
}
|
||||
|
||||
run_wizard() {
|
||||
local case_name="$1"
|
||||
local state_ref="$2"
|
||||
local send_fn="$3"
|
||||
local validate_fn="${4:-}"
|
||||
|
||||
# Default onboarding command wrapper.
|
||||
run_wizard_cmd "$case_name" "$state_ref" "node \"$OPENCLAW_ENTRY\" onboard $ONBOARD_FLAGS" "$send_fn" true "$validate_fn"
|
||||
}
|
||||
|
||||
set_isolated_openclaw_env() {
|
||||
local state_ref="$1"
|
||||
openclaw_test_state_create "$state_ref" empty
|
||||
}
|
||||
|
||||
select_skip_hooks() {
|
||||
# Hooks multiselect: pick "Skip for now".
|
||||
wait_for_log "Enable hooks?" 60
|
||||
send $'"'"' \r'"'"' 0.6
|
||||
}
|
||||
|
||||
send_local_basic() {
|
||||
# Risk acknowledgement (default is "No").
|
||||
wait_for_log "Continue?" 60
|
||||
send $'"'"'y\r'"'"' 0.6
|
||||
# Non-interactive flow; no gateway-location prompt.
|
||||
select_skip_hooks
|
||||
}
|
||||
|
||||
send_reset_config_only() {
|
||||
# Risk acknowledgement (default is "No").
|
||||
wait_for_log "Continue?" 40
|
||||
send $'"'"'y\r'"'"' 0.8
|
||||
# Select reset flow for existing config.
|
||||
wait_for_log "Config handling" 40
|
||||
send $'"'"'\e[B'"'"' 0.3
|
||||
send $'"'"'\e[B'"'"' 0.3
|
||||
send $'"'"'\r'"'"' 0.4
|
||||
# Reset scope -> Config only (default).
|
||||
wait_for_log "Reset scope" 40
|
||||
send $'"'"'\r'"'"' 0.4
|
||||
select_skip_hooks
|
||||
}
|
||||
|
||||
send_channels_flow() {
|
||||
# Configure channels via configure wizard. Use the remove-config branch for
|
||||
# a stable no-op smoke path when the config starts empty.
|
||||
wait_for_log "Where will the Gateway run?" 120
|
||||
send $'"'"'\r'"'"' 0.6
|
||||
wait_for_log "Configure/link" 120
|
||||
send $'"'"'\e[B\r'"'"' 0.8
|
||||
# Keep stdin open until wizard exits.
|
||||
send "" 2.0
|
||||
}
|
||||
|
||||
send_skills_flow() {
|
||||
# configure --section skills still runs the configure wizard.
|
||||
wait_for_log "Where will the Gateway run?" 120
|
||||
send $'"'"'\r'"'"' 0.6
|
||||
wait_for_log "Configure skills now?" 120
|
||||
send $'"'"'n\r'"'"' 0.8
|
||||
send "" 2.0
|
||||
}
|
||||
|
||||
run_case_local_basic() {
|
||||
set_isolated_openclaw_env local-basic
|
||||
openclaw_e2e_run_logged local-basic node "$OPENCLAW_ENTRY" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-health
|
||||
|
||||
# Assert config + workspace scaffolding.
|
||||
workspace_dir="$OPENCLAW_STATE_DIR/workspace"
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
sessions_dir="$OPENCLAW_STATE_DIR/agents/main/sessions"
|
||||
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
openclaw_e2e_assert_dir "$sessions_dir"
|
||||
for file in AGENTS.md BOOTSTRAP.md IDENTITY.md SOUL.md TOOLS.md USER.md; do
|
||||
openclaw_e2e_assert_file "$workspace_dir/$file"
|
||||
done
|
||||
|
||||
CONFIG_PATH="$config_path" WORKSPACE_DIR="$workspace_dir" node --input-type=module - <<'"'"'NODE'"'"'
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
|
||||
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||
const expectedWorkspace = process.env.WORKSPACE_DIR;
|
||||
const errors = [];
|
||||
|
||||
if (cfg?.agents?.defaults?.workspace !== expectedWorkspace) {
|
||||
errors.push(
|
||||
`agents.defaults.workspace mismatch (got ${cfg?.agents?.defaults?.workspace ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
if (cfg?.gateway?.mode !== "local") {
|
||||
errors.push(`gateway.mode mismatch (got ${cfg?.gateway?.mode ?? "unset"})`);
|
||||
}
|
||||
if (cfg?.gateway?.bind !== "loopback") {
|
||||
errors.push(`gateway.bind mismatch (got ${cfg?.gateway?.bind ?? "unset"})`);
|
||||
}
|
||||
if ((cfg?.gateway?.tailscale?.mode ?? "off") !== "off") {
|
||||
errors.push(
|
||||
`gateway.tailscale.mode mismatch (got ${cfg?.gateway?.tailscale?.mode ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
if (!cfg?.wizard?.lastRunAt) {
|
||||
errors.push("wizard.lastRunAt missing");
|
||||
}
|
||||
if (!cfg?.wizard?.lastRunVersion) {
|
||||
errors.push("wizard.lastRunVersion missing");
|
||||
}
|
||||
if (cfg?.wizard?.lastRunCommand !== "onboard") {
|
||||
errors.push(
|
||||
`wizard.lastRunCommand mismatch (got ${cfg?.wizard?.lastRunCommand ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
if (cfg?.wizard?.lastRunMode !== "local") {
|
||||
errors.push(
|
||||
`wizard.lastRunMode mismatch (got ${cfg?.wizard?.lastRunMode ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
}
|
||||
|
||||
run_case_remote_non_interactive() {
|
||||
set_isolated_openclaw_env remote-non-interactive
|
||||
# Smoke test non-interactive remote config write.
|
||||
openclaw_e2e_run_logged remote-non-interactive node "$OPENCLAW_ENTRY" onboard --non-interactive --accept-risk \
|
||||
--mode remote \
|
||||
--remote-url ws://gateway.local:18789 \
|
||||
--remote-token remote-token \
|
||||
--skip-skills \
|
||||
--skip-health
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
CONFIG_PATH="$config_path" node --input-type=module - <<'"'"'NODE'"'"'
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
|
||||
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||
const errors = [];
|
||||
|
||||
if (cfg?.gateway?.mode !== "remote") {
|
||||
errors.push(`gateway.mode mismatch (got ${cfg?.gateway?.mode ?? "unset"})`);
|
||||
}
|
||||
if (cfg?.gateway?.remote?.url !== "ws://gateway.local:18789") {
|
||||
errors.push(`gateway.remote.url mismatch (got ${cfg?.gateway?.remote?.url ?? "unset"})`);
|
||||
}
|
||||
if (cfg?.gateway?.remote?.token !== "remote-token") {
|
||||
errors.push(`gateway.remote.token mismatch (got ${cfg?.gateway?.remote?.token ?? "unset"})`);
|
||||
}
|
||||
if (cfg?.wizard?.lastRunMode !== "remote") {
|
||||
errors.push(`wizard.lastRunMode mismatch (got ${cfg?.wizard?.lastRunMode ?? "unset"})`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
run_case_reset() {
|
||||
set_isolated_openclaw_env reset-config
|
||||
# Seed a remote config to exercise reset path.
|
||||
cat > "$OPENCLAW_CONFIG_PATH" <<'"'"'JSON'"'"'
|
||||
{
|
||||
"meta": {},
|
||||
"agents": { "defaults": { "workspace": "/root/old" } },
|
||||
"gateway": {
|
||||
"mode": "remote",
|
||||
"remote": { "url": "ws://old.example:18789", "token": "old-token" }
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
openclaw_e2e_run_logged reset-config node "$OPENCLAW_ENTRY" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--mode local \
|
||||
--reset \
|
||||
--skip-channels \
|
||||
--skip-skills \
|
||||
--skip-daemon \
|
||||
--skip-ui \
|
||||
--skip-health
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
CONFIG_PATH="$config_path" node --input-type=module - <<'"'"'NODE'"'"'
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
|
||||
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||
const errors = [];
|
||||
|
||||
if (cfg?.gateway?.mode !== "local") {
|
||||
errors.push(`gateway.mode mismatch (got ${cfg?.gateway?.mode ?? "unset"})`);
|
||||
}
|
||||
if (cfg?.gateway?.remote?.url) {
|
||||
errors.push(`gateway.remote.url should be cleared (got ${cfg?.gateway?.remote?.url})`);
|
||||
}
|
||||
if (cfg?.wizard?.lastRunMode !== "local") {
|
||||
errors.push(`wizard.lastRunMode mismatch (got ${cfg?.wizard?.lastRunMode ?? "unset"})`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
run_case_channels() {
|
||||
# Channels-only configure flow.
|
||||
run_wizard_cmd channels channels "node \"$OPENCLAW_ENTRY\" configure --section channels" send_channels_flow
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
CONFIG_PATH="$config_path" node --input-type=module - <<'"'"'NODE'"'"'
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
|
||||
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||
const errors = [];
|
||||
|
||||
if (cfg?.telegram?.botToken) {
|
||||
errors.push(`telegram.botToken should be unset (got ${cfg?.telegram?.botToken})`);
|
||||
}
|
||||
if (cfg?.discord?.token) {
|
||||
errors.push(`discord.token should be unset (got ${cfg?.discord?.token})`);
|
||||
}
|
||||
if (cfg?.slack?.botToken || cfg?.slack?.appToken) {
|
||||
errors.push(
|
||||
`slack tokens should be unset (got bot=${cfg?.slack?.botToken ?? "unset"}, app=${cfg?.slack?.appToken ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
if (cfg?.wizard?.lastRunCommand !== "configure") {
|
||||
errors.push(
|
||||
`wizard.lastRunCommand mismatch (got ${cfg?.wizard?.lastRunCommand ?? "unset"})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
run_case_skills() {
|
||||
local home_dir
|
||||
set_isolated_openclaw_env skills
|
||||
home_dir="$HOME"
|
||||
# Seed skills config to ensure it survives the wizard.
|
||||
cat > "$OPENCLAW_CONFIG_PATH" <<'"'"'JSON'"'"'
|
||||
{
|
||||
"meta": {},
|
||||
"skills": {
|
||||
"allowBundled": ["__none__"],
|
||||
"install": { "nodeManager": "bun" }
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
run_wizard_cmd skills "$home_dir" "node \"$OPENCLAW_ENTRY\" configure --section skills" send_skills_flow
|
||||
|
||||
config_path="$OPENCLAW_CONFIG_PATH"
|
||||
openclaw_e2e_assert_file "$config_path"
|
||||
|
||||
CONFIG_PATH="$config_path" node --input-type=module - <<'"'"'NODE'"'"'
|
||||
import fs from "node:fs";
|
||||
import JSON5 from "json5";
|
||||
|
||||
const cfg = JSON5.parse(fs.readFileSync(process.env.CONFIG_PATH, "utf-8"));
|
||||
const errors = [];
|
||||
|
||||
if (cfg?.skills?.install?.nodeManager !== "bun") {
|
||||
errors.push(`skills.install.nodeManager mismatch (got ${cfg?.skills?.install?.nodeManager ?? "unset"})`);
|
||||
}
|
||||
if (!Array.isArray(cfg?.skills?.allowBundled) || cfg.skills.allowBundled[0] !== "__none__") {
|
||||
errors.push("skills.allowBundled missing");
|
||||
}
|
||||
if (cfg?.wizard?.lastRunMode !== "local") {
|
||||
errors.push(`wizard.lastRunMode mismatch (got ${cfg?.wizard?.lastRunMode ?? "unset"})`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
}
|
||||
|
||||
validate_local_basic_log() {
|
||||
local log_path="$1"
|
||||
openclaw_e2e_assert_log_not_contains "$log_path" "systemctl --user unavailable"
|
||||
}
|
||||
|
||||
run_case_local_basic
|
||||
run_case_remote_non_interactive
|
||||
run_case_reset
|
||||
run_case_channels
|
||||
run_case_skills
|
||||
'
|
||||
"$IMAGE_NAME" bash scripts/e2e/lib/onboard/scenario.sh
|
||||
|
||||
echo "E2E complete."
|
||||
|
||||
@@ -124,30 +124,15 @@ EOF
|
||||
' >/dev/null
|
||||
|
||||
echo "Waiting for gateway HTTP surface..."
|
||||
gateway_ready=0
|
||||
for _ in $(seq 1 240); do
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" != "true" ]; then
|
||||
break
|
||||
fi
|
||||
if docker_cmd docker exec "$GW_NAME" bash -lc "node --input-type=module -e '
|
||||
if ! docker_e2e_wait_container_bash_while_running "$OW_NAME" "$GW_NAME" 240 1 "node --input-type=module -e '
|
||||
const res = await fetch(\"http://127.0.0.1:$PORT/v1/models\", {
|
||||
headers: { authorization: \"Bearer $TOKEN\" },
|
||||
}).catch(() => null);
|
||||
process.exit(res?.status === 200 ? 0 : 1);
|
||||
' >/dev/null 2>&1"; then
|
||||
gateway_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$gateway_ready" -ne 1 ]; then
|
||||
'"; then
|
||||
echo "Gateway failed to start"
|
||||
docker_cmd docker inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" = "true" ]; then
|
||||
docker_cmd docker exec "$GW_NAME" bash -lc 'tail -n 200 /tmp/openwebui-gateway.log' || true
|
||||
fi
|
||||
docker_cmd docker logs "$GW_NAME" 2>&1 | tail -n 200 || true
|
||||
docker_e2e_docker_cmd inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
docker_e2e_tail_container_file_if_running "$GW_NAME" /tmp/openwebui-gateway.log 200
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -176,34 +161,17 @@ docker_cmd docker run -d \
|
||||
"$OPENWEBUI_IMAGE" >/dev/null
|
||||
|
||||
echo "Waiting for Open WebUI..."
|
||||
ow_ready=0
|
||||
for _ in $(seq 1 240); do
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$OW_NAME" 2>/dev/null || echo false)" != "true" ]; then
|
||||
break
|
||||
fi
|
||||
if docker_cmd docker exec "$GW_NAME" bash -lc "node --input-type=module -e '
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 240 1 "node --input-type=module -e '
|
||||
const res = await fetch(\"http://$OW_NAME:$WEBUI_PORT/\").catch(() => null);
|
||||
process.exit(res && res.status < 500 ? 0 : 1);
|
||||
' >/dev/null 2>&1"; then
|
||||
ow_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$ow_ready" -ne 1 ]; then
|
||||
'"; then
|
||||
echo "Open WebUI failed to start"
|
||||
docker_cmd docker logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
docker_e2e_docker_cmd logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for gateway model endpoint after Open WebUI startup..."
|
||||
gateway_model_ready=0
|
||||
for _ in $(seq 1 90); do
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" != "true" ]; then
|
||||
break
|
||||
fi
|
||||
if docker_cmd docker exec "$GW_NAME" bash -lc "node --input-type=module -e '
|
||||
if ! docker_e2e_wait_container_bash "$GW_NAME" 90 5 "node --input-type=module -e '
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
@@ -217,21 +185,11 @@ for _ in $(seq 1 90); do
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
' >/dev/null 2>&1"; then
|
||||
gateway_model_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ "$gateway_model_ready" -ne 1 ]; then
|
||||
'"; then
|
||||
echo "Gateway model endpoint did not stay reachable after Open WebUI startup"
|
||||
docker_cmd docker inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" = "true" ]; then
|
||||
docker_cmd docker exec "$GW_NAME" bash -lc 'tail -n 200 /tmp/openwebui-gateway.log' || true
|
||||
fi
|
||||
docker_cmd docker logs "$GW_NAME" 2>&1 | tail -n 200 || true
|
||||
docker_cmd docker logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
docker_e2e_docker_cmd inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
docker_e2e_tail_container_file_if_running "$GW_NAME" /tmp/openwebui-gateway.log 200
|
||||
docker_e2e_docker_cmd logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -248,13 +206,10 @@ if ! docker_cmd docker exec \
|
||||
node /app/scripts/e2e/openwebui-probe.mjs >/tmp/openwebui-probe.log 2>&1; then
|
||||
cat /tmp/openwebui-probe.log 2>/dev/null || true
|
||||
echo "Open WebUI probe failed; gateway log tail:"
|
||||
docker_cmd docker inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
if [ "$(docker_cmd docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" = "true" ]; then
|
||||
docker_cmd docker exec "$GW_NAME" bash -lc 'tail -n 200 /tmp/openwebui-gateway.log' || true
|
||||
fi
|
||||
docker_cmd docker logs "$GW_NAME" 2>&1 | tail -n 200 || true
|
||||
docker_e2e_docker_cmd inspect "$GW_NAME" --format '{{json .State}}' 2>/dev/null || true
|
||||
docker_e2e_tail_container_file_if_running "$GW_NAME" /tmp/openwebui-gateway.log 200
|
||||
echo "Open WebUI container logs:"
|
||||
docker_cmd docker logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
docker_e2e_docker_cmd logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
56
scripts/lib/docker-e2e-container.sh
Normal file
56
scripts/lib/docker-e2e-container.sh
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Shared helpers for Docker E2E scripts that keep a named container running
|
||||
# while polling readiness from the host.
|
||||
|
||||
docker_e2e_docker_cmd() {
|
||||
timeout "${DOCKER_COMMAND_TIMEOUT:-600s}" docker "$@"
|
||||
}
|
||||
|
||||
docker_e2e_container_running() {
|
||||
local container_name="$1"
|
||||
[ "$(docker_e2e_docker_cmd inspect -f '{{.State.Running}}' "$container_name" 2>/dev/null || echo false)" = "true" ]
|
||||
}
|
||||
|
||||
docker_e2e_container_exec_bash() {
|
||||
local container_name="$1"
|
||||
shift
|
||||
docker_e2e_docker_cmd exec "$container_name" bash -lc "$*"
|
||||
}
|
||||
|
||||
docker_e2e_wait_container_bash() {
|
||||
local container_name="$1"
|
||||
shift
|
||||
docker_e2e_wait_container_bash_while_running "$container_name" "$container_name" "$@"
|
||||
}
|
||||
|
||||
docker_e2e_wait_container_bash_while_running() {
|
||||
local running_container_name="$1"
|
||||
local exec_container_name="$2"
|
||||
local attempts="$3"
|
||||
local sleep_seconds="$4"
|
||||
shift 4
|
||||
local probe="$*"
|
||||
|
||||
for _ in $(seq 1 "$attempts"); do
|
||||
if ! docker_e2e_container_running "$running_container_name"; then
|
||||
return 1
|
||||
fi
|
||||
if docker_e2e_container_exec_bash "$exec_container_name" "$probe" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep "$sleep_seconds"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
docker_e2e_tail_container_file_if_running() {
|
||||
local container_name="$1"
|
||||
local file_path="$2"
|
||||
local lines="${3:-120}"
|
||||
if docker_e2e_container_running "$container_name"; then
|
||||
docker_e2e_container_exec_bash "$container_name" "tail -n $lines $file_path" || true
|
||||
else
|
||||
docker_e2e_docker_cmd logs "$container_name" 2>&1 | tail -n "$lines" || true
|
||||
fi
|
||||
}
|
||||
@@ -10,6 +10,7 @@ ROOT_DIR="${ROOT_DIR:-$(cd "$DOCKER_E2E_LIB_DIR/../.." && pwd)}"
|
||||
source "$DOCKER_E2E_LIB_DIR/docker-e2e-logs.sh"
|
||||
source "$DOCKER_E2E_LIB_DIR/docker-build.sh"
|
||||
source "$DOCKER_E2E_LIB_DIR/docker-e2e-package.sh"
|
||||
source "$DOCKER_E2E_LIB_DIR/docker-e2e-container.sh"
|
||||
|
||||
docker_e2e_resolve_image() {
|
||||
local default_image="$1"
|
||||
@@ -87,13 +88,13 @@ docker_e2e_test_state_shell_b64() {
|
||||
local scenario="${2:-empty}"
|
||||
node "$ROOT_DIR/scripts/lib/openclaw-test-state.mjs" shell \
|
||||
--label "$label" \
|
||||
--scenario "$scenario" \
|
||||
| base64 \
|
||||
| tr -d '\n'
|
||||
--scenario "$scenario" |
|
||||
base64 |
|
||||
tr -d '\n'
|
||||
}
|
||||
|
||||
docker_e2e_test_state_function_b64() {
|
||||
node "$ROOT_DIR/scripts/lib/openclaw-test-state.mjs" shell-function \
|
||||
| base64 \
|
||||
| tr -d '\n'
|
||||
node "$ROOT_DIR/scripts/lib/openclaw-test-state.mjs" shell-function |
|
||||
base64 |
|
||||
tr -d '\n'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user