349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { test } from "node:test";
|
|
import { captureEntrypoint, inspectFixtureSet, inspectSourceText, loadInspectorConfig } from "../src/advanced.js";
|
|
|
|
test("source inspection records hook, registrar, and SDK import evidence", () => {
|
|
const inspection = inspectSourceText(
|
|
[
|
|
'import type { OpenClawPluginApi } from "openclaw/plugin-sdk";',
|
|
"",
|
|
"export function register(api) {",
|
|
' // api.on("llm_output", () => {});',
|
|
' api.on("before_tool_call", () => {});',
|
|
" /* api.registerHttpRoute({ path: '/ignored' }); */",
|
|
" api.registerService({ name: 'svc', start() {} });",
|
|
" return definePluginEntry({ register() {} });",
|
|
"}",
|
|
].join("\n"),
|
|
"plugins/example/index.ts",
|
|
);
|
|
|
|
assert.deepEqual(inspection.hooks, [
|
|
{
|
|
name: "before_tool_call",
|
|
file: "plugins/example/index.ts",
|
|
line: 5,
|
|
ref: "plugins/example/index.ts:5",
|
|
},
|
|
]);
|
|
assert.deepEqual(
|
|
inspection.registrations.map((registration) => `${registration.name}@${registration.ref}`),
|
|
["registerService@plugins/example/index.ts:7", "definePluginEntry@plugins/example/index.ts:8"],
|
|
);
|
|
assert.deepEqual(inspection.sdkImports, [
|
|
{
|
|
specifier: "openclaw/plugin-sdk",
|
|
file: "plugins/example/index.ts",
|
|
line: 1,
|
|
ref: "plugins/example/index.ts:1",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("source inspection strips long comments before matching registrations", () => {
|
|
const inspection = inspectSourceText(
|
|
[`/* ${"a/*".repeat(512)} */`, "api.registerTool({ name: 'weather' });"].join("\n"),
|
|
"plugins/example/index.ts",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
inspection.registrations.map((registration) => `${registration.name}@${registration.ref}`),
|
|
["registerTool@plugins/example/index.ts:2"],
|
|
);
|
|
});
|
|
|
|
test("fixture set inspection produces a passing report", async () => {
|
|
const config = await loadInspectorConfig("test/fixtures/inspector.config.json");
|
|
const report = await inspectFixtureSet(config);
|
|
|
|
assert.equal(report.status, "pass");
|
|
assert.equal(report.summary.fixtureCount, 1);
|
|
assert.deepEqual(report.fixtures[0].hooks, ["before_tool_call"]);
|
|
assert.deepEqual(report.fixtures[0].registrations, ["definePluginEntry", "registerTool"]);
|
|
assert.deepEqual(report.fixtures[0].manifestContracts, ["tools"]);
|
|
});
|
|
|
|
test("fixture set inspection reports missing expected seams", async () => {
|
|
const config = await loadInspectorConfig("test/fixtures/inspector.config.json");
|
|
config.fixtures[0].expect.hooks.push("llm_output");
|
|
|
|
const report = await inspectFixtureSet(config);
|
|
|
|
assert.equal(report.status, "fail");
|
|
assert.equal(report.breakages[0].code, "missing-expected-seam");
|
|
assert.match(report.breakages[0].message, /llm_output/);
|
|
});
|
|
|
|
test("fixture set inspection treats channel factories as channel registration coverage", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-channel-factory-"));
|
|
await mkdir(path.join(dir, "fixture"), { recursive: true });
|
|
await writeFile(
|
|
path.join(dir, "fixture", "index.js"),
|
|
[
|
|
'import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";',
|
|
'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";',
|
|
"",
|
|
"export const channel = createChatChannelPlugin({ id: 'fixture-channel' });",
|
|
"export default defineBundledChannelEntry({ id: 'bundled-channel' });",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const report = await inspectFixtureSet({
|
|
version: 1,
|
|
submoduleRoot: ".",
|
|
rootDir: dir,
|
|
fixtures: [
|
|
{
|
|
id: "fixture",
|
|
path: "fixture",
|
|
repo: "https://github.com/openclaw/fixture.git",
|
|
priority: "high",
|
|
seams: ["channel"],
|
|
expect: {
|
|
registrations: ["registerChannel"],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(report.status, "pass");
|
|
assert.deepEqual(report.breakages, []);
|
|
assert.deepEqual(report.fixtures[0].registrations, ["createChatChannelPlugin", "defineBundledChannelEntry"]);
|
|
});
|
|
|
|
test("capture entrypoint imports a local fixture and records registrations", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-capture-"));
|
|
const entrypoint = path.join(dir, "fixture.mjs");
|
|
await writeFile(
|
|
entrypoint,
|
|
[
|
|
"export default {",
|
|
" register(api) {",
|
|
" api.on('before_tool_call', () => undefined);",
|
|
" api.registerTool({ name: 'fixture_tool', inputSchema: { type: 'object' }, run() {} });",
|
|
" }",
|
|
"};",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = await captureEntrypoint(entrypoint, {
|
|
apiOptions: { knownRegistrars: ["registerTool"] },
|
|
});
|
|
|
|
assert.equal(result.status, "captured");
|
|
assert.deepEqual(
|
|
result.captured.map((item) => `${item.kind}:${item.name}`),
|
|
["hook:before_tool_call", "registration:registerTool"],
|
|
);
|
|
});
|
|
|
|
test("capture entrypoint can mock OpenClaw plugin SDK imports", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-capture-"));
|
|
await mkdir(path.join(dir, "src"), { recursive: true });
|
|
const entrypoint = path.join(dir, "src", "index.mjs");
|
|
await writeFile(
|
|
entrypoint,
|
|
[
|
|
'import { pluginSdkMock } from "openclaw/plugin-sdk";',
|
|
'import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";',
|
|
'import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";',
|
|
'import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";',
|
|
'import { GPT5_BEHAVIOR_CONTRACT } from "openclaw/plugin-sdk/provider-model-shared";',
|
|
'import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";',
|
|
'import { registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";',
|
|
"",
|
|
"const provider = defineSingleProviderPluginEntry({",
|
|
" id: 'fixture-provider',",
|
|
" name: 'Fixture provider',",
|
|
" description: 'Fixture provider',",
|
|
" provider: {",
|
|
" label: 'Fixture',",
|
|
" docsPath: '/docs/fixture',",
|
|
" catalog: { run: async () => ({ provider: { id: 'fixture-provider' } }) },",
|
|
" },",
|
|
"});",
|
|
"",
|
|
"createChatChannelPlugin({ register() {} });",
|
|
"buildSecretInputSchema();",
|
|
"registerPluginHttpRoute({ path: resolveWebhookPath('hook') });",
|
|
"",
|
|
"export default definePluginEntry((api) => {",
|
|
" if (!pluginSdkMock) throw new Error('expected mock SDK');",
|
|
" if (!GPT5_BEHAVIOR_CONTRACT) throw new Error('expected dynamic subpath mock export');",
|
|
" provider.register(api);",
|
|
" api.registerHttpRoute({ path: resolveWebhookPath('hook'), handler() {} });",
|
|
" api.registerTool({ name: 'fixture_tool', inputSchema: { type: 'object' }, run() {} });",
|
|
"});",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = await captureEntrypoint("src/index.mjs", {
|
|
cwd: dir,
|
|
pluginRoot: dir,
|
|
mockSdk: true,
|
|
});
|
|
|
|
assert.equal(result.status, "captured");
|
|
assert.equal(result.mockSdk, true);
|
|
assert.deepEqual(
|
|
result.captured.map((item) => `${item.kind}:${item.name}`),
|
|
["registration:registerProvider", "registration:registerHttpRoute", "registration:registerTool"],
|
|
);
|
|
});
|
|
|
|
test("mock capture accepts valid output when plugin code dirties process exit code", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-exit-code-"));
|
|
const entrypoint = path.join(dir, "index.mjs");
|
|
await writeFile(
|
|
entrypoint,
|
|
[
|
|
'import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";',
|
|
"process.exitCode = 1;",
|
|
"export default definePluginEntry({",
|
|
" register(api) {",
|
|
" api.registerProvider({ id: 'fixture-provider' });",
|
|
" },",
|
|
"});",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = await captureEntrypoint("index.mjs", {
|
|
cwd: dir,
|
|
pluginRoot: dir,
|
|
mockSdk: true,
|
|
});
|
|
|
|
assert.equal(result.status, "captured");
|
|
assert.deepEqual(result.captured.map((item) => `${item.kind}:${item.name}`), [
|
|
"registration:registerProvider",
|
|
]);
|
|
});
|
|
|
|
test("mock capture prefers discovered bare mocks over installed dependency exports", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-bare-capture-"));
|
|
await mkdir(path.join(dir, "node_modules/typebox"), { recursive: true });
|
|
await writeFile(
|
|
path.join(dir, "node_modules/typebox/package.json"),
|
|
JSON.stringify({ name: "typebox", version: "0.0.0", type: "module", exports: "./index.js" }, null, 2),
|
|
"utf8",
|
|
);
|
|
await writeFile(path.join(dir, "node_modules/typebox/index.js"), "export const Type = {};\n", "utf8");
|
|
const entrypoint = path.join(dir, "index.mjs");
|
|
await writeFile(
|
|
entrypoint,
|
|
[
|
|
"import path from 'node:path';",
|
|
'import { Static, Type } from "typebox";',
|
|
'import { resolvePreferredOpenClawTmpDir } from "fixture-api";',
|
|
"export function register(api) {",
|
|
" if (!Static || !Type) throw new Error('expected mocked typebox exports');",
|
|
" path.join(resolvePreferredOpenClawTmpDir(), 'fixture');",
|
|
" api.registerTool({ name: 'fixture_tool', inputSchema: Type.Object({}), run() {} });",
|
|
"}",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = await captureEntrypoint("index.mjs", {
|
|
cwd: dir,
|
|
pluginRoot: dir,
|
|
mockSdk: true,
|
|
});
|
|
|
|
assert.equal(result.status, "captured");
|
|
assert.deepEqual(result.captured.map((item) => `${item.kind}:${item.name}`), ["registration:registerTool"]);
|
|
});
|
|
|
|
test("mock capture expands bundled channel entry registration shells", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-bundled-channel-capture-"));
|
|
const entrypoint = path.join(dir, "index.mjs");
|
|
await writeFile(
|
|
entrypoint,
|
|
[
|
|
'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";',
|
|
"export default defineBundledChannelEntry({",
|
|
" id: 'fixture-channel',",
|
|
" name: 'Fixture Channel',",
|
|
" description: 'Fixture channel',",
|
|
" plugin: { specifier: './channel.js', exportName: 'fixtureChannel' },",
|
|
" registerFull(api) {",
|
|
" api.registerTool({ name: 'fixture_tool', inputSchema: { type: 'object' }, run() {} });",
|
|
" },",
|
|
"});",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = await captureEntrypoint("index.mjs", {
|
|
cwd: dir,
|
|
pluginRoot: dir,
|
|
mockSdk: true,
|
|
});
|
|
|
|
assert.equal(result.status, "captured");
|
|
assert.deepEqual(result.captured.map((item) => `${item.kind}:${item.name}`), [
|
|
"registration:registerChannel",
|
|
"registration:registerTool",
|
|
]);
|
|
});
|
|
|
|
test("mock capture follows bundled channel linked registerFull exports", async () => {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-bundled-channel-linked-capture-"));
|
|
await writeFile(
|
|
path.join(dir, "index.ts"),
|
|
[
|
|
'import { defineBundledChannelEntry, loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";',
|
|
"",
|
|
"function registerFull(api) {",
|
|
" const register = loadBundledEntryExportSync(import.meta.url, {",
|
|
" specifier: './api.js',",
|
|
" exportName: 'registerFixtureFull',",
|
|
" });",
|
|
" register(api);",
|
|
"}",
|
|
"",
|
|
"export default defineBundledChannelEntry({",
|
|
" id: 'fixture-channel',",
|
|
" name: 'Fixture Channel',",
|
|
" description: 'Fixture channel',",
|
|
" plugin: { specifier: './channel-plugin-api.js', exportName: 'fixtureChannel' },",
|
|
" registerFull,",
|
|
"});",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await writeFile(
|
|
path.join(dir, "api.ts"),
|
|
[
|
|
'import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";',
|
|
"",
|
|
"const schema = buildSecretInputSchema().optional();",
|
|
"",
|
|
"export function registerFixtureFull(api) {",
|
|
" schema.parse(undefined);",
|
|
" api.registerCommand({ name: 'fixture.command', run() {} });",
|
|
"}",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = await captureEntrypoint("index.ts", {
|
|
cwd: dir,
|
|
pluginRoot: dir,
|
|
mockSdk: true,
|
|
});
|
|
|
|
assert.equal(result.status, "captured");
|
|
assert.deepEqual(result.captured.map((item) => `${item.kind}:${item.name}`), [
|
|
"registration:registerChannel",
|
|
"registration:registerCommand",
|
|
]);
|
|
});
|