Skip to content

Commit 0acc3e3

Browse files
committed
test(e2e): select installable bundled plugins
1 parent 43252c8 commit 0acc3e3

4 files changed

Lines changed: 284 additions & 51 deletions

File tree

scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,93 @@
1+
import { spawnSync } from "node:child_process";
12
import fs from "node:fs";
23
import path from "node:path";
34

45
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
56

6-
function loadManifestEntries() {
7-
const explicit = (process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "")
8-
.split(/[,\s]+/u)
9-
.map((entry) => entry.trim())
10-
.filter(Boolean);
11-
const extensionRoot = path.join(process.cwd(), "dist", "extensions");
12-
const manifestEntries = fs
13-
.readdirSync(extensionRoot, { withFileTypes: true })
14-
.filter((entry) => entry.isDirectory())
15-
.map((entry) => {
16-
const manifestPath = path.join(extensionRoot, entry.name, "openclaw.plugin.json");
17-
if (!fs.existsSync(manifestPath)) {
7+
function resolveOpenClawEntry() {
8+
if (process.env.OPENCLAW_ENTRY) {
9+
return process.env.OPENCLAW_ENTRY;
10+
}
11+
for (const entry of ["dist/index.mjs", "dist/index.js"]) {
12+
if (fs.existsSync(entry)) {
13+
return entry;
14+
}
15+
}
16+
throw new Error("Missing OPENCLAW_ENTRY and dist/index.(m)js");
17+
}
18+
19+
function readPluginsList() {
20+
const entry = resolveOpenClawEntry();
21+
const result = spawnSync(process.execPath, [entry, "plugins", "list", "--json"], {
22+
cwd: process.cwd(),
23+
encoding: "utf8",
24+
env: process.env,
25+
});
26+
if (result.status !== 0) {
27+
throw new Error(
28+
`Unable to list packaged bundled plugins: ${result.stderr || result.stdout || `exit ${result.status}`}`,
29+
);
30+
}
31+
const payload = JSON.parse(result.stdout);
32+
return Array.isArray(payload.plugins) ? payload.plugins : [];
33+
}
34+
35+
function pluginRequiresConfig(pluginDir) {
36+
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
37+
if (!fs.existsSync(manifestPath)) {
38+
throw new Error(`missing bundled plugin manifest: ${manifestPath}`);
39+
}
40+
const manifest = readJson(manifestPath);
41+
const required = manifest.configSchema?.required;
42+
return Array.isArray(required) && required.some((value) => typeof value === "string");
43+
}
44+
45+
async function loadPackagedBundledEntries() {
46+
return readPluginsList()
47+
.filter((plugin) => plugin?.origin === "bundled")
48+
.map((plugin) => {
49+
const id = typeof plugin.id === "string" ? plugin.id.trim() : "";
50+
const rootDir = typeof plugin.rootDir === "string" ? plugin.rootDir.trim() : "";
51+
const source = typeof plugin.source === "string" ? plugin.source.trim() : "";
52+
const pluginDir = rootDir || (source ? path.dirname(source) : "");
53+
if (!id || !pluginDir) {
1854
return null;
1955
}
20-
const manifest = readJson(manifestPath);
21-
const id = typeof manifest.id === "string" ? manifest.id.trim() : "";
22-
if (!id) {
23-
throw new Error(`Bundled plugin manifest is missing id: ${manifestPath}`);
24-
}
25-
const required = manifest.configSchema?.required;
2656
return {
2757
id,
28-
dir: entry.name,
29-
requiresConfig:
30-
Array.isArray(required) && required.some((value) => typeof value === "string"),
58+
dir: path.basename(pluginDir),
59+
rootDir: pluginDir,
60+
requiresConfig: pluginRequiresConfig(pluginDir),
3161
};
3262
})
3363
.filter(Boolean)
3464
.toSorted((a, b) => a.id.localeCompare(b.id));
65+
}
66+
67+
async function loadManifestEntries() {
68+
const explicit = (process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS || "")
69+
.split(/[,\s]+/u)
70+
.map((entry) => entry.trim())
71+
.filter(Boolean);
72+
const manifestEntries = await loadPackagedBundledEntries();
3573

3674
if (explicit.length === 0) {
3775
return manifestEntries;
3876
}
39-
return explicit.map(
40-
(lookup) =>
41-
manifestEntries.find((entry) => entry.id === lookup || entry.dir === lookup) || {
42-
id: lookup,
43-
dir: lookup,
44-
requiresConfig: false,
45-
},
46-
);
77+
const available = manifestEntries.map((entry) => entry.id).join(", ");
78+
return explicit.map((lookup) => {
79+
const found = manifestEntries.find((entry) => entry.id === lookup || entry.dir === lookup);
80+
if (!found) {
81+
throw new Error(
82+
`OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS entry is not an installable bundled plugin in this package: ${lookup}. Available: ${available}`,
83+
);
84+
}
85+
return found;
86+
});
4787
}
4888

49-
function selectedManifestEntries() {
50-
const allEntries = loadManifestEntries();
89+
async function selectedManifestEntries() {
90+
const allEntries = await loadManifestEntries();
5191
const total = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL || "1", 10);
5292
const index = Number.parseInt(process.env.OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX || "0", 10);
5393
if (!Number.isInteger(total) || total < 1) {
@@ -85,15 +125,23 @@ function assertInstalled(pluginId, pluginDir, requiresConfig) {
85125
}
86126
if (
87127
typeof record.sourcePath !== "string" ||
88-
!record.sourcePath.includes(`/dist/extensions/${pluginDir}`)
128+
![`/dist/extensions/${pluginDir}`, `/dist-runtime/extensions/${pluginDir}`].some((fragment) =>
129+
record.sourcePath.includes(fragment),
130+
)
89131
) {
90132
throw new Error(`unexpected bundled source path for ${pluginId}: ${record.sourcePath}`);
91133
}
92134
if (record.installPath !== record.sourcePath) {
93135
throw new Error(`bundled install path should equal source path for ${pluginId}`);
94136
}
95137
const paths = config.plugins?.load?.paths || [];
96-
if (paths.some((entry) => String(entry).includes(`/dist/extensions/${pluginDir}`))) {
138+
if (
139+
paths.some((entry) =>
140+
[`/dist/extensions/${pluginDir}`, `/dist-runtime/extensions/${pluginDir}`].some(
141+
(fragment) => String(entry).includes(fragment),
142+
),
143+
)
144+
) {
97145
throw new Error(`config load paths should not include bundled install path for ${pluginId}`);
98146
}
99147
if (requiresConfig && config.plugins?.entries?.[pluginId]?.enabled === true) {
@@ -123,7 +171,13 @@ function assertUninstalled(pluginId, pluginDir) {
123171
throw new Error(`install record still present after uninstall for ${pluginId}`);
124172
}
125173
const paths = config.plugins?.load?.paths || [];
126-
if (paths.some((entry) => String(entry).includes(`/dist/extensions/${pluginDir}`))) {
174+
if (
175+
paths.some((entry) =>
176+
[`/dist/extensions/${pluginDir}`, `/dist-runtime/extensions/${pluginDir}`].some(
177+
(fragment) => String(entry).includes(fragment),
178+
),
179+
)
180+
) {
127181
throw new Error(`load path still present after uninstall for ${pluginId}`);
128182
}
129183
if (config.plugins?.entries?.[pluginId]) {
@@ -145,8 +199,8 @@ function assertUninstalled(pluginId, pluginDir) {
145199

146200
const [command, pluginId, pluginDir, requiresConfig] = process.argv.slice(2);
147201
if (command === "select") {
148-
for (const entry of selectedManifestEntries()) {
149-
console.log(`${entry.id}\t${entry.dir}\t${entry.requiresConfig ? "1" : "0"}`);
202+
for (const entry of await selectedManifestEntries()) {
203+
console.log(`${entry.id}\t${entry.dir}\t${entry.requiresConfig ? "1" : "0"}\t${entry.rootDir}`);
150204
}
151205
} else if (command === "assert-installed") {
152206
assertInstalled(pluginId, pluginDir, requiresConfig === "1");

scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@ function writeJson(file, value) {
3131
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
3232
}
3333

34-
function manifestPath(pluginDir) {
35-
return path.join(process.cwd(), "dist", "extensions", pluginDir, "openclaw.plugin.json");
34+
function manifestPath(pluginDir, pluginRoot) {
35+
const candidates = [
36+
...(isNonEmptyString(pluginRoot)
37+
? [path.join(pluginRoot, "openclaw.plugin.json")]
38+
: []),
39+
path.join(process.cwd(), "dist", "extensions", pluginDir, "openclaw.plugin.json"),
40+
path.join(process.cwd(), "dist-runtime", "extensions", pluginDir, "openclaw.plugin.json"),
41+
];
42+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
3643
}
3744

38-
function loadManifest(pluginDir) {
39-
const file = manifestPath(pluginDir);
45+
function loadManifest(pluginDir, pluginRoot) {
46+
const file = manifestPath(pluginDir, pluginRoot);
4047
if (!fs.existsSync(file)) {
4148
throw new Error(`missing bundled plugin manifest: ${file}`);
4249
}
@@ -373,7 +380,7 @@ function unwrapRpcPayload(raw) {
373380
return raw?.result ?? raw?.payload ?? raw?.data ?? raw;
374381
}
375382

376-
async function smokePlugin(pluginId, pluginDir, requiresConfig, pluginIndex) {
383+
async function smokePlugin(pluginId, pluginDir, requiresConfig, pluginIndex, pluginRoot) {
377384
if (requiresConfig) {
378385
console.log(`Runtime smoke skipped for ${pluginId}: plugin requires config`);
379386
return;
@@ -382,7 +389,7 @@ async function smokePlugin(pluginId, pluginDir, requiresConfig, pluginIndex) {
382389
if (!entrypoint) {
383390
throw new Error("missing OPENCLAW_ENTRY");
384391
}
385-
const manifest = loadManifest(pluginDir);
392+
const manifest = loadManifest(pluginDir, pluginRoot);
386393
const plan = buildPluginPlan(manifest);
387394
const port =
388395
readPositiveInt(process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_PORT_BASE, 19000) + pluginIndex * 3;
@@ -600,12 +607,12 @@ async function assertNoPackageManagerChildren(pid) {
600607
}
601608
}
602609

603-
async function smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex) {
610+
async function smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex, pluginRoot) {
604611
const entrypoint = process.env.OPENCLAW_ENTRY;
605612
if (!entrypoint) {
606613
throw new Error("missing OPENCLAW_ENTRY");
607614
}
608-
const manifest = loadManifest(pluginDir);
615+
const manifest = loadManifest(pluginDir, pluginRoot);
609616
const plan = buildPluginPlan(manifest);
610617
const selectedProvider = provider || plan.speechProviders[0];
611618
if (!selectedProvider) {
@@ -745,14 +752,14 @@ function tailText(text) {
745752
return text.split(/\r?\n/u).slice(-120).join("\n");
746753
}
747754

748-
const [command, pluginId, pluginDir, requiresConfigRaw, pluginIndexRaw, provider] =
755+
const [command, pluginId, pluginDir, requiresConfigRaw, pluginIndexRaw, pluginRoot, provider] =
749756
process.argv.slice(2);
750757
const pluginIndex = Number.parseInt(pluginIndexRaw || "0", 10);
751758

752759
if (command === "plugin") {
753-
await smokePlugin(pluginId, pluginDir, requiresConfigRaw === "1", pluginIndex);
760+
await smokePlugin(pluginId, pluginDir, requiresConfigRaw === "1", pluginIndex, pluginRoot);
754761
} else if (command === "tts-global-disable") {
755-
await smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex);
762+
await smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex, pluginRoot);
756763
} else if (command === "tts-openai-live") {
757764
await smokeOpenAiTts(pluginIndex);
758765
} else {

scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ node "$probe" select > /tmp/bundled-plugin-sweep-ids
2323
mapfile -t plugin_entries < /tmp/bundled-plugin-sweep-ids
2424
selected_labels=()
2525
for plugin_entry in "${plugin_entries[@]}"; do
26-
IFS=$'\t' read -r plugin_id plugin_dir _requires_config <<<"$plugin_entry"
26+
IFS=$'\t' read -r plugin_id plugin_dir _requires_config _plugin_root <<<"$plugin_entry"
2727
selected_labels+=("${plugin_id}@${plugin_dir}")
2828
done
2929
echo "Selected ${#plugin_entries[@]} bundled plugins for shard ${OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX:-0}/${OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL:-1}: ${selected_labels[*]}"
3030

3131
plugin_index=0
3232
for plugin_entry in "${plugin_entries[@]}"; do
33-
IFS=$'\t' read -r plugin_id plugin_dir requires_config <<<"$plugin_entry"
33+
IFS=$'\t' read -r plugin_id plugin_dir requires_config plugin_root <<<"$plugin_entry"
3434
install_log="/tmp/openclaw-install-${plugin_index}.log"
3535
uninstall_log="/tmp/openclaw-uninstall-${plugin_index}.log"
3636
plugin_started_at="$(date +%s)"
@@ -43,8 +43,8 @@ for plugin_entry in "${plugin_entries[@]}"; do
4343
node "$probe" assert-installed "$plugin_id" "$plugin_dir" "$requires_config"
4444
if [[ "${OPENCLAW_BUNDLED_PLUGIN_RUNTIME_SMOKE:-1}" != "0" ]]; then
4545
echo "Running bundled plugin runtime smoke: $plugin_id ($plugin_dir)"
46-
node "$runtime_smoke" plugin "$plugin_id" "$plugin_dir" "$requires_config" "$plugin_index"
47-
node "$runtime_smoke" tts-global-disable "$plugin_id" "$plugin_dir" "$requires_config" "$plugin_index" ""
46+
node "$runtime_smoke" plugin "$plugin_id" "$plugin_dir" "$requires_config" "$plugin_index" "$plugin_root"
47+
node "$runtime_smoke" tts-global-disable "$plugin_id" "$plugin_dir" "$requires_config" "$plugin_index" "$plugin_root" ""
4848
if [[ "$plugin_id" == "${OPENCLAW_BUNDLED_PLUGIN_TTS_LIVE_PROVIDER:-openai}" ]]; then
4949
node "$runtime_smoke" tts-openai-live "$plugin_id" "$plugin_dir" "$requires_config" "$plugin_index"
5050
fi

0 commit comments

Comments
 (0)