|
| 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"); |
0 commit comments