Skip to content

Commit 5a2a4ab

Browse files
authored
CI: add built plugin singleton smoke (openclaw#48710)
1 parent 3d3f292 commit 5a2a4ab

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ jobs:
330330
- name: Smoke test CLI launcher status json
331331
run: node openclaw.mjs status --json --timeout 1
332332

333+
- name: Smoke test built bundled plugin singleton
334+
run: pnpm test:build:singleton
335+
333336
- name: Check CLI startup memory
334337
run: pnpm test:startup:memory
335338

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@
564564
"test": "node scripts/test-parallel.mjs",
565565
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
566566
"test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",
567+
"test:build:singleton": "node scripts/test-built-plugin-singleton.mjs",
567568
"test:channels": "vitest run --config vitest.channels.config.ts",
568569
"test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins",
569570
"test:contracts:channels": "OPENCLAW_TEST_PROFILE=low pnpm test -- src/channels/plugins/contracts",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import os from "node:os";
4+
import path from "node:path";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
import { stageBundledPluginRuntime } from "./stage-bundled-plugin-runtime.mjs";
7+
8+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9+
const smokeEntryPath = path.join(repoRoot, "dist", "plugins", "build-smoke-entry.js");
10+
assert.ok(fs.existsSync(smokeEntryPath), `missing build output: ${smokeEntryPath}`);
11+
12+
const { clearPluginCommands, getPluginCommandSpecs, loadOpenClawPlugins, matchPluginCommand } =
13+
await import(pathToFileURL(smokeEntryPath).href);
14+
15+
assert.equal(typeof loadOpenClawPlugins, "function", "built loader export missing");
16+
assert.equal(typeof clearPluginCommands, "function", "clearPluginCommands missing");
17+
assert.equal(typeof getPluginCommandSpecs, "function", "getPluginCommandSpecs missing");
18+
assert.equal(typeof matchPluginCommand, "function", "matchPluginCommand missing");
19+
20+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-build-smoke-"));
21+
22+
function cleanup() {
23+
clearPluginCommands();
24+
fs.rmSync(tempRoot, { recursive: true, force: true });
25+
}
26+
27+
process.on("exit", cleanup);
28+
process.on("SIGINT", () => {
29+
cleanup();
30+
process.exit(130);
31+
});
32+
process.on("SIGTERM", () => {
33+
cleanup();
34+
process.exit(143);
35+
});
36+
37+
const pluginId = "build-smoke-plugin";
38+
const distPluginDir = path.join(tempRoot, "dist", "extensions", pluginId);
39+
fs.mkdirSync(distPluginDir, { recursive: true });
40+
fs.writeFileSync(path.join(tempRoot, "package.json"), '{ "type": "module" }\n', "utf8");
41+
fs.writeFileSync(
42+
path.join(distPluginDir, "package.json"),
43+
JSON.stringify(
44+
{
45+
name: "@openclaw/build-smoke-plugin",
46+
type: "module",
47+
openclaw: {
48+
extensions: ["./index.js"],
49+
},
50+
},
51+
null,
52+
2,
53+
),
54+
"utf8",
55+
);
56+
fs.writeFileSync(
57+
path.join(distPluginDir, "openclaw.plugin.json"),
58+
JSON.stringify(
59+
{
60+
id: pluginId,
61+
configSchema: {
62+
type: "object",
63+
additionalProperties: false,
64+
properties: {},
65+
},
66+
},
67+
null,
68+
2,
69+
),
70+
"utf8",
71+
);
72+
fs.writeFileSync(
73+
path.join(distPluginDir, "index.js"),
74+
[
75+
"import sdk from 'openclaw/plugin-sdk';",
76+
"const { emptyPluginConfigSchema } = sdk;",
77+
"",
78+
"export default {",
79+
` id: ${JSON.stringify(pluginId)},`,
80+
" configSchema: emptyPluginConfigSchema(),",
81+
" register(api) {",
82+
" api.registerCommand({",
83+
" name: 'pair',",
84+
" description: 'Pair a device',",
85+
" acceptsArgs: true,",
86+
" nativeNames: { telegram: 'pair', discord: 'pair' },",
87+
" async handler({ args }) {",
88+
" return { text: `paired:${args ?? ''}` };",
89+
" },",
90+
" });",
91+
" },",
92+
"};",
93+
"",
94+
].join("\n"),
95+
"utf8",
96+
);
97+
98+
stageBundledPluginRuntime({ repoRoot: tempRoot });
99+
100+
const runtimeEntryPath = path.join(tempRoot, "dist-runtime", "extensions", pluginId, "index.js");
101+
assert.ok(fs.existsSync(runtimeEntryPath), "runtime overlay entry missing");
102+
assert.equal(
103+
fs.existsSync(path.join(tempRoot, "dist-runtime", "plugins", "commands.js")),
104+
false,
105+
"dist-runtime must not stage a duplicate commands module",
106+
);
107+
108+
clearPluginCommands();
109+
110+
const registry = loadOpenClawPlugins({
111+
cache: false,
112+
workspaceDir: tempRoot,
113+
env: {
114+
...process.env,
115+
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(tempRoot, "dist-runtime", "extensions"),
116+
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
117+
},
118+
config: {
119+
plugins: {
120+
enabled: true,
121+
allow: [pluginId],
122+
entries: {
123+
[pluginId]: { enabled: true },
124+
},
125+
},
126+
},
127+
});
128+
129+
const record = registry.plugins.find((entry) => entry.id === pluginId);
130+
assert.ok(record, "smoke plugin missing from registry");
131+
assert.equal(record.status, "loaded", record.error ?? "smoke plugin failed to load");
132+
133+
assert.deepEqual(getPluginCommandSpecs("telegram"), [
134+
{ name: "pair", description: "Pair a device", acceptsArgs: true },
135+
]);
136+
137+
const match = matchPluginCommand("/pair now");
138+
assert.ok(match, "canonical built command registry did not receive the command");
139+
assert.equal(match.args, "now");
140+
const result = await match.command.handler({ args: match.args });
141+
assert.deepEqual(result, { text: "paired:now" });
142+
143+
process.stdout.write("[build-smoke] built plugin singleton smoke passed\n");

src/plugins/build-smoke-entry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
clearPluginCommands,
3+
executePluginCommand,
4+
getPluginCommandSpecs,
5+
matchPluginCommand,
6+
} from "./commands.js";
7+
export { loadOpenClawPlugins } from "./loader.js";

tsdown.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ function buildCoreDistEntries(): Record<string, string> {
171171
"line/accounts": "src/line/accounts.ts",
172172
"line/send": "src/line/send.ts",
173173
"line/template-messages": "src/line/template-messages.ts",
174+
"plugins/build-smoke-entry": "src/plugins/build-smoke-entry.ts",
174175
"plugins/runtime/index": "src/plugins/runtime/index.ts",
175176
"llm-slug-generator": "src/hooks/llm-slug-generator.ts",
176177
};

0 commit comments

Comments
 (0)