test(docker): cover slack bundled runtime deps

This commit is contained in:
Peter Steinberger
2026-04-22 03:36:06 +01:00
parent 25e2e64ce4
commit e8f18f95d5
4 changed files with 113 additions and 10 deletions

View File

@@ -60,6 +60,7 @@ command -v openclaw >/dev/null
package_root="$(npm root -g)/openclaw"
test -d "$package_root/dist/extensions/telegram"
test -d "$package_root/dist/extensions/discord"
test -d "$package_root/dist/extensions/slack"
if [ -d "$package_root/dist/extensions/telegram/node_modules" ]; then
echo "telegram runtime deps should not be preinstalled in package" >&2
@@ -71,6 +72,11 @@ if [ -d "$package_root/dist/extensions/discord/node_modules" ]; then
find "$package_root/dist/extensions/discord/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
if [ -d "$package_root/dist/extensions/slack/node_modules" ]; then
echo "slack runtime deps should not be preinstalled in package" >&2
find "$package_root/dist/extensions/slack/node_modules" -maxdepth 2 -type f | head -20 >&2 || true
exit 1
fi
write_config() {
local mode="$1"
@@ -138,6 +144,15 @@ if (mode === "discord") {
},
};
}
if (mode === "slack") {
config.channels = {
...(config.channels || {}),
slack: {
...(config.channels?.slack || {}),
enabled: true,
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
@@ -392,6 +407,12 @@ config.channels = {
dmPolicy: "disabled",
groupPolicy: "disabled",
},
slack: {
...(config.channels?.slack || {}),
enabled: mode === "slack",
botToken: "xoxb-bundled-channel-update-token",
appToken: "xapp-bundled-channel-update-token",
},
};
fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -542,7 +563,11 @@ assert_dep_available telegram grammy
echo "Mutating installed package: remove Telegram deps, then update-mode doctor repairs them..."
remove_runtime_dep telegram grammy
assert_no_dep_available telegram grammy
OPENCLAW_UPDATE_IN_PROGRESS=1 openclaw doctor --non-interactive >/tmp/openclaw-update-mode-doctor.log 2>&1
if ! OPENCLAW_UPDATE_IN_PROGRESS=1 openclaw doctor --non-interactive >/tmp/openclaw-update-mode-doctor.log 2>&1; then
echo "update-mode doctor failed while repairing Telegram deps" >&2
cat /tmp/openclaw-update-mode-doctor.log >&2
exit 1
fi
assert_dep_available telegram grammy
echo "Mutating config to Discord and rerunning same-version update path..."
@@ -554,6 +579,15 @@ cat /tmp/openclaw-update-discord.json
assert_update_ok /tmp/openclaw-update-discord.json "$candidate_version"
assert_dep_available discord discord-api-types
echo "Mutating config to Slack and rerunning same-version update path..."
write_config slack
remove_runtime_dep slack @slack/web-api
assert_no_dep_available slack @slack/web-api
run_update_and_capture slack /tmp/openclaw-update-slack.json
cat /tmp/openclaw-update-slack.json
assert_update_ok /tmp/openclaw-update-slack.json "$candidate_version"
assert_dep_available slack @slack/web-api
echo "bundled channel runtime deps Docker update E2E passed"
EOF
then
@@ -568,6 +602,7 @@ EOF
run_channel_scenario telegram grammy
run_channel_scenario discord discord-api-types
run_channel_scenario slack @slack/web-api
if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then
run_update_scenario
fi

View File

@@ -172,7 +172,11 @@ describe("doctor bundled plugin runtime deps", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
writeJson(path.join(root, "package.json"), { name: "openclaw" });
writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" });
const installed: Array<{ installRoot: string; missingSpecs: string[] }> = [];
const installed: Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}> = [];
const prompter = {
shouldRepair: false,
shouldForce: false,
@@ -207,6 +211,64 @@ describe("doctor bundled plugin runtime deps", () => {
{
installRoot: root,
missingSpecs: ["grammy@1.37.0"],
installSpecs: ["grammy@1.37.0"],
},
]);
});
it("retains configured bundled deps when repairing a subset", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
writeJson(path.join(root, "package.json"), { name: "openclaw" });
writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" });
writeBundledChannelPlugin(root, "slack", { "@slack/web-api": "7.15.1" });
writeJson(path.join(root, "node_modules", "@slack", "web-api", "package.json"), {
name: "@slack/web-api",
version: "7.15.1",
});
const installed: Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}> = [];
const prompter = {
shouldRepair: false,
shouldForce: false,
repairMode: {
shouldRepair: false,
shouldForce: false,
nonInteractive: true,
canPrompt: false,
updateInProgress: false,
},
confirm: async () => false,
confirmAutoFix: async () => false,
confirmAggressiveAutoFix: async () => false,
confirmRuntimeRepair: async () => false,
select: async (_params: unknown, fallback: unknown) => fallback,
} as DoctorPrompter;
await maybeRepairBundledPluginRuntimeDeps({
runtime: { error: () => {} } as never,
prompter,
packageRoot: root,
includeConfiguredChannels: true,
config: {
plugins: { enabled: true },
channels: {
telegram: { enabled: true },
slack: { enabled: false, botToken: "xoxb-test", appToken: "xapp-test" },
},
},
installDeps: (params) => {
installed.push(params);
},
});
expect(installed).toEqual([
{
installRoot: root,
missingSpecs: ["grammy@1.37.0"],
installSpecs: ["@slack/web-api@7.15.1", "grammy@1.37.0"],
},
]);
});

