feat: add platform probe inventory
This commit is contained in:
parent
51c02712d3
commit
22cb50fdbd
@ -47,15 +47,18 @@ import {
|
||||
buildCiSummary,
|
||||
buildColdImportReadiness,
|
||||
buildContractCapture,
|
||||
buildPlatformProbes,
|
||||
createCaptureApi,
|
||||
inspectFixtureSet,
|
||||
loadInspectorConfig,
|
||||
renderColdImportReadinessMarkdown,
|
||||
renderContractCaptureMarkdown,
|
||||
renderPlatformProbesMarkdown,
|
||||
renderMarkdownReport,
|
||||
writeCiSummary,
|
||||
writeColdImportReadiness,
|
||||
writeContractCapture,
|
||||
writePlatformProbes,
|
||||
writeReport,
|
||||
} from "@openclaw/plugin-inspector";
|
||||
|
||||
@ -71,6 +74,9 @@ await writeContractCapture(capture);
|
||||
|
||||
const readiness = buildColdImportReadiness({ report });
|
||||
await writeColdImportReadiness(readiness);
|
||||
|
||||
const platformProbes = buildPlatformProbes({ plan: existingWorkspacePlan });
|
||||
await writePlatformProbes(platformProbes);
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
".": "./src/index.js",
|
||||
"./capture-api": "./src/capture-api.js",
|
||||
"./cold-import-readiness": "./src/cold-import-readiness.js",
|
||||
"./contract-capture": "./src/contract-capture.js"
|
||||
"./contract-capture": "./src/contract-capture.js",
|
||||
"./platform-probes": "./src/platform-probes.js"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@ -58,6 +58,13 @@ export {
|
||||
loadInspectorConfig,
|
||||
validateInspectorConfig,
|
||||
} from "./config.js";
|
||||
export {
|
||||
buildPlatformProbes,
|
||||
defaultPlatformTargets,
|
||||
renderPlatformProbesMarkdown,
|
||||
validatePlatformProbes,
|
||||
writePlatformProbes,
|
||||
} from "./platform-probes.js";
|
||||
export {
|
||||
renderMarkdownReport,
|
||||
renderTextSummary,
|
||||
|
||||
238
src/platform-probes.js
Normal file
238
src/platform-probes.js
Normal file
@ -0,0 +1,238 @@
|
||||
import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
||||
|
||||
export const defaultPlatformTargets = ["linux", "macos", "windows", "container"];
|
||||
|
||||
export function buildPlatformProbes(options = {}) {
|
||||
const plan = options.plan;
|
||||
if (!plan) {
|
||||
throw new TypeError("buildPlatformProbes requires an isolated workspace plan");
|
||||
}
|
||||
|
||||
const targets = options.targets ?? defaultPlatformTargets;
|
||||
const entrypoints = plan.fixtures.flatMap((fixture) =>
|
||||
fixture.entrypoints.map((entrypoint) => summarizeEntrypoint(fixture.id, entrypoint)),
|
||||
);
|
||||
const portabilityFindings = plan.fixtures.flatMap((fixture) =>
|
||||
fixture.entrypoints.flatMap((entrypoint) =>
|
||||
entrypoint.steps
|
||||
.map((step) => summarizeStep(fixture.id, entrypoint, step))
|
||||
.filter((finding) => finding.riskCodes.length > 0),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
generatedAt: plan.generatedAt,
|
||||
mode: "plan-only",
|
||||
targets,
|
||||
summary: {
|
||||
fixtureCount: plan.summary.fixtureCount,
|
||||
entrypointCount: entrypoints.length,
|
||||
tsLoaderEntrypointCount: entrypoints.filter((entrypoint) => entrypoint.loaderPrimary === "tsx").length,
|
||||
jitiAlternativeCount: entrypoints.filter((entrypoint) => entrypoint.loaderAlternatives.includes("jiti")).length,
|
||||
lazyImportProbeCount: entrypoints.filter((entrypoint) => entrypoint.capturePlanned && entrypoint.syntheticProbePlanned).length,
|
||||
portabilityFindingCount: portabilityFindings.length,
|
||||
windowsRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("windows")).length,
|
||||
macosRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("macos")).length,
|
||||
linuxRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("linux")).length,
|
||||
containerRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("container")).length,
|
||||
},
|
||||
entrypoints,
|
||||
portabilityFindings,
|
||||
recommendations: buildRecommendations(portabilityFindings, entrypoints),
|
||||
};
|
||||
}
|
||||
|
||||
export function validatePlatformProbes(report, options = {}) {
|
||||
const targets = options.targets ?? defaultPlatformTargets;
|
||||
const errors = [];
|
||||
if (report.mode !== "plan-only") {
|
||||
errors.push("platform probes must stay plan-only in default checks");
|
||||
}
|
||||
if (!targets.every((target) => report.targets.includes(target))) {
|
||||
errors.push(`platform probes must cover ${targets.join(", ")} targets`);
|
||||
}
|
||||
if (report.summary.tsLoaderEntrypointCount !== report.summary.jitiAlternativeCount) {
|
||||
errors.push("all TypeScript loader entrypoints must track a Jiti fallback candidate");
|
||||
}
|
||||
for (const entrypoint of report.entrypoints) {
|
||||
if (entrypoint.loaderPrimary === "tsx" && (!entrypoint.captureUsesTsx || !entrypoint.syntheticUsesTsx)) {
|
||||
errors.push(`${entrypoint.id}: tsx loader strategy is not reflected in capture and synthetic commands`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export async function writePlatformProbes(report, options = {}) {
|
||||
return writeJsonMarkdownArtifacts({
|
||||
jsonPath: options.jsonPath,
|
||||
markdownPath: options.markdownPath,
|
||||
json: report,
|
||||
markdown: renderPlatformProbesMarkdown(report, options),
|
||||
check: options.check,
|
||||
});
|
||||
}
|
||||
|
||||
export function renderPlatformProbesMarkdown(report, options = {}) {
|
||||
return [
|
||||
`# ${options.title ?? "Plugin Inspector Platform And Loader Probes"}`,
|
||||
"",
|
||||
`Generated: ${report.generatedAt}`,
|
||||
`Mode: ${report.mode}`,
|
||||
`Targets: ${report.targets.join(", ")}`,
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
markdownTable(Object.entries(report.summary).map(([key, value]) => [key, value]), ["Metric", "Value"]),
|
||||
"",
|
||||
"## Loader Probes",
|
||||
"",
|
||||
markdownTable(
|
||||
report.entrypoints.map((entrypoint) => [
|
||||
entrypoint.fixture,
|
||||
entrypoint.status,
|
||||
entrypoint.loaderPrimary,
|
||||
entrypoint.loaderAlternatives.join(", ") || "-",
|
||||
entrypoint.captureUsesTsx ? "yes" : "no",
|
||||
entrypoint.syntheticUsesTsx ? "yes" : "no",
|
||||
entrypoint.entrypoint,
|
||||
]),
|
||||
["Fixture", "Status", "Primary", "Alternatives", "Capture TSX", "Synthetic TSX", "Entrypoint"],
|
||||
),
|
||||
"",
|
||||
"## Portability Findings",
|
||||
"",
|
||||
markdownTable(
|
||||
report.portabilityFindings.map((finding) => [
|
||||
finding.fixture,
|
||||
finding.kind,
|
||||
finding.platforms.join(", ") || "-",
|
||||
finding.riskCodes.join(", "),
|
||||
finding.mitigation,
|
||||
]),
|
||||
["Fixture", "Step", "Platforms", "Risks", "Mitigation"],
|
||||
),
|
||||
"",
|
||||
"## Recommendations",
|
||||
"",
|
||||
markdownTable(
|
||||
report.recommendations.map((recommendation) => [recommendation.area, recommendation.action]),
|
||||
["Area", "Action"],
|
||||
),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function summarizeEntrypoint(fixtureId, entrypoint) {
|
||||
const captureStep = entrypoint.steps.find((step) => step.kind === "capture");
|
||||
const syntheticStep = entrypoint.steps.find((step) => step.kind === "synthetic-probe");
|
||||
return {
|
||||
fixture: fixtureId,
|
||||
id: entrypoint.id,
|
||||
status: entrypoint.status,
|
||||
entrypoint: entrypoint.entrypoint,
|
||||
packageManager: entrypoint.packageManager,
|
||||
loaderSource: entrypoint.loaderStrategy.source,
|
||||
loaderPrimary: entrypoint.loaderStrategy.primary,
|
||||
loaderAlternatives: entrypoint.loaderStrategy.alternatives,
|
||||
capturePlanned: Boolean(captureStep),
|
||||
syntheticProbePlanned: Boolean(syntheticStep),
|
||||
captureUsesTsx: Boolean(captureStep?.command.includes("--import tsx")),
|
||||
syntheticUsesTsx: Boolean(syntheticStep?.command.includes("--import tsx")),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeStep(fixtureId, entrypoint, step) {
|
||||
const riskCodes = stepRiskCodes(step);
|
||||
return {
|
||||
fixture: fixtureId,
|
||||
entrypoint: entrypoint.id,
|
||||
kind: step.kind,
|
||||
platforms: platformsForRiskCodes(riskCodes),
|
||||
riskCodes,
|
||||
command: step.command,
|
||||
mitigation: mitigationForRiskCodes(riskCodes),
|
||||
};
|
||||
}
|
||||
|
||||
function stepRiskCodes(step) {
|
||||
const risks = new Set();
|
||||
if (/\bmkdir -p\b/.test(step.command)) {
|
||||
risks.add("posix-mkdir");
|
||||
}
|
||||
if (/\brsync\b/.test(step.command)) {
|
||||
risks.add("rsync-required");
|
||||
}
|
||||
if (/^[A-Z0-9_]+=/.test(step.command)) {
|
||||
risks.add("posix-env-prefix");
|
||||
}
|
||||
if (/\|\|\s*true/.test(step.command)) {
|
||||
risks.add("posix-null-failure");
|
||||
}
|
||||
if (/\s>\s/.test(step.command)) {
|
||||
risks.add("shell-redirection");
|
||||
}
|
||||
if (step.command.includes("--import tsx")) {
|
||||
risks.add("tsx-loader-runtime");
|
||||
}
|
||||
if (/^(pnpm|yarn|bun)\b/.test(step.command)) {
|
||||
risks.add("package-manager-availability");
|
||||
}
|
||||
return [...risks].sort();
|
||||
}
|
||||
|
||||
function platformsForRiskCodes(riskCodes) {
|
||||
const platforms = new Set();
|
||||
for (const code of riskCodes) {
|
||||
if (["posix-mkdir", "rsync-required", "posix-env-prefix", "posix-null-failure"].includes(code)) {
|
||||
platforms.add("windows");
|
||||
}
|
||||
if (["rsync-required", "package-manager-availability"].includes(code)) {
|
||||
platforms.add("container");
|
||||
}
|
||||
if (code === "package-manager-availability") {
|
||||
platforms.add("linux");
|
||||
platforms.add("macos");
|
||||
platforms.add("windows");
|
||||
}
|
||||
}
|
||||
return [...platforms].sort();
|
||||
}
|
||||
|
||||
function mitigationForRiskCodes(riskCodes) {
|
||||
const mitigations = {
|
||||
"package-manager-availability": "install the declared package manager before isolated execution",
|
||||
"posix-env-prefix": "run isolated commands through a Node wrapper or set env via the runner API",
|
||||
"posix-mkdir": "replace shell mkdir with fs.mkdir({ recursive: true }) in the executor",
|
||||
"posix-null-failure": "capture audit failures in the executor instead of relying on shell fallthrough",
|
||||
"rsync-required": "copy workspaces with a Node fs.cp fallback before Windows/container lanes",
|
||||
"shell-redirection": "write audit JSON from the executor instead of shell redirection",
|
||||
"tsx-loader-runtime": "verify TS source entrypoints with tsx and Jiti loader lanes",
|
||||
};
|
||||
return riskCodes.map((code) => mitigations[code]).filter(Boolean).join("; ");
|
||||
}
|
||||
|
||||
function buildRecommendations(portabilityFindings, entrypoints) {
|
||||
const recommendations = [];
|
||||
if (entrypoints.some((entrypoint) => entrypoint.loaderPrimary === "tsx")) {
|
||||
recommendations.push({
|
||||
area: "loader",
|
||||
action: "keep tsx as the source-entrypoint smoke path, add a Jiti execution lane before treating TS plugin source compatibility as covered",
|
||||
});
|
||||
}
|
||||
if (portabilityFindings.some((finding) => finding.riskCodes.includes("rsync-required"))) {
|
||||
recommendations.push({
|
||||
area: "workspace-copy",
|
||||
action: "move isolated workspace copy into Node fs.cp so Windows and slim containers do not depend on rsync",
|
||||
});
|
||||
}
|
||||
if (portabilityFindings.some((finding) => finding.riskCodes.includes("posix-env-prefix"))) {
|
||||
recommendations.push({
|
||||
area: "executor",
|
||||
action: "replace shell env-prefix commands with structured spawn env for Windows parity",
|
||||
});
|
||||
}
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
function markdownTable(rows, headers) {
|
||||
return renderMarkdownTable(rows, headers, { empty: "_none_", escape: false, padding: true });
|
||||
}
|
||||
86
test/platform-probes.test.js
Normal file
86
test/platform-probes.test.js
Normal file
@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
buildPlatformProbes,
|
||||
renderPlatformProbesMarkdown,
|
||||
validatePlatformProbes,
|
||||
} from "../src/index.js";
|
||||
|
||||
test("platform probes classify loader and shell portability risks", () => {
|
||||
const report = buildPlatformProbes({
|
||||
plan: {
|
||||
generatedAt: "test",
|
||||
mode: "plan-only",
|
||||
summary: {
|
||||
fixtureCount: 1,
|
||||
},
|
||||
fixtures: [
|
||||
{
|
||||
id: "fixture",
|
||||
entrypoints: [
|
||||
{
|
||||
id: "cold-import.extension:fixture:index",
|
||||
status: "ts-loader-required",
|
||||
entrypoint: "plugins/fixture/src/index.ts",
|
||||
packageManager: "pnpm",
|
||||
loaderStrategy: {
|
||||
source: "typescript-source",
|
||||
primary: "tsx",
|
||||
alternatives: ["jiti"],
|
||||
reason: "test",
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
kind: "prepare",
|
||||
command: "mkdir -p .workspaces/fixture && rsync -a plugins/fixture/ .workspaces/fixture/",
|
||||
},
|
||||
{
|
||||
kind: "install",
|
||||
command: "pnpm install --ignore-scripts",
|
||||
},
|
||||
{
|
||||
kind: "capture",
|
||||
command: "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 node --import tsx capture.mjs ./src/index.ts",
|
||||
},
|
||||
{
|
||||
kind: "synthetic-probe",
|
||||
command: "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 node --import tsx synthetic.mjs --entrypoint ./src/index.ts",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(validatePlatformProbes(report), []);
|
||||
assert.equal(report.summary.tsLoaderEntrypointCount, 1);
|
||||
assert.equal(report.summary.jitiAlternativeCount, 1);
|
||||
assert.ok(report.summary.windowsRiskStepCount > 0);
|
||||
assert.ok(report.summary.containerRiskStepCount > 0);
|
||||
assert.match(renderPlatformProbesMarkdown(report), /Jiti/);
|
||||
assert.match(renderPlatformProbesMarkdown(report), /rsync/);
|
||||
});
|
||||
|
||||
test("platform probe validation requires jiti fallback and reflected tsx commands", () => {
|
||||
const errors = validatePlatformProbes({
|
||||
mode: "plan-only",
|
||||
targets: ["linux", "macos", "windows", "container"],
|
||||
summary: {
|
||||
tsLoaderEntrypointCount: 1,
|
||||
jitiAlternativeCount: 0,
|
||||
},
|
||||
entrypoints: [
|
||||
{
|
||||
id: "cold-import.extension:fixture:index",
|
||||
loaderPrimary: "tsx",
|
||||
captureUsesTsx: true,
|
||||
syntheticUsesTsx: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.ok(errors.some((error) => error.includes("Jiti fallback")));
|
||||
assert.ok(errors.some((error) => error.includes("tsx loader strategy")));
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user