mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
test(resilience): cover broken plugin startup and onboarding
This commit is contained in:
@@ -624,6 +624,73 @@ run_ref_onboard() {
|
||||
--json
|
||||
}
|
||||
|
||||
inject_bad_plugin_fixture() {
|
||||
guest_exec bash -lc "$(cat <<'EOF'
|
||||
set -euo pipefail
|
||||
plugin_dir=/root/.openclaw/test-bad-plugin
|
||||
mkdir -p "$plugin_dir"
|
||||
cat >"$plugin_dir/package.json" <<'JSON'
|
||||
{
|
||||
"name": "@openclaw/test-bad-plugin",
|
||||
"version": "1.0.0",
|
||||
"openclaw": {
|
||||
"extensions": ["./index.cjs"],
|
||||
"setupEntry": "./setup-entry.cjs"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
cat >"$plugin_dir/openclaw.plugin.json" <<'JSON'
|
||||
{
|
||||
"id": "test-bad-plugin",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
},
|
||||
"channels": ["test-bad-plugin"]
|
||||
}
|
||||
JSON
|
||||
cat >"$plugin_dir/index.cjs" <<'JS'
|
||||
module.exports = { id: "test-bad-plugin", register() {} };
|
||||
JS
|
||||
cat >"$plugin_dir/setup-entry.cjs" <<'JS'
|
||||
module.exports = {
|
||||
kind: "bundled-channel-setup-entry",
|
||||
loadSetupPlugin() {
|
||||
throw new Error("boom: bad plugin smoke fixture");
|
||||
},
|
||||
};
|
||||
JS
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path("/root/.openclaw/openclaw.json")
|
||||
config = {}
|
||||
if config_path.exists():
|
||||
config = json.loads(config_path.read_text())
|
||||
|
||||
plugins = config.setdefault("plugins", {})
|
||||
load = plugins.setdefault("load", {})
|
||||
paths = load.setdefault("paths", [])
|
||||
plugin_dir = "/root/.openclaw/test-bad-plugin"
|
||||
if plugin_dir not in paths:
|
||||
paths.append(plugin_dir)
|
||||
|
||||
allow = plugins.get("allow")
|
||||
if isinstance(allow, list) and "test-bad-plugin" not in allow:
|
||||
allow.append("test-bad-plugin")
|
||||
|
||||
config_path.write_text(json.dumps(config, indent=2) + "\n")
|
||||
PY
|
||||
EOF
|
||||
)"
|
||||
}
|
||||
|
||||
verify_bad_plugin_diagnostic() {
|
||||
guest_exec grep -F "failed to load setup entry" /tmp/openclaw-parallels-linux-gateway.log
|
||||
}
|
||||
|
||||
start_gateway_background() {
|
||||
local cmd api_key_value_q
|
||||
api_key_value_q="$(shell_quote "$API_KEY_VALUE")"
|
||||
@@ -786,8 +853,10 @@ run_fresh_main_lane() {
|
||||
phase_run "fresh.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz"
|
||||
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
|
||||
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
phase_run "fresh.inject-bad-plugin" "$TIMEOUT_VERIFY_S" inject_bad_plugin_fixture
|
||||
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "fresh.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
|
||||
phase_run "fresh.bad-plugin-diagnostic" "$TIMEOUT_VERIFY_S" verify_bad_plugin_diagnostic
|
||||
phase_run "fresh.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
|
||||
FRESH_GATEWAY_STATUS="pass"
|
||||
phase_run "fresh.first-local-agent-turn" "$TIMEOUT_AGENT_S" verify_local_turn
|
||||
@@ -805,8 +874,10 @@ run_upgrade_lane() {
|
||||
phase_run "upgrade.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz"
|
||||
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-main)")"
|
||||
phase_run "upgrade.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
phase_run "upgrade.inject-bad-plugin" "$TIMEOUT_VERIFY_S" inject_bad_plugin_fixture
|
||||
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "upgrade.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
|
||||
phase_run "upgrade.bad-plugin-diagnostic" "$TIMEOUT_VERIFY_S" verify_bad_plugin_diagnostic
|
||||
phase_run "upgrade.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
|
||||
UPGRADE_GATEWAY_STATUS="pass"
|
||||
phase_run "upgrade.first-local-agent-turn" "$TIMEOUT_AGENT_S" verify_local_turn
|
||||
|
||||
@@ -859,6 +859,60 @@ describe("setupChannels", () => {
|
||||
expect(multiselect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the channel picker usable when the active registry contains broken sibling diagnostics", async () => {
|
||||
const registry = createTestRegistry([
|
||||
{
|
||||
pluginId: "healthy-channel",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createChannelTestPluginBase({
|
||||
id: "external-chat",
|
||||
label: "Healthy Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
registry.diagnostics.push({
|
||||
level: "error",
|
||||
pluginId: "broken-channel",
|
||||
source: "/tmp/broken-channel/setup-entry.cjs",
|
||||
message: "failed to load setup entry: boom: setup plugin missing",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const note = vi.fn(async (_message?: string, _title?: string) => {});
|
||||
const { multiselect, text } = createUnexpectedPromptGuards();
|
||||
const select = vi.fn(async ({ message, options }: { message: string; options: unknown[] }) => {
|
||||
if (message === "Select a channel") {
|
||||
const entries = options as Array<{ value: string; label?: string }>;
|
||||
expect(entries.find((entry) => entry.value === "external-chat")?.label).toBe(
|
||||
"Healthy Chat",
|
||||
);
|
||||
expect(entries.some((entry) => entry.value === "broken-channel")).toBe(false);
|
||||
return "__done__";
|
||||
}
|
||||
return "__done__";
|
||||
});
|
||||
|
||||
const prompter = createPrompter({
|
||||
note,
|
||||
select: select as unknown as WizardPrompter["select"],
|
||||
multiselect,
|
||||
text,
|
||||
});
|
||||
|
||||
await runSetupChannels({} as OpenClawConfig, prompter);
|
||||
|
||||
expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: "Select a channel" }));
|
||||
expect(
|
||||
note.mock.calls.some((call) =>
|
||||
(call[0] ?? "").includes("broken-channel plugin not available"),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(multiselect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps configured external plugin channels visible when the active registry starts empty", async () => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([createMSTeamsCatalogEntry()]);
|
||||
|
||||
@@ -3437,6 +3437,105 @@ module.exports = {
|
||||
expect(diagnostic!.message).toContain("failed to load setup entry");
|
||||
});
|
||||
|
||||
it("keeps healthy sibling channel plugins loadable when a setup entry throws", () => {
|
||||
useNoBundledPlugins();
|
||||
const brokenDir = makeTempDir();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(brokenDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/setup-entry-throws-sibling-test",
|
||||
openclaw: {
|
||||
extensions: ["./index.cjs"],
|
||||
setupEntry: "./setup-entry.cjs",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(brokenDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "setup-entry-throws-sibling-test",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["broken-chat"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(brokenDir, "index.cjs"),
|
||||
`module.exports = { id: "setup-entry-throws-sibling-test", register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(brokenDir, "setup-entry.cjs"),
|
||||
`module.exports = {
|
||||
kind: "bundled-channel-setup-entry",
|
||||
loadSetupPlugin: () => { throw new Error("boom: setup plugin missing"); },
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const healthy = writePlugin({
|
||||
id: "healthy-channel",
|
||||
filename: "healthy-channel.cjs",
|
||||
body: `module.exports = { id: "healthy-channel", register(api) {
|
||||
api.registerChannel({
|
||||
plugin: {
|
||||
id: "healthy-chat",
|
||||
meta: {
|
||||
id: "healthy-chat",
|
||||
label: "Healthy Chat",
|
||||
selectionLabel: "Healthy Chat",
|
||||
docsPath: "/channels/healthy-chat",
|
||||
blurb: "healthy sibling channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
}
|
||||
});
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
load: { paths: [brokenDir, healthy.file] },
|
||||
allow: ["setup-entry-throws-sibling-test", "healthy-channel"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
registry.channels.find((entry) => entry.plugin.id === "healthy-chat")?.plugin.meta,
|
||||
).toMatchObject({
|
||||
label: "Healthy Chat",
|
||||
docsPath: "/channels/healthy-chat",
|
||||
});
|
||||
expect(registry.plugins.find((entry) => entry.id === "healthy-channel")?.status).toBe("loaded");
|
||||
expect(
|
||||
registry.diagnostics.some(
|
||||
(diag) =>
|
||||
diag.pluginId === "setup-entry-throws-sibling-test" &&
|
||||
diag.level === "error" &&
|
||||
diag.message.includes("failed to load setup entry"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("prefers setupEntry for configured channel loads during startup when opted in", () => {
|
||||
expect(
|
||||
__testing.shouldLoadChannelPluginInSetupRuntime({
|
||||
|
||||
Reference in New Issue
Block a user