View File

@@ -16,7 +16,11 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
env?: NodeJS.ProcessEnv;
packageRoot?: string | null;
includeConfiguredChannels?: boolean;
installDeps?: (params: { installRoot: string; missingSpecs: string[] }) => void;
installDeps?: (params: {
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}) => void;
}): Promise<void> {
const packageRoot =
params.packageRoot ??
@@ -29,7 +33,7 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
return;
}
const { missing, conflicts } = scanBundledPluginRuntimeDeps({
const { deps, missing, conflicts } = scanBundledPluginRuntimeDeps({
packageRoot,
config: params.config,
includeConfiguredChannels: params.includeConfiguredChannels,
@@ -58,6 +62,7 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
}
const missingSpecs = missing.map((dep) => `${dep.name}@${dep.version}`);
const installSpecs = deps.map((dep) => `${dep.name}@${dep.version}`);
note(
[
"Bundled plugin runtime deps are missing.",
@@ -84,11 +89,11 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
((installParams) =>
installBundledRuntimeDeps({
installRoot: installParams.installRoot,
missingSpecs: installParams.missingSpecs,
missingSpecs: installParams.installSpecs,
env: params.env ?? process.env,
}));
install({ installRoot: packageRoot, missingSpecs });
note(`Installed bundled plugin deps: ${missingSpecs.join(", ")}`, "Bundled plugins");
install({ installRoot: packageRoot, missingSpecs, installSpecs });
note(`Installed bundled plugin deps: ${installSpecs.join(", ")}`, "Bundled plugins");
} catch (error) {
params.runtime.error(`Failed to install bundled plugin runtime deps: ${String(error)}`);
}

View File

@@ -484,15 +484,16 @@ export function scanBundledPluginRuntimeDeps(params: {
pluginIds?: readonly string[];
includeConfiguredChannels?: boolean;
}): {
deps: RuntimeDepEntry[];
missing: RuntimeDepEntry[];
conflicts: RuntimeDepConflict[];
} {
if (isSourceCheckoutRoot(params.packageRoot)) {
return { missing: [], conflicts: [] };
return { deps: [], missing: [], conflicts: [] };
}
const extensionsDir = path.join(params.packageRoot, "dist", "extensions");
if (!fs.existsSync(extensionsDir)) {
return { missing: [], conflicts: [] };
return { deps: [], missing: [], conflicts: [] };
}
const { deps, conflicts } = collectBundledPluginRuntimeDeps({
extensionsDir,
@@ -509,7 +510,7 @@ export function scanBundledPluginRuntimeDeps(params: {
!fs.existsSync(path.join(extensionsDir, pluginId, dependencySentinelPath(dep.name))),
),
);
return { missing, conflicts };
return { deps, missing, conflicts };
}
export function resolveBundledRuntimeDependencyInstallRoot(pluginRoot: string): string {