diff --git a/src/index.js b/src/index.js index 50df49c..290c4ac 100644 --- a/src/index.js +++ b/src/index.js @@ -65,7 +65,37 @@ export { renderExecutionResultsMarkdown, writeExecutionResultsReport, } from "./execution-results.js"; +export { + buildImportLoopProfile, + defaultImportLoopProfileOptions, + renderImportLoopProfileMarkdown, + validateImportLoopProfile, + writeImportLoopProfile, +} from "./import-loop-profile.js"; export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js"; export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js"; export { openClawTargetPathCandidates } from "./openclaw-target.js"; +export { + buildProfileDiff, + defaultProfileDiffOptions, + renderProfileDiffMarkdown, + validateProfileDiff, + writeProfileDiff, +} from "./profile-diff.js"; +export { + buildRefDiff, + defaultRefDiffDimensions, + defaultRefDiffOptions, + renderRefDiffMarkdown, + validateRefDiff, + writeRefDiff, +} from "./ref-diff.js"; export { renderMarkdownReport, writeReport } from "./report.js"; +export { + buildRuntimeProfile, + defaultRuntimeProfileCommands, + defaultRuntimeProfileOptions, + renderRuntimeProfileMarkdown, + validateRuntimeProfile, + writeRuntimeProfile, +} from "./runtime-profile.js"; diff --git a/test/api.test.js b/test/api.test.js index 946fe61..fe69a25 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -9,8 +9,12 @@ import { buildContractCapture, buildExecutionResultsReport, buildFixtureSetColdImportReadiness, + buildImportLoopProfile, buildFixtureSetPlatformProbes, buildFixtureSetWorkspacePlan, + buildProfileDiff, + buildRefDiff, + buildRuntimeProfile, capturePluginEntrypoint, classifyIssueFinding, inspectFixtureSet, @@ -27,11 +31,15 @@ import { renderCiSummaryMarkdown, renderContractCaptureMarkdown, renderExecutionResultsMarkdown, + renderImportLoopProfileMarkdown, renderMarkdownReport, renderFixtureSetColdImportReadinessMarkdown, renderFixtureSetIssuesReport, renderFixtureSetPlatformProbesMarkdown, renderFixtureSetWorkspacePlanMarkdown, + renderProfileDiffMarkdown, + renderRefDiffMarkdown, + renderRuntimeProfileMarkdown, runFixtureSetColdImportReadiness, runFixtureSetPlatformProbes, runFixtureSetReport, @@ -45,6 +53,10 @@ import { validateColdImportReadiness, validateFixtureSetPlatformProbes, validateFixtureSetWorkspacePlan, + validateImportLoopProfile, + validateProfileDiff, + validateRefDiff, + validateRuntimeProfile, writeFixtureSetColdImportReadiness, writeFixtureSetPlatformProbes, writeReport, @@ -54,6 +66,10 @@ import { writeCiPolicyReport, writeCiSummary, writeExecutionResultsReport, + writeImportLoopProfile, + writeProfileDiff, + writeRefDiff, + writeRuntimeProfile, } from "../src/index.js"; test("public API runs the plugin-root check and writes reports", async () => { @@ -401,6 +417,89 @@ test("public API exposes execution and CI rollup helpers", async () => { assert.equal(JSON.parse(await readFile(summaryPaths.jsonPath, "utf8")).status, "pass"); }); +test("public API exposes runtime profile and diff helpers", async () => { + const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-profile-api-")); + const runtimeProfile = await buildRuntimeProfile({ runs: 1 }); + const profileDiff = await buildProfileDiff({ + baseline: profileFixture({ p95WallMs: 100, maxPeakRssMb: 80, nodeBootMs: 25 }), + current: profileFixture({ p95WallMs: 120, maxPeakRssMb: 90, nodeBootMs: 30 }), + policy: { + thresholds: { + wallP95RegressionPercent: 50, + peakRssRegressionMb: 50, + bootRegressionMs: 500, + strictMinimumSamples: 3, + }, + }, + }); + const refDiff = await buildRefDiff({ + baseReport: diffReport({ hookNames: ["before_tool_call"], issues: [] }), + headReport: diffReport({ hookNames: ["before_tool_call"], issues: [] }), + }); + const importLoopProfile = { + generatedAt: "deterministic", + mode: "subprocess-cold-import-loop", + entrypoint: "fixtures/plugin.mjs", + summary: { + runs: 1, + p50WallMs: 5, + p95WallMs: 5, + maxPeakRssMb: 10, + maxCpuMsEstimate: 2, + capturedCount: 1, + failCount: 0, + }, + samples: [ + { + index: 0, + status: "captured", + capturedCount: 1, + wallMs: 5, + peakRssMb: 10, + cpuMsEstimate: 2, + exitCode: 0, + }, + ], + }; + + assert.equal(typeof buildImportLoopProfile, "function"); + const portableRuntimeProfile = { + ...runtimeProfile, + platform: { ...runtimeProfile.platform, rssSampler: "unavailable" }, + }; + + assert.deepEqual(validateRuntimeProfile(portableRuntimeProfile), []); + assert.deepEqual(validateProfileDiff(profileDiff), []); + assert.deepEqual(validateRefDiff(refDiff), []); + assert.deepEqual(validateImportLoopProfile(importLoopProfile), []); + assert.match(renderRuntimeProfileMarkdown(portableRuntimeProfile), /Runtime Profile/); + assert.match(renderProfileDiffMarkdown(profileDiff), /Runtime Profile Diff/); + assert.match(renderRefDiffMarkdown(refDiff), /Ref Diff/); + assert.match(renderImportLoopProfileMarkdown(importLoopProfile), /Import Loop Profile/); + + const runtimePaths = await writeRuntimeProfile(portableRuntimeProfile, { + jsonPath: path.join(outDir, "runtime.json"), + markdownPath: path.join(outDir, "runtime.md"), + }); + const profileDiffPaths = await writeProfileDiff(profileDiff, { + jsonPath: path.join(outDir, "profile-diff.json"), + markdownPath: path.join(outDir, "profile-diff.md"), + }); + const refDiffPaths = await writeRefDiff(refDiff, { + jsonPath: path.join(outDir, "ref-diff.json"), + markdownPath: path.join(outDir, "ref-diff.md"), + }); + const importLoopPaths = await writeImportLoopProfile(importLoopProfile, { + jsonPath: path.join(outDir, "import-loop.json"), + markdownPath: path.join(outDir, "import-loop.md"), + }); + + assert.equal(JSON.parse(await readFile(runtimePaths.jsonPath, "utf8")).summary.commandCount, 1); + assert.equal(JSON.parse(await readFile(profileDiffPaths.jsonPath, "utf8")).status, "pass"); + assert.equal(JSON.parse(await readFile(refDiffPaths.jsonPath, "utf8")).status, "pass"); + assert.equal(JSON.parse(await readFile(importLoopPaths.jsonPath, "utf8")).summary.runs, 1); +}); + test("public API honors config-driven runtime capture", async () => { const pluginRoot = await createPluginRoot(); await writeFile( @@ -413,6 +512,57 @@ test("public API honors config-driven runtime capture", async () => { assert.equal(result.runtimeCapture.summary.registrationCount, 1); }); +function profileFixture({ p95WallMs, maxPeakRssMb, nodeBootMs }) { + return { + runs: 3, + summary: { p95WallMs, maxPeakRssMb }, + targetOpenClaw: { + compatRecords: 1, + hookNames: 1, + apiRegistrars: 1, + capturedRegistrars: 1, + sdkExports: 1, + manifestFields: 1, + manifestContractFields: 1, + }, + fixtureInventory: {}, + commands: [{ id: "node-boot", wallMs: { median: nodeBootMs } }], + }; +} + +function diffReport({ hookNames, issues }) { + return { + summary: { + fixtureCount: 1, + breakageCount: 0, + issueCount: issues.length, + p0IssueCount: issues.filter((issue) => issue.severity === "P0").length, + p1IssueCount: issues.filter((issue) => issue.severity === "P1").length, + }, + targetOpenClaw: { + status: "available", + compatRecords: [], + hookNames, + apiRegistrars: ["registerTool"], + capturedRegistrars: ["registerTool"], + sdkExports: ["definePluginEntry"], + manifestFields: ["name"], + manifestContractFields: ["permissions"], + }, + fixtures: [ + { + id: "weather", + hooks: hookNames, + registrations: ["registerTool"], + sdkImports: ["definePluginEntry"], + pluginManifests: [{ name: "weather" }], + manifestContracts: ["permissions"], + }, + ], + issues, + }; +} + async function createPluginRoot(options = {}) { const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-api-root-")); await mkdir(path.join(rootDir, "src"), { recursive: true });