mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:50:43 +00:00
refactor(test): simplify bundled channel Docker scenarios
This commit is contained in:
@@ -68,9 +68,7 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Installing mounted OpenClaw package..."
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-install.log 2>&1
|
||||
bundled_channel_install_package /tmp/openclaw-install.log
|
||||
|
||||
command -v openclaw >/dev/null
|
||||
package_root="$(npm root -g)/openclaw"
|
||||
|
||||
@@ -12,11 +12,36 @@ bundled_channel_stage_root() {
|
||||
printf "%s/.openclaw/plugin-runtime-deps" "$HOME"
|
||||
}
|
||||
|
||||
bundled_channel_stage_dir() {
|
||||
printf "%s" "${OPENCLAW_PLUGIN_STAGE_DIR:-$(bundled_channel_stage_root)}"
|
||||
}
|
||||
|
||||
bundled_channel_install_package() {
|
||||
local log_file="$1"
|
||||
local label="${2:-mounted OpenClaw package}"
|
||||
local package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
echo "Installing $label..."
|
||||
if ! npm install -g "$package_tgz" --no-fund --no-audit >"$log_file" 2>&1; then
|
||||
echo "npm install -g failed for $label" >&2
|
||||
cat "$log_file" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
bundled_channel_find_external_dep_package() {
|
||||
local dep_path="$1"
|
||||
find "$(bundled_channel_stage_root)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
|
||||
}
|
||||
|
||||
bundled_channel_find_staged_dep_package() {
|
||||
local dep_path="$1"
|
||||
find "$(bundled_channel_stage_dir)" -maxdepth 12 -path "*/node_modules/$dep_path/package.json" -type f -print -quit 2>/dev/null || true
|
||||
}
|
||||
|
||||
bundled_channel_dump_stage_dir() {
|
||||
find "$(bundled_channel_stage_dir)" -maxdepth 12 -type f | sort | head -160 >&2 || true
|
||||
}
|
||||
|
||||
bundled_channel_assert_no_package_dep_available() {
|
||||
local channel="$1"
|
||||
local dep_path="$2"
|
||||
@@ -62,6 +87,91 @@ bundled_channel_assert_no_dep_available() {
|
||||
fi
|
||||
}
|
||||
|
||||
bundled_channel_assert_no_staged_dep() {
|
||||
local channel="$1"
|
||||
local dep_path="$2"
|
||||
local message="${3:-$channel unexpectedly staged $dep_path}"
|
||||
if [ -n "$(bundled_channel_find_staged_dep_package "$dep_path")" ]; then
|
||||
echo "$message" >&2
|
||||
bundled_channel_dump_stage_dir
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
bundled_channel_assert_staged_dep() {
|
||||
local channel="$1"
|
||||
local dep_path="$2"
|
||||
local log_file="${3:-}"
|
||||
if [ -n "$(bundled_channel_find_staged_dep_package "$dep_path")" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "missing external staged dependency sentinel for $channel: $dep_path" >&2
|
||||
if [ -n "$log_file" ]; then
|
||||
cat "$log_file" >&2 || true
|
||||
fi
|
||||
bundled_channel_dump_stage_dir
|
||||
exit 1
|
||||
}
|
||||
|
||||
bundled_channel_assert_no_staged_manifest_spec() {
|
||||
local channel="$1"
|
||||
local dep_path="$2"
|
||||
local log_file="${3:-}"
|
||||
if ! node - <<'NODE' "$(bundled_channel_stage_dir)" "$dep_path"
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const stageDir = process.argv[2];
|
||||
const depName = process.argv[3];
|
||||
const manifestName = ".openclaw-runtime-deps.json";
|
||||
const matches = [];
|
||||
|
||||
function visit(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
visit(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.name !== manifestName) {
|
||||
continue;
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const specs = Array.isArray(parsed.specs) ? parsed.specs : [];
|
||||
for (const spec of specs) {
|
||||
if (typeof spec === "string" && spec.startsWith(`${depName}@`)) {
|
||||
matches.push(`${fullPath}: ${spec}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(stageDir);
|
||||
if (matches.length > 0) {
|
||||
process.stderr.write(`${matches.join("\n")}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
then
|
||||
echo "$channel unexpectedly selected $dep_path for external runtime deps" >&2
|
||||
if [ -n "$log_file" ]; then
|
||||
cat "$log_file" >&2 || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
bundled_channel_remove_runtime_dep() {
|
||||
local channel="$1"
|
||||
local dep_path="$2"
|
||||
@@ -74,18 +184,63 @@ bundled_channel_remove_runtime_dep() {
|
||||
|
||||
bundled_channel_write_config() {
|
||||
local mode="$1"
|
||||
node - <<'NODE' "$mode" "${TOKEN:?missing TOKEN}" "${PORT:?missing PORT}"
|
||||
node - <<'NODE' "$mode" "${TOKEN:-bundled-channel-config-token}" "${PORT:-18789}"
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const mode = process.argv[2];
|
||||
const token = process.argv[3];
|
||||
const port = Number(process.argv[4]);
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const configPath =
|
||||
process.env.OPENCLAW_BUNDLED_CHANNEL_CONFIG_PATH ||
|
||||
path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath)
|
||||
? JSON.parse(fs.readFileSync(configPath, "utf8"))
|
||||
: {};
|
||||
|
||||
if (mode === "disabled-config") {
|
||||
const stateDir = path.dirname(configPath);
|
||||
const disabledConfig = {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "disabled-config-runtime-deps-token",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
enabled: true,
|
||||
entries: {
|
||||
discord: { enabled: false },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: false,
|
||||
botToken: "123456:disabled-config-token",
|
||||
dmPolicy: "disabled",
|
||||
groupPolicy: "disabled",
|
||||
},
|
||||
slack: {
|
||||
enabled: false,
|
||||
botToken: "xoxb-disabled-config-token",
|
||||
appToken: "xapp-disabled-config-token",
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "disabled-plugin-entry-token",
|
||||
dmPolicy: "disabled",
|
||||
groupPolicy: "disabled",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(path.join(stateDir, "agents", "main", "sessions"), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(disabledConfig, null, 2)}\n`, "utf8");
|
||||
fs.chmodSync(stateDir, 0o700);
|
||||
fs.chmodSync(configPath, 0o600);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
config.gateway = {
|
||||
...(config.gateway || {}),
|
||||
port,
|
||||
@@ -120,7 +275,9 @@ config.channels = {
|
||||
telegram: {
|
||||
...(config.channels?.telegram || {}),
|
||||
enabled: mode === "telegram",
|
||||
botToken: "123456:bundled-channel-update-token",
|
||||
botToken:
|
||||
process.env.OPENCLAW_BUNDLED_CHANNEL_TELEGRAM_TOKEN ||
|
||||
"123456:bundled-channel-update-token",
|
||||
dmPolicy: "disabled",
|
||||
groupPolicy: "disabled",
|
||||
},
|
||||
@@ -133,8 +290,12 @@ config.channels = {
|
||||
slack: {
|
||||
...(config.channels?.slack || {}),
|
||||
enabled: mode === "slack",
|
||||
botToken: "xoxb-bundled-channel-update-token",
|
||||
appToken: "xapp-bundled-channel-update-token",
|
||||
botToken:
|
||||
process.env.OPENCLAW_BUNDLED_CHANNEL_SLACK_BOT_TOKEN ||
|
||||
"xoxb-bundled-channel-update-token",
|
||||
appToken:
|
||||
process.env.OPENCLAW_BUNDLED_CHANNEL_SLACK_APP_TOKEN ||
|
||||
"xapp-bundled-channel-update-token",
|
||||
},
|
||||
feishu: {
|
||||
...(config.channels?.feishu || {}),
|
||||
@@ -187,6 +348,23 @@ if (mode === "acpx") {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (mode === "setup-entry-channels") {
|
||||
config.plugins = {
|
||||
...(config.plugins || {}),
|
||||
enabled: true,
|
||||
};
|
||||
config.channels = {
|
||||
...(config.channels || {}),
|
||||
feishu: {
|
||||
...(config.channels?.feishu || {}),
|
||||
enabled: true,
|
||||
},
|
||||
whatsapp: {
|
||||
...(config.channels?.whatsapp || {}),
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
|
||||
@@ -29,72 +29,11 @@ assert_dep_absent_everywhere() {
|
||||
local channel="$1"
|
||||
local dep_path="$2"
|
||||
local root="$3"
|
||||
for candidate in \
|
||||
"$root/dist/extensions/$channel/node_modules/$dep_path/package.json" \
|
||||
"$root/dist/extensions/node_modules/$dep_path/package.json" \
|
||||
"$root/node_modules/$dep_path/package.json"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
echo "disabled $channel unexpectedly installed $dep_path at $candidate" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! node - <<'NODE' "$OPENCLAW_PLUGIN_STAGE_DIR" "$dep_path"
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const stageDir = process.argv[2];
|
||||
const depName = process.argv[3];
|
||||
const manifestName = ".openclaw-runtime-deps.json";
|
||||
const matches = [];
|
||||
|
||||
function visit(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
visit(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.name !== manifestName) {
|
||||
continue;
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const specs = Array.isArray(parsed.specs) ? parsed.specs : [];
|
||||
for (const spec of specs) {
|
||||
if (typeof spec === "string" && spec.startsWith(`${depName}@`)) {
|
||||
matches.push(`${fullPath}: ${spec}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
bundled_channel_assert_no_package_dep_available "$channel" "$dep_path" "$root"
|
||||
bundled_channel_assert_no_staged_manifest_spec "$channel" "$dep_path" /tmp/openclaw-disabled-config-doctor.log
|
||||
}
|
||||
|
||||
visit(stageDir);
|
||||
if (matches.length > 0) {
|
||||
process.stderr.write(`${matches.join("\n")}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
then
|
||||
echo "disabled $channel unexpectedly selected $dep_path for external runtime deps" >&2
|
||||
cat /tmp/openclaw-disabled-config-doctor.log >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Installing mounted OpenClaw package..."
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-disabled-config-install.log 2>&1
|
||||
bundled_channel_install_package /tmp/openclaw-disabled-config-install.log
|
||||
|
||||
root="$(bundled_channel_package_root)"
|
||||
test -d "$root/dist/extensions/telegram"
|
||||
@@ -104,51 +43,7 @@ rm -rf "$root/dist/extensions/telegram/node_modules"
|
||||
rm -rf "$root/dist/extensions/discord/node_modules"
|
||||
rm -rf "$root/dist/extensions/slack/node_modules"
|
||||
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const stateDir = path.dirname(configPath);
|
||||
const config = {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "disabled-config-runtime-deps-token",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
enabled: true,
|
||||
entries: {
|
||||
discord: { enabled: false },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: false,
|
||||
botToken: "123456:disabled-config-token",
|
||||
dmPolicy: "disabled",
|
||||
groupPolicy: "disabled",
|
||||
},
|
||||
slack: {
|
||||
enabled: false,
|
||||
botToken: "xoxb-disabled-config-token",
|
||||
appToken: "xapp-disabled-config-token",
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "disabled-plugin-entry-token",
|
||||
dmPolicy: "disabled",
|
||||
groupPolicy: "disabled",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(path.join(stateDir, "agents", "main", "sessions"), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
fs.chmodSync(stateDir, 0o700);
|
||||
fs.chmodSync(configPath, 0o600);
|
||||
NODE
|
||||
bundled_channel_write_config disabled-config
|
||||
|
||||
if ! openclaw doctor --non-interactive >/tmp/openclaw-disabled-config-doctor.log 2>&1; then
|
||||
echo "doctor failed for disabled-config runtime deps smoke" >&2
|
||||
|
||||
85
scripts/e2e/lib/bundled-channel/guided-whatsapp-setup.mjs
Normal file
85
scripts/e2e/lib/bundled-channel/guided-whatsapp-setup.mjs
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
import { readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const root = process.argv[2] || process.env.OPENCLAW_PACKAGE_ROOT;
|
||||
if (!root) {
|
||||
throw new Error("missing package root");
|
||||
}
|
||||
|
||||
const distDir = path.join(root, "dist");
|
||||
const onboardChannelFiles = (await readdir(distDir))
|
||||
.filter((entry) => /^onboard-channels-.*\.js$/.test(entry))
|
||||
.toSorted();
|
||||
let setupChannels;
|
||||
for (const entry of onboardChannelFiles) {
|
||||
const module = await import(pathToFileURL(path.join(distDir, entry)));
|
||||
if (typeof module.setupChannels === "function") {
|
||||
setupChannels = module.setupChannels;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!setupChannels) {
|
||||
throw new Error(
|
||||
`could not find packaged setupChannels export in ${JSON.stringify(onboardChannelFiles)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let channelSelectCount = 0;
|
||||
const notes = [];
|
||||
const prompter = {
|
||||
intro: async () => {},
|
||||
outro: async () => {},
|
||||
note: async (body, title) => {
|
||||
notes.push({ title, body });
|
||||
},
|
||||
confirm: async ({ message, initialValue }) => {
|
||||
if (message === "Link WhatsApp now (QR)?") {
|
||||
return false;
|
||||
}
|
||||
return initialValue ?? true;
|
||||
},
|
||||
select: async ({ message, options }) => {
|
||||
if (message === "Select a channel") {
|
||||
channelSelectCount += 1;
|
||||
return channelSelectCount === 1 ? "whatsapp" : "__done__";
|
||||
}
|
||||
if (message === "Install WhatsApp plugin?") {
|
||||
if (!options?.some((option) => option.value === "local")) {
|
||||
throw new Error(`missing bundled local install option: ${JSON.stringify(options)}`);
|
||||
}
|
||||
return "local";
|
||||
}
|
||||
if (message === "WhatsApp phone setup") {
|
||||
return "separate";
|
||||
}
|
||||
if (message === "WhatsApp DM policy") {
|
||||
return "disabled";
|
||||
}
|
||||
throw new Error(`unexpected select prompt: ${message}`);
|
||||
},
|
||||
multiselect: async ({ message }) => {
|
||||
throw new Error(`unexpected multiselect prompt: ${message}`);
|
||||
},
|
||||
text: async ({ message }) => {
|
||||
throw new Error(`unexpected text prompt: ${message}`);
|
||||
},
|
||||
};
|
||||
const runtime = {
|
||||
log: (message) => console.log(message),
|
||||
error: (message) => console.error(message),
|
||||
};
|
||||
|
||||
const result = await setupChannels({ plugins: { enabled: true } }, runtime, prompter, {
|
||||
deferStatusUntilSelection: true,
|
||||
skipConfirm: true,
|
||||
skipStatusNote: true,
|
||||
skipDmPolicyPrompt: true,
|
||||
initialSelection: ["whatsapp"],
|
||||
});
|
||||
|
||||
if (!result.channels?.whatsapp) {
|
||||
throw new Error(`WhatsApp setup did not write channel config: ${JSON.stringify(result)}`);
|
||||
}
|
||||
console.log("packaged guided WhatsApp setup completed");
|
||||
@@ -23,9 +23,7 @@ export NPM_CONFIG_PREFIX="$HOME/.npm-global"
|
||||
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
|
||||
export OPENCLAW_NO_ONBOARD=1
|
||||
|
||||
echo "Installing mounted OpenClaw package..."
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-load-failure-install.log 2>&1
|
||||
bundled_channel_install_package /tmp/openclaw-load-failure-install.log
|
||||
|
||||
root="$(bundled_channel_package_root)"
|
||||
plugin_dir="$root/dist/extensions/load-failure-alpha"
|
||||
@@ -85,85 +83,7 @@ export default {
|
||||
JS
|
||||
|
||||
echo "Loading synthetic failing bundled channel through packaged loader..."
|
||||
(
|
||||
cd "$root"
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR="$root/dist/extensions" node --input-type=module - <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const root = process.cwd();
|
||||
const distDir = path.join(root, "dist");
|
||||
const bundledPath = fs
|
||||
.readdirSync(distDir)
|
||||
.filter((entry) => /^bundled-[A-Za-z0-9_-]+\.js$/.test(entry))
|
||||
.map((entry) => path.join(distDir, entry))
|
||||
.find((entry) => fs.readFileSync(entry, "utf8").includes("src/channels/plugins/bundled.ts"));
|
||||
if (!bundledPath) {
|
||||
throw new Error("missing packaged bundled channel loader artifact");
|
||||
}
|
||||
const bundled = await import(pathToFileURL(bundledPath));
|
||||
const loaderNames = [
|
||||
"getBundledChannelPlugin",
|
||||
"getBundledChannelSetupPlugin",
|
||||
"getBundledChannelSecrets",
|
||||
"getBundledChannelSetupSecrets",
|
||||
];
|
||||
const exportedLoaders = new Map(
|
||||
Object.values(bundled)
|
||||
.filter((value) => typeof value === "function")
|
||||
.map((fn) => [fn.name, fn]),
|
||||
);
|
||||
const loaders = loaderNames.map((name) => {
|
||||
const fn = exportedLoaders.get(name);
|
||||
if (typeof fn !== "function") {
|
||||
throw new Error(`missing packaged bundled loader export ${name}; exports=${Object.keys(bundled).join(",")}`);
|
||||
}
|
||||
return [name, fn];
|
||||
});
|
||||
|
||||
const id = "load-failure-alpha";
|
||||
function exerciseLoaders() {
|
||||
for (const [name, fn] of loaders) {
|
||||
try {
|
||||
fn(id);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes("synthetic")) {
|
||||
throw new Error(`bundled export ${name} leaked synthetic load failure: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadCounts() {
|
||||
return {
|
||||
plugin: globalThis.__loadFailurePlugin,
|
||||
setup: globalThis.__loadFailureSetup,
|
||||
secrets: globalThis.__loadFailureSecrets,
|
||||
setupSecrets: globalThis.__loadFailureSetupSecrets,
|
||||
};
|
||||
}
|
||||
|
||||
exerciseLoaders();
|
||||
const firstCounts = loadCounts();
|
||||
exerciseLoaders();
|
||||
const secondCounts = loadCounts();
|
||||
for (const key of ["plugin", "setup", "setupSecrets"]) {
|
||||
const first = firstCounts[key];
|
||||
if (!Number.isInteger(first) || first < 1) {
|
||||
throw new Error(`expected ${key} failure to be exercised at least once, got ${first}`);
|
||||
}
|
||||
if (secondCounts[key] !== first) {
|
||||
throw new Error(`expected ${key} failure to be cached after first pass, got ${first} then ${secondCounts[key]}`);
|
||||
}
|
||||
}
|
||||
if (firstCounts.secrets !== undefined && secondCounts.secrets !== firstCounts.secrets) {
|
||||
throw new Error(`expected secrets failure to be cached after first pass, got ${firstCounts.secrets} then ${secondCounts.secrets}`);
|
||||
}
|
||||
console.log("synthetic bundled channel load failures were isolated and cached");
|
||||
NODE
|
||||
)
|
||||
node scripts/e2e/lib/bundled-channel/loader-probe.mjs load-failure "$root" load-failure-alpha
|
||||
|
||||
echo "bundled channel load-failure isolation Docker E2E passed"
|
||||
EOF
|
||||
|
||||
121
scripts/e2e/lib/bundled-channel/loader-probe.mjs
Normal file
121
scripts/e2e/lib/bundled-channel/loader-probe.mjs
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
function usage() {
|
||||
console.error("Usage: loader-probe.mjs <setup-entries|load-failure> <package-root> [channel...]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function findBundledLoader(root) {
|
||||
const distDir = path.join(root, "dist");
|
||||
const bundledPath = fs
|
||||
.readdirSync(distDir)
|
||||
.filter((entry) => /^bundled-[A-Za-z0-9_-]+\.js$/.test(entry))
|
||||
.map((entry) => path.join(distDir, entry))
|
||||
.find((entry) => fs.readFileSync(entry, "utf8").includes("src/channels/plugins/bundled.ts"));
|
||||
if (!bundledPath) {
|
||||
throw new Error("missing packaged bundled channel loader artifact");
|
||||
}
|
||||
return bundledPath;
|
||||
}
|
||||
|
||||
function namedExport(module, name) {
|
||||
const fn = Object.values(module).find(
|
||||
(value) => typeof value === "function" && value.name === name,
|
||||
);
|
||||
if (typeof fn !== "function") {
|
||||
throw new Error(
|
||||
`missing packaged bundled loader export ${name}; exports=${Object.keys(module).join(",")}`,
|
||||
);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
async function importBundled(root) {
|
||||
return import(pathToFileURL(findBundledLoader(root)));
|
||||
}
|
||||
|
||||
function loadCounts() {
|
||||
return {
|
||||
plugin: globalThis.__loadFailurePlugin,
|
||||
setup: globalThis.__loadFailureSetup,
|
||||
secrets: globalThis.__loadFailureSecrets,
|
||||
setupSecrets: globalThis.__loadFailureSetupSecrets,
|
||||
};
|
||||
}
|
||||
|
||||
function exerciseLoaders(loaders, id) {
|
||||
for (const [name, fn] of loaders) {
|
||||
try {
|
||||
fn(id);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes("synthetic")) {
|
||||
throw new Error(`bundled export ${name} leaked synthetic load failure: ${message}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [command, root, ...args] = process.argv.slice(2);
|
||||
if (!command || !root) {
|
||||
usage();
|
||||
}
|
||||
|
||||
if (command === "load-failure") {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(root, "dist/extensions");
|
||||
}
|
||||
|
||||
const bundled = await importBundled(root);
|
||||
|
||||
if (command === "setup-entries") {
|
||||
const channels = args.length > 0 ? args : ["feishu", "whatsapp"];
|
||||
const setupPluginLoader = namedExport(bundled, "getBundledChannelSetupPlugin");
|
||||
for (const channel of channels) {
|
||||
const plugin = setupPluginLoader(channel);
|
||||
if (!plugin) {
|
||||
throw new Error(`${channel} setup plugin did not load pre-config`);
|
||||
}
|
||||
if (plugin.id !== channel) {
|
||||
throw new Error(`${channel} setup plugin id mismatch: ${plugin.id}`);
|
||||
}
|
||||
console.log(`${channel} setup plugin loaded pre-config`);
|
||||
}
|
||||
} else if (command === "load-failure") {
|
||||
const id = args[0] || "load-failure-alpha";
|
||||
const loaderNames = [
|
||||
"getBundledChannelPlugin",
|
||||
"getBundledChannelSetupPlugin",
|
||||
"getBundledChannelSecrets",
|
||||
"getBundledChannelSetupSecrets",
|
||||
];
|
||||
const loaders = loaderNames.map((name) => [name, namedExport(bundled, name)]);
|
||||
|
||||
exerciseLoaders(loaders, id);
|
||||
const firstCounts = loadCounts();
|
||||
exerciseLoaders(loaders, id);
|
||||
const secondCounts = loadCounts();
|
||||
for (const key of ["plugin", "setup", "setupSecrets"]) {
|
||||
const first = firstCounts[key];
|
||||
if (!Number.isInteger(first) || first < 1) {
|
||||
throw new Error(`expected ${key} failure to be exercised at least once, got ${first}`);
|
||||
}
|
||||
if (secondCounts[key] !== first) {
|
||||
throw new Error(
|
||||
`expected ${key} failure to be cached after first pass, got ${first} then ${secondCounts[key]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (firstCounts.secrets !== undefined && secondCounts.secrets !== firstCounts.secrets) {
|
||||
throw new Error(
|
||||
`expected secrets failure to be cached after first pass, got ${firstCounts.secrets} then ${secondCounts.secrets}`,
|
||||
);
|
||||
}
|
||||
console.log("synthetic bundled channel load failures were isolated and cached");
|
||||
} else {
|
||||
usage();
|
||||
}
|
||||
@@ -33,13 +33,7 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Installing mounted OpenClaw package into root-owned global npm..."
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
if ! npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-root-owned-install.log 2>&1; then
|
||||
echo "root-owned global npm install failed" >&2
|
||||
cat /tmp/openclaw-root-owned-install.log >&2
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_install_package /tmp/openclaw-root-owned-install.log "mounted OpenClaw package into root-owned global npm"
|
||||
|
||||
root="$(bundled_channel_package_root)"
|
||||
test -d "$root/dist/extensions/$CHANNEL"
|
||||
@@ -53,44 +47,10 @@ if runuser -u appuser -- test -w "$root"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node - <<'NODE' "$TOKEN" "$PORT"
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const token = process.argv[2];
|
||||
const port = Number(process.argv[3]);
|
||||
const configPath = "/home/appuser/.openclaw/openclaw.json";
|
||||
const config = {
|
||||
gateway: {
|
||||
port,
|
||||
auth: { mode: "token", token },
|
||||
controlUi: { enabled: false },
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: { enabled: true },
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-bundled-channel-root-owned-token",
|
||||
appToken: "xapp-bundled-channel-root-owned-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
OPENCLAW_BUNDLED_CHANNEL_CONFIG_PATH=/home/appuser/.openclaw/openclaw.json \
|
||||
OPENCLAW_BUNDLED_CHANNEL_SLACK_BOT_TOKEN=xoxb-bundled-channel-root-owned-token \
|
||||
OPENCLAW_BUNDLED_CHANNEL_SLACK_APP_TOKEN=xapp-bundled-channel-root-owned-token \
|
||||
bundled_channel_write_config slack
|
||||
chown appuser:appuser /home/appuser/.openclaw/openclaw.json
|
||||
|
||||
start_gateway() {
|
||||
@@ -140,25 +100,16 @@ wait_for_slack_provider_start() {
|
||||
start_gateway /tmp/openclaw-root-owned-gateway.log
|
||||
wait_for_slack_provider_start
|
||||
|
||||
if [ -e "$root/dist/extensions/$CHANNEL/node_modules/$DEP_SENTINEL/package.json" ]; then
|
||||
echo "root-owned package tree was mutated" >&2
|
||||
find "$root/dist/extensions/$CHANNEL/node_modules" -maxdepth 4 -type f | sort | head -80 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if ! find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/$DEP_SENTINEL/package.json" -type f | grep -q .; then
|
||||
echo "missing external staged dependency sentinel for $DEP_SENTINEL" >&2
|
||||
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -120 >&2 || true
|
||||
cat /tmp/openclaw-root-owned-gateway.log >&2
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_assert_no_package_dep_available "$CHANNEL" "$DEP_SENTINEL" "$root"
|
||||
bundled_channel_assert_staged_dep "$CHANNEL" "$DEP_SENTINEL" /tmp/openclaw-root-owned-gateway.log
|
||||
if [ -e "$root/dist/extensions/node_modules/openclaw/package.json" ]; then
|
||||
echo "root-owned package tree was mutated with SDK alias" >&2
|
||||
find "$root/dist/extensions/node_modules/openclaw" -maxdepth 4 -type f | sort | head -80 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if ! find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/dist/extensions/node_modules/openclaw/package.json" -type f | grep -q .; then
|
||||
if ! find "$(bundled_channel_stage_dir)" -maxdepth 12 -path "*/dist/extensions/node_modules/openclaw/package.json" -type f | grep -q .; then
|
||||
echo "missing external staged openclaw/plugin-sdk alias" >&2
|
||||
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -120 >&2 || true
|
||||
bundled_channel_dump_stage_dir
|
||||
cat /tmp/openclaw-root-owned-gateway.log >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -30,225 +30,39 @@ declare -A SETUP_ENTRY_DEP_SENTINELS=(
|
||||
[whatsapp]="@whiskeysockets/baileys"
|
||||
)
|
||||
|
||||
echo "Installing mounted OpenClaw package..."
|
||||
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
|
||||
if ! npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-setup-entry-install.log 2>&1; then
|
||||
cat /tmp/openclaw-setup-entry-install.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_install_package /tmp/openclaw-setup-entry-install.log
|
||||
|
||||
root="$(bundled_channel_package_root)"
|
||||
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
|
||||
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
|
||||
test -d "$root/dist/extensions/$channel"
|
||||
if [ -d "$root/dist/extensions/$channel/node_modules" ]; then
|
||||
echo "$channel runtime deps should not be preinstalled in package" >&2
|
||||
find "$root/dist/extensions/$channel/node_modules" -maxdepth 3 -type f | head -40 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$root/node_modules/$dep_sentinel/package.json" ]; then
|
||||
echo "$dep_sentinel should not be installed at package root before setup-entry load" >&2
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
|
||||
done
|
||||
|
||||
echo "Probing real bundled setup entries before channel configuration..."
|
||||
(
|
||||
cd "$root"
|
||||
node --input-type=module - <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const root = process.cwd();
|
||||
const distDir = path.join(root, "dist");
|
||||
const bundledPath = fs
|
||||
.readdirSync(distDir)
|
||||
.filter((entry) => /^bundled-[A-Za-z0-9_-]+\.js$/.test(entry))
|
||||
.map((entry) => path.join(distDir, entry))
|
||||
.find((entry) => fs.readFileSync(entry, "utf8").includes("src/channels/plugins/bundled.ts"));
|
||||
if (!bundledPath) {
|
||||
throw new Error("missing packaged bundled channel loader artifact");
|
||||
}
|
||||
const bundled = await import(pathToFileURL(bundledPath));
|
||||
const setupPluginLoader = Object.values(bundled).find(
|
||||
(value) => typeof value === "function" && value.name === "getBundledChannelSetupPlugin",
|
||||
);
|
||||
if (!setupPluginLoader) {
|
||||
throw new Error("missing packaged getBundledChannelSetupPlugin export");
|
||||
}
|
||||
for (const channel of ["feishu", "whatsapp"]) {
|
||||
const plugin = setupPluginLoader(channel);
|
||||
if (!plugin) {
|
||||
throw new Error(`${channel} setup plugin did not load pre-config`);
|
||||
}
|
||||
if (plugin.id !== channel) {
|
||||
throw new Error(`${channel} setup plugin id mismatch: ${plugin.id}`);
|
||||
}
|
||||
console.log(`${channel} setup plugin loaded pre-config`);
|
||||
}
|
||||
NODE
|
||||
)
|
||||
node scripts/e2e/lib/bundled-channel/loader-probe.mjs setup-entries "$root" feishu whatsapp
|
||||
|
||||
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
|
||||
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
|
||||
if [ -e "$root/dist/extensions/$channel/node_modules/$dep_sentinel/package.json" ]; then
|
||||
echo "setup-entry discovery installed $channel deps into bundled plugin tree before channel configuration" >&2
|
||||
exit 1
|
||||
fi
|
||||
if find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/$dep_sentinel/package.json" -type f | grep -q .; then
|
||||
echo "setup-entry discovery installed $channel external staged deps before channel configuration" >&2
|
||||
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -160 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
|
||||
bundled_channel_assert_no_staged_dep "$channel" "$dep_sentinel" "setup-entry discovery installed $channel external staged deps before channel configuration"
|
||||
done
|
||||
|
||||
echo "Running packaged guided WhatsApp setup; runtime deps should be staged before finalize..."
|
||||
OPENCLAW_PACKAGE_ROOT="$root" node --input-type=module - <<'NODE'
|
||||
import path from "node:path";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
node scripts/e2e/lib/bundled-channel/guided-whatsapp-setup.mjs "$root"
|
||||
|
||||
const root = process.env.OPENCLAW_PACKAGE_ROOT;
|
||||
if (!root) {
|
||||
throw new Error("missing OPENCLAW_PACKAGE_ROOT");
|
||||
}
|
||||
const distDir = path.join(root, "dist");
|
||||
const onboardChannelFiles = (await readdir(distDir))
|
||||
.filter((entry) => /^onboard-channels-.*\.js$/.test(entry))
|
||||
.sort();
|
||||
let setupChannels;
|
||||
for (const entry of onboardChannelFiles) {
|
||||
const module = await import(pathToFileURL(path.join(distDir, entry)));
|
||||
if (typeof module.setupChannels === "function") {
|
||||
setupChannels = module.setupChannels;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!setupChannels) {
|
||||
throw new Error(
|
||||
`could not find packaged setupChannels export in ${JSON.stringify(onboardChannelFiles)}`,
|
||||
);
|
||||
}
|
||||
|
||||
let channelSelectCount = 0;
|
||||
const notes = [];
|
||||
const prompter = {
|
||||
intro: async () => {},
|
||||
outro: async () => {},
|
||||
note: async (body, title) => {
|
||||
notes.push({ title, body });
|
||||
},
|
||||
confirm: async ({ message, initialValue }) => {
|
||||
if (message === "Link WhatsApp now (QR)?") {
|
||||
return false;
|
||||
}
|
||||
return initialValue ?? true;
|
||||
},
|
||||
select: async ({ message, options }) => {
|
||||
if (message === "Select a channel") {
|
||||
channelSelectCount += 1;
|
||||
return channelSelectCount === 1 ? "whatsapp" : "__done__";
|
||||
}
|
||||
if (message === "Install WhatsApp plugin?") {
|
||||
if (!options?.some((option) => option.value === "local")) {
|
||||
throw new Error(`missing bundled local install option: ${JSON.stringify(options)}`);
|
||||
}
|
||||
return "local";
|
||||
}
|
||||
if (message === "WhatsApp phone setup") {
|
||||
return "separate";
|
||||
}
|
||||
if (message === "WhatsApp DM policy") {
|
||||
return "disabled";
|
||||
}
|
||||
throw new Error(`unexpected select prompt: ${message}`);
|
||||
},
|
||||
multiselect: async ({ message }) => {
|
||||
throw new Error(`unexpected multiselect prompt: ${message}`);
|
||||
},
|
||||
text: async ({ message }) => {
|
||||
throw new Error(`unexpected text prompt: ${message}`);
|
||||
},
|
||||
};
|
||||
const runtime = {
|
||||
log: (message) => console.log(message),
|
||||
error: (message) => console.error(message),
|
||||
};
|
||||
|
||||
const result = await setupChannels(
|
||||
{ plugins: { enabled: true } },
|
||||
runtime,
|
||||
prompter,
|
||||
{
|
||||
deferStatusUntilSelection: true,
|
||||
skipConfirm: true,
|
||||
skipStatusNote: true,
|
||||
skipDmPolicyPrompt: true,
|
||||
initialSelection: ["whatsapp"],
|
||||
},
|
||||
);
|
||||
|
||||
if (!result.channels?.whatsapp) {
|
||||
throw new Error(`WhatsApp setup did not write channel config: ${JSON.stringify(result)}`);
|
||||
}
|
||||
console.log("packaged guided WhatsApp setup completed");
|
||||
NODE
|
||||
|
||||
if [ -e "$root/dist/extensions/whatsapp/node_modules/@whiskeysockets/baileys/package.json" ]; then
|
||||
echo "expected guided WhatsApp setup deps to be installed externally, not into bundled plugin tree" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/@whiskeysockets/baileys/package.json" -type f | grep -q .; then
|
||||
echo "guided WhatsApp setup did not stage @whiskeysockets/baileys before finalize" >&2
|
||||
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -160 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_assert_no_package_dep_available whatsapp @whiskeysockets/baileys "$root"
|
||||
bundled_channel_assert_staged_dep whatsapp @whiskeysockets/baileys
|
||||
|
||||
echo "Configuring setup-entry channels; doctor should now install bundled runtime deps externally..."
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
const config = fs.existsSync(configPath)
|
||||
? JSON.parse(fs.readFileSync(configPath, "utf8"))
|
||||
: {};
|
||||
|
||||
config.plugins = {
|
||||
...(config.plugins || {}),
|
||||
enabled: true,
|
||||
};
|
||||
config.channels = {
|
||||
...(config.channels || {}),
|
||||
feishu: {
|
||||
...(config.channels?.feishu || {}),
|
||||
enabled: true,
|
||||
},
|
||||
whatsapp: {
|
||||
...(config.channels?.whatsapp || {}),
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
bundled_channel_write_config setup-entry-channels
|
||||
|
||||
openclaw doctor --non-interactive >/tmp/openclaw-setup-entry-doctor.log 2>&1
|
||||
|
||||
for channel in "${!SETUP_ENTRY_DEP_SENTINELS[@]}"; do
|
||||
dep_sentinel="${SETUP_ENTRY_DEP_SENTINELS[$channel]}"
|
||||
if [ -e "$root/dist/extensions/$channel/node_modules/$dep_sentinel/package.json" ]; then
|
||||
echo "expected configured $channel deps to be installed externally, not into bundled plugin tree" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -path "*/node_modules/$dep_sentinel/package.json" -type f | grep -q .; then
|
||||
echo "missing external staged dependency sentinel for configured $channel: $dep_sentinel" >&2
|
||||
cat /tmp/openclaw-setup-entry-doctor.log >&2
|
||||
find "$OPENCLAW_PLUGIN_STAGE_DIR" -maxdepth 12 -type f | sort | head -160 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
bundled_channel_assert_no_package_dep_available "$channel" "$dep_sentinel" "$root"
|
||||
bundled_channel_assert_staged_dep "$channel" "$dep_sentinel" /tmp/openclaw-setup-entry-doctor.log
|
||||
done
|
||||
|
||||
echo "bundled channel setup-entry runtime deps Docker E2E passed"
|
||||
|
||||
@@ -113,9 +113,8 @@ should_run_update_target() {
|
||||
esac
|
||||
}
|
||||
|
||||
echo "Installing current candidate as update baseline..."
|
||||
echo "Update targets: $UPDATE_TARGETS"
|
||||
npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-update-baseline-install.log 2>&1
|
||||
bundled_channel_install_package /tmp/openclaw-update-baseline-install.log "current candidate as update baseline"
|
||||
command -v openclaw >/dev/null
|
||||
poison_home_npm_project
|
||||
baseline_root="$(bundled_channel_package_root)"
|
||||
|
||||
Reference in New Issue
Block a user