plugin-inspector/test/report.test.js
2026-04-28 18:32:50 -07:00

691 lines
24 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 {
buildSarifReport,
buildCompatibilityReport,
buildCompatibilityFixtureReport,
buildIssues,
classifyCompatibilityFixture,
classifyCompatRecordCoverage,
classifyPackageContracts,
classifyTargetOpenClawCoverage,
inspectFixtureSet,
loadInspectorConfig,
renderCompatibilityIssuesReport,
renderCompatibilityMarkdownReport,
renderJunitXml,
renderMarkdownReport,
renderMarkdownTable,
renderTextSummary,
writeArtifacts,
writeCompatibilityReport,
writeCiOutputArtifacts,
writeReport,
} from "../src/advanced.js";
test("markdown report includes summary and inventory", async () => {
const config = await loadInspectorConfig("test/fixtures/inspector.config.json");
const report = await inspectFixtureSet(config);
const markdown = renderMarkdownReport(report);
assert.match(markdown, /# OpenClaw Plugin Inspector Report/);
assert.match(markdown, /\| sample-plugin \| high \| native-tool \| before_tool_call \| definePluginEntry, registerTool \| tools \|/);
});
test("text summary includes artifact paths and top blocking findings", () => {
const summary = renderTextSummary(
{
status: "fail",
summary: {
fixtureCount: 1,
breakageCount: 1,
issueCount: 1,
logCount: 0,
},
breakages: [
{
fixture: "weather",
code: "missing-expected-seam",
level: "breakage",
message: "weather: missing expected registration registerTool",
evidence: ["src/index.js:12"],
},
],
warnings: [],
issues: [
{
fixture: "weather",
code: "sdk-export-missing",
severity: "P0",
status: "blocking",
title: "SDK export is missing",
evidence: ["src/index.js:1"],
},
],
},
{
artifacts: {
jsonPath: "reports/plugin-inspector-report.json",
markdownPath: "reports/plugin-inspector-report.md",
},
},
);
assert.match(summary, /Status: FAIL/);
assert.match(summary, /Issues: 1/);
assert.match(summary, /Reports:/);
assert.match(summary, /json: reports\/plugin-inspector-report\.json/);
assert.match(summary, /Top findings:/);
assert.match(summary, /BREAKAGE weather missing-expected-seam/);
});
test("compatibility report renderer supports issue metadata and evidence links", () => {
const report = {
generatedAt: "test",
status: "pass",
targetOpenClaw: {
status: "ok",
compatRecords: ["plugin-sdk-export-aliases"],
compatRecordStatuses: { "plugin-sdk-export-aliases": "active" },
},
summary: {
fixtureCount: 1,
highPriorityFixtures: 1,
breakageCount: 0,
warningCount: 1,
suggestionCount: 0,
decisionCount: 1,
issueCount: 1,
p0IssueCount: 1,
p1IssueCount: 0,
liveIssueCount: 1,
liveP0IssueCount: 1,
compatGapCount: 0,
deprecationWarningCount: 0,
inspectorGapCount: 0,
upstreamIssueCount: 0,
fixtureRegressionCount: 0,
contractProbeCount: 1,
},
fixtures: [
{
id: "sample-plugin",
priority: "high",
seams: ["native-tool"],
hooks: ["before_tool_call"],
registrations: ["registerTool"],
manifestContracts: ["tools"],
},
],
breakages: [],
warnings: [
{
fixture: "sample-plugin",
code: "sdk-export-missing",
level: "warning",
message: "SDK alias is unavailable",
evidence: ["plugins/sample/src/index.ts:1"],
compatRecord: "plugin-sdk-export-aliases",
},
],
suggestions: [],
issues: [
{
fixture: "sample-plugin",
code: "sdk-export-missing",
issueClass: "live-issue",
decision: "core-compat-adapter",
severity: "P0",
title: "SDK alias is unavailable",
status: "blocking",
compatStatus: "untracked",
live: true,
evidence: ["plugins/sample/src/index.ts:1"],
},
],
contractProbes: [
{
fixture: "sample-plugin",
priority: "P1",
target: "sdk-alias",
contract: "package export exists",
id: "sdk.import.package-export-cold-import:sample-plugin",
evidence: ["plugins/sample/src/index.ts:1"],
},
],
logs: [],
decisions: [
{
fixture: "sample-plugin",
decision: "core-compat-adapter",
seam: "sdk-alias",
action: "add compat record",
evidence: "plugins/sample/src/index.ts:1",
},
],
};
const options = {
title: "Crabpot Compatibility Report",
severityLabels: { P0: "P0!" },
formatEvidence: (evidence) => `[linked](${evidence})`,
};
const markdown = renderCompatibilityMarkdownReport(report, options);
const issues = renderCompatibilityIssuesReport(report, {
...options,
title: "Crabpot Issue Findings",
});
assert.match(markdown, /# Crabpot Compatibility Report/);
assert.match(markdown, /## Target OpenClaw Compat Records/);
assert.match(markdown, /sdk\.import\.package-export-cold-import:sample-plugin/);
assert.match(issues, /# Crabpot Issue Findings/);
assert.match(issues, /P0! \*\*sample-plugin\*\* `live-issue` `core-compat-adapter`/);
assert.match(issues, /\[linked\]\(plugins\/sample\/src\/index\.ts:1\)/);
});
test("compatibility report artifacts sanitize absolute OpenClaw target paths", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-sanitized-report-"));
const absoluteOpenClawPath = path.join(outDir, "openclaw");
const report = {
generatedAt: "test",
status: "pass",
targetOpenClaw: {
status: "ok",
configuredPath: absoluteOpenClawPath,
searchedPaths: [absoluteOpenClawPath],
compatRecords: [],
compatRecordStatuses: {},
},
summary: {
fixtureCount: 1,
highPriorityFixtures: 1,
breakageCount: 0,
warningCount: 0,
suggestionCount: 0,
decisionCount: 0,
issueCount: 1,
p0IssueCount: 0,
p1IssueCount: 0,
liveIssueCount: 0,
liveP0IssueCount: 0,
compatGapCount: 0,
deprecationWarningCount: 0,
inspectorGapCount: 1,
upstreamIssueCount: 0,
fixtureRegressionCount: 0,
contractProbeCount: 0,
},
fixtures: [
{
id: "sample-plugin",
priority: "high",
seams: ["native-tool"],
hooks: [],
registrations: [],
manifestContracts: [],
},
],
breakages: [],
warnings: [],
suggestions: [],
issues: [
{
fixture: "sample-plugin",
code: "package-dependency-install-required",
issueClass: "inspector-gap",
decision: "inspector-follow-up",
severity: "P2",
title: `sample-plugin: path ${absoluteOpenClawPath}`,
status: "open",
compatStatus: "none",
live: false,
evidence: [absoluteOpenClawPath],
},
],
contractProbes: [],
logs: [],
decisions: [],
};
const markdown = renderCompatibilityMarkdownReport(report);
const issues = renderCompatibilityIssuesReport(report);
const paths = await writeCompatibilityReport(report, {
jsonPath: path.join(outDir, "report.json"),
markdownPath: path.join(outDir, "report.md"),
issuesPath: path.join(outDir, "issues.md"),
});
const artifact = JSON.parse(await readFile(paths.jsonPath, "utf8"));
assert.equal(report.targetOpenClaw.configuredPath, absoluteOpenClawPath);
assert.equal(artifact.targetOpenClaw.configuredPath, "<OPENCLAW_PATH>");
assert.deepEqual(artifact.targetOpenClaw.searchedPaths, ["<OPENCLAW_PATH>"]);
assert.equal(artifact.issues[0].evidence[0], "<OPENCLAW_PATH>");
assert.equal(artifact.issues[0].title, "sample-plugin: path <OPENCLAW_PATH>");
assert.doesNotMatch(markdown, new RegExp(escapeRegExp(absoluteOpenClawPath)));
assert.doesNotMatch(issues, new RegExp(escapeRegExp(absoluteOpenClawPath)));
assert.match(markdown, /<OPENCLAW_PATH>/);
assert.match(await readFile(paths.markdownPath, "utf8"), /<OPENCLAW_PATH>/);
assert.match(await readFile(paths.issuesPath, "utf8"), /<OPENCLAW_PATH>/);
});
test("compatibility report assembly classifies fixtures, issues, probes, and compat records", async () => {
const report = await buildCompatibilityReport({
generatedAt: "test",
fixtures: [
{
id: "fixture",
name: "Fixture",
path: "plugins/fixture",
priority: "high",
seams: ["native-tool"],
why: "covers native tool seams",
},
],
inspections: [
{
id: "fixture",
status: "ok",
hooks: ["before_tool_call"],
hookDetails: [{ name: "before_tool_call", ref: "plugins/fixture/src/index.ts:1" }],
registrations: ["registerTool"],
registrationDetails: [{ name: "registerTool", ref: "plugins/fixture/src/index.ts:2" }],
manifestContracts: [],
manifestFiles: [],
sdkImports: [{ specifier: "openclaw/plugin-sdk", ref: "plugins/fixture/src/index.ts:3" }],
sourceFiles: ["plugins/fixture/src/index.ts"],
},
],
failures: ["fixture: missing hooks: missing_hook"],
targetOpenClaw: {
status: "ok",
compatRecords: [],
compatRecordStatuses: {},
hookNames: ["before_tool_call"],
apiRegistrars: ["registerTool"],
capturedRegistrars: ["registerTool"],
sdkExports: ["openclaw/plugin-sdk"],
manifestFields: ["id"],
manifestContractFields: [],
},
buildFixtureReport: ({ fixture, inspection }) => ({
id: fixture.id,
name: fixture.name,
priority: fixture.priority,
seams: fixture.seams,
why: fixture.why,
status: inspection.status,
hooks: inspection.hooks,
hookDetails: inspection.hookDetails,
registrations: inspection.registrations,
registrationDetails: inspection.registrationDetails,
manifestContracts: inspection.manifestContracts,
manifestFiles: [],
sourceFiles: inspection.sourceFiles,
pluginManifests: [],
package: {
path: "plugins/fixture/package.json",
name: "fixture-plugin",
version: "1.0.0",
dependencies: [],
peerDependencies: [],
optionalDependencies: [],
openclaw: {
compatPluginApi: "^1.0.0",
entrypoints: [
{
kind: "extension",
specifier: "dist/index.js",
relativePath: "plugins/fixture/dist/index.js",
exists: true,
requiresBuild: false,
},
],
},
},
packages: [],
sdkImports: ["openclaw/plugin-sdk"],
sdkImportDetails: inspection.sdkImports,
}),
});
assert.equal(report.status, "fail");
assert.equal(report.summary.fixtureCount, 1);
assert.equal(report.summary.breakageCount, 1);
assert.ok(report.logs.some((finding) => finding.code === "seam-inventory"));
assert.ok(report.warnings.some((finding) => finding.code === "legacy-root-sdk-import"));
assert.ok(report.suggestions.some((finding) => finding.code === "before-tool-call-probe"));
assert.ok(report.suggestions.some((finding) => finding.code === "missing-compat-record"));
assert.ok(report.issues.some((issue) => issue.code === "missing-expected-seam"));
assert.ok(report.contractProbes.some((probe) => probe.id === "hook.before_tool_call.terminal-block-approval:fixture"));
assert.ok(report.decisions.some((decision) => decision.seam === "compat-registry"));
});
test("compat record coverage logs unavailable targets", () => {
const logs = [];
classifyCompatRecordCoverage({
targetOpenClaw: { status: "missing", configuredPath: "../openclaw" },
findings: [{ fixture: "fixture", compatRecord: "legacy-root-sdk-import" }],
suggestions: [],
logs,
decisions: [],
});
assert.deepEqual(logs[0], {
fixture: "openclaw",
code: "target-openclaw-unavailable",
level: "log",
message: "target OpenClaw checkout was not available, so compat record coverage was not checked",
evidence: ["../openclaw"],
});
});
test("compatibility fixture summary reads manifests and OpenClaw package metadata", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-fixture-summary-"));
const fixtureDir = path.join(rootDir, "plugin");
await mkdir(path.join(fixtureDir, "src"), { recursive: true });
await writeFile(
path.join(fixtureDir, "openclaw.plugin.json"),
`${JSON.stringify({ id: "fixture", name: "Fixture", version: "1.0.0", contracts: { tools: {} } }, null, 2)}\n`,
"utf8",
);
await writeFile(path.join(fixtureDir, "src", "index.js"), "export function register() {}\n", "utf8");
await writeFile(
path.join(fixtureDir, "package.json"),
`${JSON.stringify(
{
name: "fixture-plugin",
version: "1.0.0",
type: "module",
dependencies: { zod: "^1.0.0" },
openclaw: {
extensions: ["src/index.js"],
compat: { pluginApi: "^1.0.0" },
},
},
null,
2,
)}\n`,
"utf8",
);
const report = await buildCompatibilityFixtureReport({
rootDir,
checkoutPath: fixtureDir,
sourceRoot: fixtureDir,
fixture: {
id: "fixture",
name: "Fixture",
priority: "high",
seams: ["native-tool"],
why: "covers package metadata",
},
inspection: {
status: "ok",
hooks: [],
hookDetails: [],
registrations: ["registerTool"],
registrationDetails: [],
manifestContracts: ["tools"],
manifestFiles: ["plugin/openclaw.plugin.json"],
sourceFiles: ["plugin/src/index.js"],
sdkImports: [{ specifier: "openclaw/plugin-sdk" }],
},
});
assert.equal(report.pluginManifests[0].id, "fixture");
assert.equal(report.package.name, "fixture-plugin");
assert.equal(report.package.openclaw.compatPluginApi, "^1.0.0");
assert.deepEqual(report.package.openclaw.entrypoints[0], {
kind: "extension",
specifier: "src/index.js",
relativePath: "plugin/src/index.js",
exists: true,
requiresBuild: false,
});
assert.deepEqual(report.sdkImports, ["openclaw/plugin-sdk"]);
});
test("package contract classifier reports install and entrypoint blockers", () => {
const result = classifyPackageContracts({
fixture: {
id: "fixture",
path: "plugins/fixture",
},
inspection: {
registrations: ["registerTool"],
},
fixtureReport: {
pluginManifests: [{ version: "2.0.0" }],
package: {
path: "plugins/fixture/package.json",
name: "fixture-plugin",
version: "1.0.0",
dependencies: ["zod"],
peerDependencies: [],
optionalDependencies: [],
openclaw: {
compatPluginApi: null,
entrypoints: [
{
kind: "extension",
specifier: "dist/index.js",
relativePath: "plugins/fixture/dist/index.js",
exists: false,
requiresBuild: true,
},
],
},
},
},
});
assert.ok(result.logs.some((finding) => finding.code === "package-metadata"));
assert.ok(result.warnings.some((finding) => finding.code === "package-manifest-version-drift"));
assert.ok(result.warnings.some((finding) => finding.code === "package-plugin-api-compat-missing"));
assert.ok(result.suggestions.some((finding) => finding.code === "package-build-artifact-entrypoint"));
assert.ok(result.suggestions.some((finding) => finding.code === "package-dependency-install-required"));
assert.ok(result.decisions.some((decision) => decision.seam === "cold-import"));
const issues = buildIssues({
suggestions: result.suggestions,
targetOpenClaw: { status: "ok", compatRecordStatuses: {} },
});
assert.ok(
issues.some(
(issue) =>
issue.code === "package-dependency-install-required" &&
issue.title === "fixture: cold import requires dependency installation in an isolated workspace",
),
);
});
test("target OpenClaw coverage classifier reports missing public surface", () => {
const result = classifyTargetOpenClawCoverage({
fixture: { id: "fixture" },
inspection: {
hooks: ["missing_hook"],
hookDetails: [{ name: "missing_hook", ref: "plugins/fixture/src/index.ts:1" }],
registrationDetails: [{ name: "registerMissing", ref: "plugins/fixture/src/index.ts:2" }],
},
fixtureReport: {
sdkImports: ["openclaw/plugin-sdk/browser-security-runtime", "openclaw/plugin-sdk/missing"],
sdkImportDetails: [
{
specifier: "openclaw/plugin-sdk/browser-security-runtime",
ref: "plugins/fixture/src/index.ts:3",
},
{ specifier: "openclaw/plugin-sdk/missing", ref: "plugins/fixture/src/index.ts:4" },
],
pluginManifests: [
{
path: "plugins/fixture/openclaw.plugin.json",
keys: ["id", "unknownField"],
contracts: ["unknownContract"],
},
],
},
targetOpenClaw: {
status: "ok",
hookNames: ["known_hook"],
apiRegistrars: ["registerTool"],
sdkExports: ["openclaw/plugin-sdk", "openclaw/plugin-sdk/browser-security-runtime"],
reservedSdkExports: ["openclaw/plugin-sdk/browser-security-runtime"],
manifestFields: ["id"],
manifestContractFields: ["tools"],
},
});
assert.ok(result.warnings.some((finding) => finding.code === "unknown-hook-name"));
assert.ok(result.warnings.some((finding) => finding.code === "unknown-registration-name"));
assert.ok(result.warnings.some((finding) => finding.code === "reserved-sdk-import"));
assert.ok(result.warnings.some((finding) => finding.code === "sdk-export-missing"));
assert.ok(result.warnings.some((finding) => finding.code === "manifest-unknown-fields"));
assert.ok(result.warnings.some((finding) => finding.code === "manifest-unknown-contracts"));
assert.ok(result.logs.some((finding) => finding.code === "manifest-fields-checked"));
assert.ok(result.decisions.some((decision) => decision.seam === "sdk-alias"));
});
test("compatibility fixture classifier reports seam and metadata follow-ups", () => {
const result = classifyCompatibilityFixture({
fixture: { id: "fixture", path: "plugins/fixture" },
inspection: {
status: "ok",
hooks: ["llm_input", "before_tool_call"],
hookDetails: [
{ name: "llm_input", ref: "plugins/fixture/src/index.ts:1" },
{ name: "before_tool_call", ref: "plugins/fixture/src/index.ts:2" },
],
registrations: ["registerTool", "registerService"],
registrationDetails: [
{ name: "registerTool", ref: "plugins/fixture/src/index.ts:3" },
{ name: "registerService", ref: "plugins/fixture/src/index.ts:4" },
],
manifestContracts: [],
},
fixtureReport: {
sdkImports: ["openclaw/plugin-sdk"],
sdkImportDetails: [{ specifier: "openclaw/plugin-sdk", ref: "plugins/fixture/src/index.ts:5" }],
pluginManifests: [
{
path: "plugins/fixture/openclaw.plugin.json",
keys: ["id"],
contracts: [],
providerAuthEnvVars: { API_KEY: "api key" },
channelEnvVars: { CHANNEL_ID: "channel id" },
},
],
package: null,
},
targetOpenClaw: {
status: "ok",
hookNames: ["llm_input", "before_tool_call"],
apiRegistrars: ["registerTool"],
sdkExports: ["openclaw/plugin-sdk"],
manifestFields: ["id"],
manifestContractFields: [],
capturedRegistrars: ["registerTool"],
},
});
assert.ok(result.warnings.some((finding) => finding.code === "provider-auth-env-vars"));
assert.ok(result.warnings.some((finding) => finding.code === "channel-env-vars"));
assert.ok(result.warnings.some((finding) => finding.code === "conversation-access-hook"));
assert.ok(result.warnings.some((finding) => finding.code === "legacy-root-sdk-import"));
assert.ok(result.warnings.some((finding) => finding.code === "package-json-missing"));
assert.ok(result.suggestions.some((finding) => finding.code === "registration-capture-gap"));
assert.ok(result.suggestions.some((finding) => finding.code === "before-tool-call-probe"));
assert.ok(result.suggestions.some((finding) => finding.code === "runtime-tool-capture"));
assert.ok(result.decisions.some((decision) => decision.seam === "conversation-access"));
});
test("writeReport writes JSON and Markdown artifacts", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-report-"));
const config = await loadInspectorConfig("test/fixtures/inspector.config.json");
const report = await inspectFixtureSet(config);
const paths = await writeReport(report, { outDir, basename: "report" });
const json = JSON.parse(await readFile(paths.jsonPath, "utf8"));
const markdown = await readFile(paths.markdownPath, "utf8");
assert.equal(json.status, "pass");
assert.match(markdown, /Status: PASS/);
});
test("CI output helpers write SARIF and JUnit artifacts", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-ci-outputs-"));
const report = {
status: "fail",
summary: { fixtureCount: 1, breakageCount: 1 },
fixtures: [{ id: "weather", path: "." }],
breakages: [
{
fixture: "weather",
code: "missing-expected-seam",
level: "breakage",
message: "weather: missing expected registration registerTool",
evidence: ["registerTool @ src/index.js:12:4"],
},
],
warnings: [],
suggestions: [],
issues: [],
};
const sarif = buildSarifReport(report);
const junit = renderJunitXml(report);
const paths = await writeCiOutputArtifacts(report, {
outDir,
sarifPath: "plugin-inspector.sarif",
junitPath: "plugin-inspector.junit.xml",
});
assert.equal(sarif.version, "2.1.0");
assert.equal(sarif.runs[0].results[0].ruleId, "missing-expected-seam");
assert.equal(sarif.runs[0].results[0].level, "error");
assert.equal(sarif.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri, "src/index.js");
assert.match(junit, /tests="1" failures="1"/);
assert.match(junit, /missing expected registration registerTool/);
assert.equal(JSON.parse(await readFile(paths.sarifPath, "utf8")).runs[0].results.length, 1);
assert.match(await readFile(paths.junitPath, "utf8"), /<testsuite name="plugin-inspector"/);
});
test("artifact helpers write stable CI files", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-artifacts-"));
const jsonPath = path.join(outDir, "summary.json");
const markdownPath = path.join(outDir, "summary.md");
const paths = await writeArtifacts(
[
{ name: "jsonPath", path: jsonPath, json: { status: "pass" } },
{ name: "markdownPath", path: markdownPath, markdown: "# Summary" },
],
{ check: true },
);
assert.deepEqual(paths, { jsonPath, markdownPath });
assert.equal(await readFile(jsonPath, "utf8"), '{\n "status": "pass"\n}\n');
assert.equal(await readFile(markdownPath, "utf8"), "# Summary\n");
});
test("markdown table helper supports padded empty-table reports", () => {
assert.equal(
renderMarkdownTable(
[
["fixture", "P1"],
["none", null],
],
["Name", "Priority"],
{ padding: true, nullValue: "-", escape: false },
),
["| Name | Priority |", "| ------- | -------- |", "| fixture | P1 |", "| none | - |"].join("\n"),
);
assert.equal(renderMarkdownTable([], ["Name"], { empty: "_none_" }), "_none_");
});
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}