plugin-inspector/test/runtime-capture-report.test.js
2026-05-03 00:11:24 -07:00

568 lines
20 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import {
buildRuntimeCaptureReport,
captureEntrypoint,
inspectCompatibilityFixtureSet,
loadPluginRootConfig,
writeRuntimeCaptureReport,
} from "../src/advanced.js";
test("runtime capture report imports plugin entrypoints with mocked SDK", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-weather",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk";',
"",
"export default definePluginEntry((api) => {",
" api.on('before_tool_call', () => undefined);",
" api.registerTool({ name: 'weather', inputSchema: { type: 'object' }, run() {} });",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.targetCount, 1);
assert.equal(captureReport.summary.capturedCount, 1);
assert.equal(captureReport.summary.registrationCount, 1);
assert.equal(captureReport.summary.hookCount, 1);
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-out-"));
await writeRuntimeCaptureReport(captureReport, {
jsonPath: path.join(outDir, "capture.json"),
markdownPath: path.join(outDir, "capture.md"),
});
assert.equal(JSON.parse(await readFile(path.join(outDir, "capture.json"), "utf8")).summary.capturedCount, 1);
assert.match(await readFile(path.join(outDir, "capture.md"), "utf8"), /registerTool/);
});
test("runtime capture records conversation binding resolved callbacks", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-binding-resolved-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-binding-resolved",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk";',
"",
"export default definePluginEntry((api) => {",
" api.onConversationBindingResolved(() => undefined);",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 0);
assert.equal(captureReport.summary.capturedCount, 1);
assert.equal(captureReport.summary.hookCount, 1);
assert.deepEqual(
captureReport.results[0].captured.map((entry) => `${entry.kind}:${entry.name}`),
["hook:onConversationBindingResolved"],
);
});
test("runtime capture report synthesizes newly imported mocked SDK exports", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-dynamic-export-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-dynamic-sdk-export",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { definitelyMissing } from "openclaw/plugin-sdk/plugin-entry";',
"",
"export function register(api) {",
" if (!definitelyMissing) throw new Error('expected dynamic mock export');",
" api.registerTool({ name: 'fixture_tool', inputSchema: { type: 'object' }, run() {} });",
"}",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 0);
assert.equal(captureReport.results[0].status, "captured");
assert.deepEqual(captureReport.results[0].captured.map((entry) => `${entry.kind}:${entry.name}`), [
"registration:registerTool",
]);
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-dynamic-export-out-"));
await writeRuntimeCaptureReport(captureReport, {
jsonPath: path.join(outDir, "capture.json"),
markdownPath: path.join(outDir, "capture.md"),
});
assert.match(await readFile(path.join(outDir, "capture.md"), "utf8"), /registerTool/);
});
test("runtime capture report classifies registration execution failures", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-registration-error-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-registration-error",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk";',
"",
"export default definePluginEntry(() => {",
" throw new Error('register exploded');",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 1);
assert.equal(captureReport.results[0].failureClass, "registration-execution-error");
assert.match(captureReport.results[0].error, /register exploded/);
});
test("runtime capture supports TypeScript entrypoints, SDK subpaths, external mocks, and noisy output", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-ts-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-typescript-weather",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.ts"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "helper.ts"),
["export type WeatherConfig = { city: string };", "export const toolName: string = 'weather_ts';"].join("\n"),
"utf8",
);
await writeFile(
path.join(rootDir, "src", "sdk-barrel.ts"),
'export { ReexportedConfigSchema, reexportedSdkHelper } from "openclaw/plugin-sdk/reexported";\n',
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.ts"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";',
'import { buildChannelConfigSchema } from "openclaw/plugin-sdk/core";',
'import { z } from "openclaw/plugin-sdk/zod";',
'import defaultZod, { type ZodType } from "zod";',
'import { externalHelper } from "missing-runtime-dependency";',
'import { MemoryStore } from "missing-class-dependency";',
'import { toolName } from "./helper";',
'import { ReexportedConfigSchema, reexportedSdkHelper } from "./sdk-barrel.js";',
"",
"class FileStore extends MemoryStore {}",
"new FileStore();",
"const typedSchema: ZodType | undefined = undefined;",
"const defaultConfig = z",
" .object({ enabled: z.boolean().default(false), reexported: ReexportedConfigSchema })",
" .parse({});",
"defaultZod.object({ city: defaultZod.string().default('sf') }).parse({});",
"structuredClone(defaultConfig);",
"buildChannelConfigSchema(z.object({})).parse?.({});",
"console.log('plugin startup noise', Boolean(typedSchema));",
"setTimeout(() => console.log('late plugin noise'), 0);",
"externalHelper?.();",
"reexportedSdkHelper?.();",
"export default definePluginEntry((api) => {",
" api.runtime.state.resolveStateDir();",
" api.on('before_tool_call', () => undefined);",
" api.registerTool({ name: toolName, inputSchema: z.object({}), run() {} });",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 0);
assert.equal(captureReport.summary.capturedCount, 1);
assert.equal(captureReport.summary.registrationCount, 1);
assert.equal(captureReport.summary.hookCount, 1);
assert.match(captureReport.results[0].processOutput.stdout, /plugin startup noise/);
assert.match(captureReport.results[0].processOutput.stdout, /late plugin noise/);
});
test("runtime capture synthesizes manifest config before plugin registration", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-config-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-configured-memory",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.ts"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "openclaw.plugin.json"),
`${JSON.stringify(
{
id: "configured-memory",
configSchema: {
type: "object",
properties: {
embedding: {
type: "object",
minProperties: 1,
properties: {
provider: { type: "string" },
model: { type: "string" },
},
},
autoCapture: { type: "boolean" },
autoRecall: { type: "boolean" },
},
required: ["embedding"],
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.ts"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";',
"export default definePluginEntry({",
" register(api) {",
" if (!api.pluginConfig?.embedding) {",
" api.registerService({ id: 'configured-memory-disabled', start() {} });",
" return;",
" }",
" api.on('agent_end', () => undefined);",
" },",
"});",
].join("\n"),
"utf8",
);
const result = await captureEntrypoint("src/index.ts", {
cwd: rootDir,
pluginRoot: rootDir,
mockSdk: true,
});
assert.equal(result.status, "captured");
assert.deepEqual(
result.captured.map((item) => `${item.kind}:${item.name}`),
["hook:agent_end"],
);
});
test("runtime capture supports namespace imports from mocked externals", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-zod-namespace-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-zod-namespace",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk";',
'import * as z from "zod";',
"const schema = z.object({ city: z.string().default('sf') });",
"export default definePluginEntry((api) => {",
" api.registerTool({ name: schema.parse({}).city, inputSchema: {}, run() {} });",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 0);
assert.equal(captureReport.summary.registrationCount, 1);
});
test("runtime capture mock SDK supports config schemas and provider catalogs", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-provider-catalog-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-provider-catalog",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { buildPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";',
'import { buildSingleProviderApiKeyCatalog, createProviderApiKeyAuthMethod, defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";',
"",
"const config = buildPluginConfigSchema({ providerId: { parse: (value) => value ?? 'fixture-provider' } }).parse({});",
"const auth = createProviderApiKeyAuthMethod({ id: 'fixture-key' });",
"const catalog = buildSingleProviderApiKeyCatalog({",
" id: config.providerId,",
" auth,",
" buildModels: async () => [{ id: 'fixture-model' }],",
"});",
"const listed = await catalog.run({ apiKey: 'redacted' });",
"",
"export default defineSingleProviderPluginEntry({",
" id: config.providerId,",
" provider: listed.provider,",
" register(api) {",
" api.registerProvider({ id: listed.provider.id, auth, catalog, modelCount: listed.models.length });",
" },",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 0);
assert.equal(captureReport.summary.registrationCount, 2);
assert.deepEqual(
captureReport.results[0].captured.map((entry) => `${entry.kind}:${entry.name}`),
["registration:registerProvider", "registration:registerProvider"],
);
});
test("runtime capture mock SDK supports channel entries, gateway responders, and action gates", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-channel-entry-"));
await mkdir(path.join(rootDir, "src"), { recursive: true });
await writeFile(
path.join(rootDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-channel-entry",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["src/index.mjs"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(
path.join(rootDir, "src", "index.mjs"),
[
'import { createActionGate } from "openclaw/plugin-sdk/channel-actions";',
'import { buildChannelOutboundSessionRoute, createChannelPluginBase, createChatChannelPlugin, defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";',
"",
"const gate = createActionGate({ gateway: true });",
"const route = buildChannelOutboundSessionRoute({ agentId: 'agent', channel: 'fixture-channel', to: 'sender', chatType: 'direct' });",
"const plugin = createChatChannelPlugin({",
" base: createChannelPluginBase({",
" id: 'fixture-channel',",
" setup: () => ({ accountId: 'default' }),",
" }),",
" outbound: {",
" base: { resolveOutboundSessionRoute: () => route },",
" attachedResults: {",
" channel: 'fixture-channel',",
" sendText: async ({ text }) => ({ messageId: text }),",
" },",
" },",
"});",
"",
"export default defineChannelPluginEntry({",
" id: 'fixture-channel-entry',",
" name: 'Fixture channel',",
" description: 'Fixture channel',",
" plugin,",
" registerFull(api) {",
" if (gate('gateway')) {",
" api.registerGatewayMethod('fixture.ping', ({ respond }) => respond(true, { ok: true }), { scope: 'operator.read' });",
" }",
" api.registerService({ id: 'fixture-service', start() {}, stop() {} });",
" },",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: rootDir });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
assert.equal(captureReport.summary.failedCount, 0);
assert.deepEqual(
captureReport.results[0].captured.map((entry) => `${entry.kind}:${entry.name}`),
["registration:registerChannel", "registration:registerGatewayMethod", "registration:registerService"],
);
assert.equal(captureReport.results[0].captured[0].arguments[0].keys.includes("plugin"), true);
});
test("runtime capture keeps dist chunk imports rooted at their original package", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-dist-"));
await mkdir(path.join(rootDir, "dist", "extensions", "weather"), { recursive: true });
await writeFile(path.join(rootDir, "dist", "package-shared.js"), "export const toolName = 'weather_dist';\n", "utf8");
const packageRoot = path.join(rootDir, "dist", "extensions", "weather");
await writeFile(
path.join(packageRoot, "package.json"),
`${JSON.stringify(
{
name: "openclaw-dist-weather",
version: "1.0.0",
type: "module",
openclaw: {
extensions: ["index.js"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeFile(path.join(packageRoot, "index.js"), 'export { default } from "./src/index.js";\n', "utf8");
await mkdir(path.join(packageRoot, "src"), { recursive: true });
await writeFile(
path.join(packageRoot, "src", "index.js"),
[
'import { definePluginEntry } from "openclaw/plugin-sdk";',
'import { toolName } from "../../../package-shared.js";',
"",
"export default definePluginEntry((api) => {",
" api.registerTool({ name: toolName, inputSchema: { type: 'object' }, run() {} });",
"});",
].join("\n"),
"utf8",
);
const config = await loadPluginRootConfig(null, { cwd: packageRoot });
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir: packageRoot });
assert.equal(captureReport.summary.failedCount, 0);
assert.equal(captureReport.summary.registrationCount, 1);
});