Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8899fc796c | ||
|
|
feefb4ee23 | ||
|
|
f642fb5c9f | ||
|
|
68e10e0aaa | ||
|
|
12005b4658 | ||
|
|
4956ad1fbc |
17
CHANGELOG.md
17
CHANGELOG.md
@ -2,6 +2,23 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stop classifying package source entrypoints as missing when the published package provides built runtime entrypoints, and collapse SDK alias findings into a single compat-gap row.
|
||||
- Treat compat-gap issues as reconciled contract coverage for their own compatibility record.
|
||||
|
||||
## 0.3.10 - 2026-05-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Accept valid mocked capture output when plugin code leaves `process.exitCode` dirty.
|
||||
|
||||
## 0.3.9 - 2026-05-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Follow bundled channel `loadBundledEntryExportSync` registration exports during mocked runtime capture.
|
||||
|
||||
## 0.3.8 - 2026-05-03
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/plugin-inspector",
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.10",
|
||||
"private": false,
|
||||
"description": "Offline compatibility inspector for OpenClaw plugins.",
|
||||
"type": "module",
|
||||
|
||||
@ -58,9 +58,9 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
|
||||
options,
|
||||
),
|
||||
"",
|
||||
"## Live Issues",
|
||||
"## Other Live Issues",
|
||||
"",
|
||||
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
|
||||
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
|
||||
"",
|
||||
"## Compat Gaps",
|
||||
"",
|
||||
@ -183,9 +183,9 @@ export function renderCompatibilityIssuesReport(report, options = {}) {
|
||||
options,
|
||||
),
|
||||
"",
|
||||
"## Live Issues",
|
||||
"## Other Live Issues",
|
||||
"",
|
||||
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
|
||||
issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
|
||||
"",
|
||||
"## Compat Gaps",
|
||||
"",
|
||||
|
||||
@ -157,10 +157,15 @@ function requireCompatRecordReconciliation(report, errors) {
|
||||
.filter((finding) => finding.code === "missing-compat-record")
|
||||
.map((finding) => `${finding.fixture}:${finding.compatRecord}`),
|
||||
);
|
||||
const compatGapRecords = new Set(
|
||||
report.issues
|
||||
.filter((issue) => issue.issueClass === "compat-gap" && issue.compatRecord)
|
||||
.map((issue) => `${issue.fixture}:${issue.compatRecord}`),
|
||||
);
|
||||
|
||||
for (const finding of [...report.warnings, ...report.suggestions].filter((item) => item.compatRecord)) {
|
||||
const key = `${finding.fixture}:${finding.compatRecord}`;
|
||||
if (!presentRecords.has(key) && !missingRecords.has(key)) {
|
||||
if (!presentRecords.has(key) && !missingRecords.has(key) && !compatGapRecords.has(key)) {
|
||||
errors.push(`${finding.fixture}: compat record ${finding.compatRecord} was not reconciled`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,9 +340,12 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
|
||||
});
|
||||
}
|
||||
|
||||
const missingEntrypoints = packageSummary.openclaw?.entrypoints.filter((entrypoint) => !entrypoint.exists) ?? [];
|
||||
const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
|
||||
const missingEntrypoints = entrypoints.filter((entrypoint) => !entrypoint.exists);
|
||||
const buildEntrypoints = missingEntrypoints.filter((entrypoint) => entrypoint.requiresBuild);
|
||||
const plainMissingEntrypoints = missingEntrypoints.filter((entrypoint) => !entrypoint.requiresBuild);
|
||||
const plainMissingEntrypoints = missingEntrypoints.filter(
|
||||
(entrypoint) => !entrypoint.requiresBuild && !hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
|
||||
);
|
||||
|
||||
if (buildEntrypoints.length > 0) {
|
||||
suggestions.push({
|
||||
@ -920,6 +923,46 @@ function collectOpenClawEntrypoints(packageDir, openclaw, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
|
||||
if (!isSourceEntrypoint(entrypoint.specifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
|
||||
if (
|
||||
entrypoints.some(
|
||||
(candidate) =>
|
||||
candidate.exists &&
|
||||
candidate.requiresBuild &&
|
||||
normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entrypoint.kind === "extension" && entrypoints.some((candidate) => candidate.kind === "runtimeExtension" && candidate.exists)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const packageDir = path.dirname(packageSummary.path);
|
||||
return existsSync(path.resolve(packageDir, runtimeBuildSpecifier));
|
||||
}
|
||||
|
||||
function isSourceEntrypoint(specifier) {
|
||||
return /\.(?:ts|tsx)$/.test(specifier);
|
||||
}
|
||||
|
||||
function runtimeBuildSpecifierFor(specifier) {
|
||||
const normalized = normalizeEntrypointSpecifier(specifier);
|
||||
const basename = path.posix.basename(normalized).replace(/\.(?:ts|tsx)$/, ".js");
|
||||
return `./dist/${basename}`;
|
||||
}
|
||||
|
||||
function normalizeEntrypointSpecifier(specifier) {
|
||||
const normalized = specifier.replaceAll("\\", "/");
|
||||
return normalized.startsWith("./") ? normalized : `./${normalized}`;
|
||||
}
|
||||
|
||||
async function findPackageFiles(root, options, depth = 0) {
|
||||
if (!existsSync(root) || depth > options.maxDepth) {
|
||||
return [];
|
||||
@ -1059,8 +1102,13 @@ function packageNpmPackIssues(packageSummary, fixtureReport) {
|
||||
});
|
||||
}
|
||||
|
||||
const missingEntrypoints = packageSummary.openclaw?.entrypoints
|
||||
.filter((entrypoint) => !repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath))
|
||||
const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
|
||||
const missingEntrypoints = entrypoints
|
||||
.filter(
|
||||
(entrypoint) =>
|
||||
!repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath) &&
|
||||
!hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
|
||||
)
|
||||
.map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`) ?? [];
|
||||
if (missingEntrypoints.length > 0) {
|
||||
findings.push({
|
||||
@ -1088,6 +1136,35 @@ function packageNpmPackMissingMetadata(packageSummary, fixtureReport) {
|
||||
return missing;
|
||||
}
|
||||
|
||||
function hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
|
||||
if (!isSourceEntrypoint(entrypoint.specifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
|
||||
const matchingRuntimeEntrypoint = entrypoints.find(
|
||||
(candidate) =>
|
||||
candidate.requiresBuild &&
|
||||
normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
|
||||
);
|
||||
if (matchingRuntimeEntrypoint && repoPathIncludedInNpmPack(packageSummary, matchingRuntimeEntrypoint.relativePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
entrypoint.kind === "extension" &&
|
||||
entrypoints.some(
|
||||
(candidate) => candidate.kind === "runtimeExtension" && repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const packageDir = path.posix.dirname(normalizeRepoPath(packageSummary.path));
|
||||
const runtimeBuildPath = path.posix.join(packageDir === "." ? "" : packageDir, normalizeEntrypointSpecifier(runtimeBuildSpecifier));
|
||||
return repoPathIncludedInNpmPack(packageSummary, runtimeBuildPath);
|
||||
}
|
||||
|
||||
function packageMinHostVersionDrift(packageSummary) {
|
||||
const openclaw = packageSummary.openclaw;
|
||||
if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
|
||||
|
||||
@ -234,10 +234,34 @@ export async function captureEntrypointWithMockSdk(entrypoint, options = {}) {
|
||||
);
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
const captured = parseCaptureResultFromStdout(error?.stdout);
|
||||
if (captured) {
|
||||
return captured;
|
||||
}
|
||||
throw classifyMockSdkCaptureError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function parseCaptureResultFromStdout(stdout) {
|
||||
if (!stdout) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(stdout);
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
typeof parsed.status === "string" &&
|
||||
Array.isArray(parsed.captured)
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function classifyMockSdkCaptureError(error) {
|
||||
const rawMessage = [error?.stderr, error?.stdout, error?.message].filter(Boolean).join("\n");
|
||||
const missingExport = rawMessage.match(/does not provide an export named ['"]([^'"]+)['"]/)?.[1];
|
||||
|
||||
@ -342,7 +342,10 @@ export function summarizeIssueClasses(issues) {
|
||||
}
|
||||
|
||||
function issueClassFor(code, options) {
|
||||
if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)) {
|
||||
if (code === "sdk-export-missing" && options.compatRecord) {
|
||||
return "compat-gap";
|
||||
}
|
||||
if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)) {
|
||||
return "live-issue";
|
||||
}
|
||||
if (code === "missing-compat-record") {
|
||||
@ -397,7 +400,7 @@ function severityForClass(code, defaultSeverity, options) {
|
||||
if (
|
||||
options.issueClass === "live-issue" &&
|
||||
["none", "untracked"].includes(options.compatStatus) &&
|
||||
["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)
|
||||
["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)
|
||||
) {
|
||||
return "P0";
|
||||
}
|
||||
|
||||
@ -225,6 +225,10 @@ export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggest
|
||||
continue;
|
||||
}
|
||||
|
||||
if (finding.code === "sdk-export-missing") {
|
||||
continue;
|
||||
}
|
||||
|
||||
suggestions.push({
|
||||
fixture: finding.fixture,
|
||||
code: "missing-compat-record",
|
||||
|
||||
@ -551,6 +551,9 @@ function genericExportStatement(name) {
|
||||
if (name === "defineBundledChannelSetupEntry") {
|
||||
return "export { defineBundledChannelSetupEntry };";
|
||||
}
|
||||
if (name === "loadBundledEntryExportSync") {
|
||||
return "export { loadBundledEntryExportSync };";
|
||||
}
|
||||
if (/^[A-Z].*Schema$/u.test(name)) {
|
||||
return `export const ${name} = createSchema();`;
|
||||
}
|
||||
@ -558,7 +561,13 @@ function genericExportStatement(name) {
|
||||
}
|
||||
|
||||
function genericMockRuntimeSource(options = {}) {
|
||||
return `${options.includeSdkRuntime ? `function definePluginEntry(entry) {
|
||||
return `${options.includeSdkRuntime ? `import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const pendingBundledEntryLoads = new Set();
|
||||
|
||||
function definePluginEntry(entry) {
|
||||
if (entry && typeof entry === "object" && typeof entry.register === "function") {
|
||||
return entry;
|
||||
}
|
||||
@ -572,7 +581,7 @@ function defineBundledChannelEntry(entry = {}) {
|
||||
return {
|
||||
...entry,
|
||||
kind: "bundled-channel-entry",
|
||||
register(api) {
|
||||
async register(api) {
|
||||
if (api?.registrationMode === "cli-metadata") {
|
||||
return entry.registerCliMetadata?.(api);
|
||||
}
|
||||
@ -585,7 +594,12 @@ function defineBundledChannelEntry(entry = {}) {
|
||||
});
|
||||
}
|
||||
entry.registerCliMetadata?.(api);
|
||||
return entry.registerFull?.(api);
|
||||
const result = entry.registerFull?.(api);
|
||||
if (result && typeof result.then === "function") {
|
||||
await result;
|
||||
}
|
||||
await drainBundledEntryLoads();
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -596,6 +610,46 @@ function defineBundledChannelSetupEntry(entry = {}) {
|
||||
kind: "bundled-channel-setup-entry",
|
||||
};
|
||||
}
|
||||
|
||||
function loadBundledEntryExportSync(importMetaUrl, options = {}) {
|
||||
return (...args) => {
|
||||
const promise = import(resolveBundledEntryUrl(importMetaUrl, options.specifier)).then((module) => {
|
||||
const loaded = module[options.exportName] ?? module.default;
|
||||
return typeof loaded === "function" ? loaded(...args) : loaded;
|
||||
});
|
||||
pendingBundledEntryLoads.add(promise);
|
||||
promise.finally(() => pendingBundledEntryLoads.delete(promise));
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
async function drainBundledEntryLoads() {
|
||||
while (pendingBundledEntryLoads.size > 0) {
|
||||
await Promise.all([...pendingBundledEntryLoads]);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBundledEntryUrl(importMetaUrl, specifier) {
|
||||
const basePath = fileURLToPath(importMetaUrl);
|
||||
const target = specifier ? path.resolve(path.dirname(basePath), specifier) : basePath;
|
||||
const resolved = resolveExistingSourcePath(target);
|
||||
return pathToFileURL(resolved).href;
|
||||
}
|
||||
|
||||
function resolveExistingSourcePath(target) {
|
||||
if (existsSync(target)) {
|
||||
return target;
|
||||
}
|
||||
const parsed = path.parse(target);
|
||||
const withoutJsExtension = [".js", ".mjs", ".cjs"].includes(parsed.ext) ? path.join(parsed.dir, parsed.name) : null;
|
||||
const candidates = [
|
||||
...(withoutJsExtension ? [\`\${withoutJsExtension}.ts\`, \`\${withoutJsExtension}.mts\`, \`\${withoutJsExtension}.cts\`] : []),
|
||||
\`\${target}.js\`,
|
||||
\`\${target}.mjs\`,
|
||||
\`\${target}.ts\`,
|
||||
];
|
||||
return candidates.find((candidate) => existsSync(candidate)) ?? target;
|
||||
}
|
||||
` : ""}
|
||||
function createMockValue(name) {
|
||||
function fn(...args) {
|
||||
@ -1122,16 +1176,36 @@ export function normalizeSecretInputString(value) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function createSimpleSchema(defaultValue) {
|
||||
return {
|
||||
parse(value) {
|
||||
return value === undefined ? defaultValue : value;
|
||||
},
|
||||
default(value) {
|
||||
return createSimpleSchema(value);
|
||||
},
|
||||
optional() {
|
||||
return this;
|
||||
},
|
||||
nullable() {
|
||||
return this;
|
||||
},
|
||||
nullish() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSecretInputSchema() {
|
||||
return { type: "string" };
|
||||
return createSimpleSchema();
|
||||
}
|
||||
|
||||
export function buildOptionalSecretInputSchema() {
|
||||
return { anyOf: [buildSecretInputSchema(), { type: "undefined" }] };
|
||||
return createSimpleSchema();
|
||||
}
|
||||
|
||||
export function buildSecretInputArraySchema() {
|
||||
return { type: "array", items: buildSecretInputSchema() };
|
||||
return createSimpleSchema([]);
|
||||
}
|
||||
|
||||
export function registerPluginHttpRoute(options = {}) {
|
||||
|
||||
@ -165,4 +165,20 @@ test("contract coverage requires compat record reconciliation evidence", () => {
|
||||
compatRecord: "fixture.provider-auth-env-vars",
|
||||
});
|
||||
assert.deepEqual(validateContractCoverage(report), []);
|
||||
|
||||
report.logs = [];
|
||||
report.issues.push({
|
||||
id: "PLUGIN-COMPAT",
|
||||
fixture: "fixture",
|
||||
severity: "P1",
|
||||
issueClass: "compat-gap",
|
||||
code: "provider-auth-env-vars",
|
||||
evidence: ["fixture"],
|
||||
compatRecord: "fixture.provider-auth-env-vars",
|
||||
});
|
||||
report.contractProbes.push({
|
||||
fixture: "fixture",
|
||||
id: "compat.provider-auth-env-vars:fixture",
|
||||
});
|
||||
assert.deepEqual(validateContractCoverage(report), []);
|
||||
});
|
||||
|
||||
@ -197,6 +197,35 @@ test("capture entrypoint can mock OpenClaw plugin SDK imports", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 });
|
||||
@ -264,3 +293,56 @@ test("mock capture expands bundled channel entry registration shells", async ()
|
||||
"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",
|
||||
]);
|
||||
});
|
||||
|
||||
@ -19,18 +19,18 @@ test("issue ids are stable fingerprints", () => {
|
||||
test("issue classification separates live breaks from compat and deprecation buckets", () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "untracked SDK alias is a blocking live issue",
|
||||
name: "untracked SDK alias is a compat gap",
|
||||
finding: { code: "sdk-export-missing", compatRecord: "plugin-sdk-export-aliases" },
|
||||
targetOpenClaw: { compatRecordStatuses: {} },
|
||||
metadata: { severity: "P1" },
|
||||
expected: { issueClass: "live-issue", compatStatus: "untracked", severity: "P0", live: true },
|
||||
expected: { issueClass: "compat-gap", compatStatus: "untracked", severity: "P1", live: false },
|
||||
},
|
||||
{
|
||||
name: "active SDK alias compat avoids false P0 escalation",
|
||||
name: "active SDK alias compat stays a compat row",
|
||||
finding: { code: "sdk-export-missing", compatRecord: "plugin-sdk-export-aliases" },
|
||||
targetOpenClaw: { compatRecordStatuses: { "plugin-sdk-export-aliases": "active" } },
|
||||
metadata: { severity: "P1" },
|
||||
expected: { issueClass: "live-issue", compatStatus: "active", severity: "P1", live: true },
|
||||
expected: { issueClass: "compat-gap", compatStatus: "active", severity: "P1", live: false },
|
||||
},
|
||||
{
|
||||
name: "deprecated compat remains warning-class even when used",
|
||||
@ -112,17 +112,17 @@ test("issue builder applies metadata and class summaries", () => {
|
||||
assert.deepEqual(
|
||||
issues.map((issue) => [issue.fixture, issue.code, issue.severity, issue.issueClass, issue.status]),
|
||||
[
|
||||
["codex-app-server", "sdk-export-missing", "P0", "live-issue", "blocking"],
|
||||
["codex-app-server", "sdk-export-missing", "P1", "compat-gap", "open"],
|
||||
["agentchat", "manifest-unknown-fields", "P2", "upstream-metadata", "open"],
|
||||
["wecom", "registration-capture-gap", "P2", "inspector-gap", "open"],
|
||||
],
|
||||
);
|
||||
assert.deepEqual(summarizeIssueClasses(issues), {
|
||||
"compat-gap": 0,
|
||||
"compat-gap": 1,
|
||||
"deprecation-warning": 0,
|
||||
"fixture-regression": 0,
|
||||
"inspector-gap": 1,
|
||||
"live-issue": 1,
|
||||
"live-issue": 0,
|
||||
"upstream-metadata": 1,
|
||||
});
|
||||
});
|
||||
|
||||
@ -865,6 +865,79 @@ test("package contract classifier reports advertised npm pack blockers", () => {
|
||||
assert.equal(globResult.warnings.some((finding) => finding.code.startsWith("package-npm-pack-")), false);
|
||||
});
|
||||
|
||||
test("package contract classifier accepts built runtime entries for source package metadata", () => {
|
||||
const result = classifyPackageContracts({
|
||||
fixture: {
|
||||
id: "fixture",
|
||||
path: "plugins/fixture",
|
||||
},
|
||||
inspection: {
|
||||
registrations: ["registerTool"],
|
||||
},
|
||||
fixtureReport: {
|
||||
pluginManifests: [{ path: "plugins/fixture/openclaw.plugin.json", version: "1.0.0" }],
|
||||
package: {
|
||||
path: "plugins/fixture/package.json",
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
npmPack: {
|
||||
advertised: true,
|
||||
private: false,
|
||||
filesMode: "allowlist",
|
||||
files: ["dist/**", "openclaw.plugin.json"],
|
||||
invalidFileSpecs: [],
|
||||
},
|
||||
dependencies: [],
|
||||
peerDependencies: ["openclaw"],
|
||||
optionalDependencies: [],
|
||||
openclaw: {
|
||||
compatPluginApi: "^1.0.0",
|
||||
install: {
|
||||
npmSpec: "@openclaw/fixture-plugin",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
entrypoints: [
|
||||
{
|
||||
kind: "extension",
|
||||
specifier: "./index.ts",
|
||||
relativePath: "plugins/fixture/index.ts",
|
||||
exists: false,
|
||||
requiresBuild: false,
|
||||
},
|
||||
{
|
||||
kind: "runtimeExtension",
|
||||
specifier: "./dist/index.js",
|
||||
relativePath: "plugins/fixture/dist/index.js",
|
||||
exists: true,
|
||||
requiresBuild: true,
|
||||
},
|
||||
{
|
||||
kind: "setupEntry",
|
||||
specifier: "./setup-entry.ts",
|
||||
relativePath: "plugins/fixture/setup-entry.ts",
|
||||
exists: false,
|
||||
requiresBuild: false,
|
||||
},
|
||||
{
|
||||
kind: "runtimeExtension",
|
||||
specifier: "./dist/setup-entry.js",
|
||||
relativePath: "plugins/fixture/dist/setup-entry.js",
|
||||
exists: true,
|
||||
requiresBuild: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.warnings.some((finding) => finding.code === "package-entrypoint-missing"), false);
|
||||
assert.equal(result.warnings.some((finding) => finding.code === "package-npm-pack-entrypoint-missing"), false);
|
||||
assert.equal(result.decisions.some((decision) => decision.seam === "package-entrypoint"), false);
|
||||
});
|
||||
|
||||
test("target OpenClaw coverage classifier reports missing public surface", () => {
|
||||
const result = classifyTargetOpenClawCoverage({
|
||||
fixture: { id: "fixture" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user