fix(runtime): harden plugin capture mocks
This commit is contained in:
parent
b919df78d3
commit
a6af8800e0
@ -55,8 +55,11 @@ export function validatePlatformProbes(report, options = {}) {
|
||||
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`);
|
||||
if (
|
||||
entrypoint.loaderPrimary === "tsx" &&
|
||||
(!entrypoint.captureUsesTypeScriptLoader || !entrypoint.syntheticUsesTypeScriptLoader)
|
||||
) {
|
||||
errors.push(`${entrypoint.id}: TypeScript loader strategy is not reflected in capture and synthetic commands`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
@ -94,9 +97,21 @@ export function renderPlatformProbesMarkdown(report, options = {}) {
|
||||
entrypoint.loaderAlternatives.join(", ") || "-",
|
||||
entrypoint.captureUsesTsx ? "yes" : "no",
|
||||
entrypoint.syntheticUsesTsx ? "yes" : "no",
|
||||
entrypoint.captureUsesMockSdk ? "yes" : "no",
|
||||
entrypoint.syntheticUsesMockSdk ? "yes" : "no",
|
||||
entrypoint.entrypoint,
|
||||
]),
|
||||
["Fixture", "Status", "Primary", "Alternatives", "Capture TSX", "Synthetic TSX", "Entrypoint"],
|
||||
[
|
||||
"Fixture",
|
||||
"Status",
|
||||
"Primary",
|
||||
"Alternatives",
|
||||
"Capture TSX",
|
||||
"Synthetic TSX",
|
||||
"Capture Mock SDK",
|
||||
"Synthetic Mock SDK",
|
||||
"Entrypoint",
|
||||
],
|
||||
),
|
||||
"",
|
||||
"## Portability Findings",
|
||||
@ -137,6 +152,10 @@ export function renderPlatformProbesMarkdown(report, options = {}) {
|
||||
function summarizeEntrypoint(fixtureId, entrypoint) {
|
||||
const captureStep = entrypoint.steps.find((step) => step.kind === "capture");
|
||||
const syntheticStep = entrypoint.steps.find((step) => step.kind === "synthetic-probe");
|
||||
const captureUsesTsx = Boolean(captureStep?.command.includes("--import tsx"));
|
||||
const syntheticUsesTsx = Boolean(syntheticStep?.command.includes("--import tsx"));
|
||||
const captureUsesMockSdk = Boolean(captureStep?.command.includes("--mock-sdk"));
|
||||
const syntheticUsesMockSdk = Boolean(syntheticStep?.command.includes("--mock-sdk"));
|
||||
return {
|
||||
fixture: fixtureId,
|
||||
id: entrypoint.id,
|
||||
@ -148,8 +167,12 @@ function summarizeEntrypoint(fixtureId, entrypoint) {
|
||||
loaderAlternatives: entrypoint.loaderStrategy.alternatives,
|
||||
capturePlanned: Boolean(captureStep),
|
||||
syntheticProbePlanned: Boolean(syntheticStep),
|
||||
captureUsesTsx: Boolean(captureStep?.command.includes("--import tsx")),
|
||||
syntheticUsesTsx: Boolean(syntheticStep?.command.includes("--import tsx")),
|
||||
captureUsesTsx,
|
||||
syntheticUsesTsx,
|
||||
captureUsesMockSdk,
|
||||
syntheticUsesMockSdk,
|
||||
captureUsesTypeScriptLoader: captureUsesTsx || captureUsesMockSdk,
|
||||
syntheticUsesTypeScriptLoader: syntheticUsesTsx || syntheticUsesMockSdk,
|
||||
};
|
||||
}
|
||||
|
||||
@ -260,7 +283,7 @@ function buildRecommendations(portabilityFindings, entrypoints) {
|
||||
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",
|
||||
action: "keep mock-SDK TypeScript capture green, add a real host-loader/Jiti lane before treating TS plugin source compatibility as covered",
|
||||
});
|
||||
}
|
||||
if (portabilityFindings.some((finding) => finding.riskCodes.includes("rsync-required"))) {
|
||||
|
||||
23
src/prune-workspace-dev-deps-cli.js
Normal file
23
src/prune-workspace-dev-deps-cli.js
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const packageJsonPath = path.resolve(process.cwd(), "package.json");
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||
let changed = false;
|
||||
|
||||
for (const [name, specifier] of Object.entries(packageJson.devDependencies ?? {})) {
|
||||
if (typeof specifier === "string" && specifier.startsWith("workspace:")) {
|
||||
delete packageJson.devDependencies[name];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (packageJson.devDependencies && Object.keys(packageJson.devDependencies).length === 0) {
|
||||
delete packageJson.devDependencies;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
||||
}
|
||||
@ -254,9 +254,20 @@ export async function createMockSdkPackage(rootDir, options = {}) {
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(path.join(pluginSdkDir, "index.js"), mockSdkSource(), "utf8");
|
||||
const rootExportNames = new Set([
|
||||
...mockSdkExportNames,
|
||||
...(imports.bySpecifier.get("openclaw/plugin-sdk") ?? []),
|
||||
]);
|
||||
await writeFile(path.join(pluginSdkDir, "index.js"), mockSdkSource(rootExportNames), "utf8");
|
||||
for (const [subpath, exportNames] of Object.entries(mockSdkSubpathExports)) {
|
||||
await writeFile(path.join(pluginSdkDir, `${subpath}.js`), mockSdkSubpathSource(exportNames), "utf8");
|
||||
const specifier = `openclaw/plugin-sdk/${subpath}`;
|
||||
await writeFile(
|
||||
path.join(pluginSdkDir, `${subpath}.js`),
|
||||
mockSdkSubpathSource(exportNames, imports.bySpecifier.get(specifier) ?? new Set(), {
|
||||
zod: subpath === "zod",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
for (const specifier of imports.openclawSdkSpecifiers) {
|
||||
if (specifier === "openclaw/plugin-sdk") {
|
||||
@ -428,6 +439,9 @@ export async function resolve(specifier, context, nextResolve) {
|
||||
const subpath = specifier.slice("openclaw/plugin-sdk/".length);
|
||||
return moduleUrl(path.join(pluginSdkDir, \`\${subpath}.js\`));
|
||||
}
|
||||
if (externalMap.has(specifier)) {
|
||||
return moduleUrl(externalMap.get(specifier));
|
||||
}
|
||||
try {
|
||||
return await nextResolve(specifier, context);
|
||||
} catch (error) {
|
||||
@ -531,6 +545,12 @@ function genericExportStatement(name) {
|
||||
if (["createChatChannelPlugin", "createPlugin", "defineChannelPluginEntry", "definePlugin", "definePluginEntry", "defineSetupPluginEntry"].includes(name)) {
|
||||
return name === "definePluginEntry" ? "export { definePluginEntry };" : `export const ${name} = definePluginEntry;`;
|
||||
}
|
||||
if (name === "defineBundledChannelEntry") {
|
||||
return "export { defineBundledChannelEntry };";
|
||||
}
|
||||
if (name === "defineBundledChannelSetupEntry") {
|
||||
return "export { defineBundledChannelSetupEntry };";
|
||||
}
|
||||
if (/^[A-Z].*Schema$/u.test(name)) {
|
||||
return `export const ${name} = createSchema();`;
|
||||
}
|
||||
@ -547,9 +567,41 @@ function genericMockRuntimeSource(options = {}) {
|
||||
}
|
||||
return typeof entry === "function" ? { register: entry } : entry;
|
||||
}
|
||||
|
||||
function defineBundledChannelEntry(entry = {}) {
|
||||
return {
|
||||
...entry,
|
||||
kind: "bundled-channel-entry",
|
||||
register(api) {
|
||||
if (api?.registrationMode === "cli-metadata") {
|
||||
return entry.registerCliMetadata?.(api);
|
||||
}
|
||||
if (api?.registrationMode !== "tool-discovery") {
|
||||
api?.registerChannel?.({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
plugin: { id: entry.id, name: entry.name },
|
||||
});
|
||||
}
|
||||
entry.registerCliMetadata?.(api);
|
||||
return entry.registerFull?.(api);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function defineBundledChannelSetupEntry(entry = {}) {
|
||||
return {
|
||||
...entry,
|
||||
kind: "bundled-channel-setup-entry",
|
||||
};
|
||||
}
|
||||
` : ""}
|
||||
function createMockValue(name) {
|
||||
function fn(...args) {
|
||||
if (name === "resolvePreferredOpenClawTmpDir") {
|
||||
return process.env.TMPDIR || "/tmp";
|
||||
}
|
||||
if (name.startsWith("normalize")) {
|
||||
return typeof args[0] === "string" ? args[0] : "";
|
||||
}
|
||||
@ -728,7 +780,8 @@ function createTypeNamespace() {
|
||||
`;
|
||||
}
|
||||
|
||||
function mockSdkSource() {
|
||||
function mockSdkSource(exportNames = mockSdkExportNames) {
|
||||
const dynamicExportNames = [...exportNames].filter((name) => !mockSdkExportNames.includes(name));
|
||||
return `function normalizeEntry(entry) {
|
||||
return typeof entry === "function" ? { register: entry } : entry;
|
||||
}
|
||||
@ -1538,15 +1591,20 @@ export const OPENAI_RESPONSES_STREAM_HOOKS = buildProviderStreamFamilyHooks("ope
|
||||
export const OPENROUTER_THINKING_STREAM_HOOKS = buildProviderStreamFamilyHooks("openrouter-thinking");
|
||||
export const TOOL_STREAM_DEFAULT_ON_HOOKS = buildProviderStreamFamilyHooks("tool-stream-default");
|
||||
export const pluginSdkMock = true;
|
||||
${dynamicExportNames.map(genericExportStatement).join("\n")}
|
||||
|
||||
export default {
|
||||
${mockSdkExportNames.map((name) => ` ${name},`).join("\n")}
|
||||
${[...exportNames].map((name) => ` ${name},`).join("\n")}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
function mockSdkSubpathSource(exportNames) {
|
||||
return `${exportNames.map((name) => `export { ${name} } from "./index.js";`).join("\n")}
|
||||
function mockSdkSubpathSource(staticExportNames, importedExportNames, options = {}) {
|
||||
const staticNames = new Set(staticExportNames);
|
||||
const dynamicNames = [...importedExportNames].filter((name) => !staticNames.has(name));
|
||||
return `${[...staticNames].map((name) => `export { ${name} } from "./index.js";`).join("\n")}
|
||||
${dynamicNames.length > 0 ? genericMockRuntimeSource({ includeSdkRuntime: true, zod: options.zod }) : ""}
|
||||
${dynamicNames.map(genericExportStatement).join("\n")}
|
||||
export { default } from "./index.js";
|
||||
`;
|
||||
}
|
||||
|
||||
@ -71,6 +71,7 @@ export async function buildWorkspacePlan(options = {}) {
|
||||
installStepCount: allSteps.filter((step) => step.kind === "install").length,
|
||||
auditStepCount: allSteps.filter((step) => step.kind === "audit").length,
|
||||
buildStepCount: allSteps.filter((step) => step.kind === "build").length,
|
||||
pruneDevWorkspaceDependencyStepCount: allSteps.filter((step) => step.kind === "prune-dev-workspace-deps").length,
|
||||
artifactStepCount: allSteps.filter((step) => step.kind === "prepare-artifacts").length,
|
||||
captureStepCount: allSteps.filter((step) => step.kind === "capture").length,
|
||||
syntheticProbeStepCount: allSteps.filter((step) => step.kind === "synthetic-probe").length,
|
||||
@ -179,6 +180,7 @@ export function renderWorkspacePlanMarkdown(plan, options = {}) {
|
||||
["Artifact dirs", plan.summary.artifactStepCount],
|
||||
["Install steps", plan.summary.installStepCount],
|
||||
["Audit steps", plan.summary.auditStepCount],
|
||||
["Prune dev workspace dependency steps", plan.summary.pruneDevWorkspaceDependencyStepCount],
|
||||
["Build steps", plan.summary.buildStepCount],
|
||||
["Capture steps", plan.summary.captureStepCount],
|
||||
["Synthetic probe steps", plan.summary.syntheticProbeStepCount],
|
||||
@ -248,6 +250,14 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
|
||||
}
|
||||
|
||||
if (requiredCapabilities.includes("dependency-install")) {
|
||||
if (hasWorkspaceProtocolDevDependencies(packageJson)) {
|
||||
steps.push({
|
||||
kind: "prune-dev-workspace-deps",
|
||||
command: `node ${helperScript(settings, workspacePath, settings.pruneWorkspaceDevDepsScript, "prune-workspace-dev-deps-cli.js")}`,
|
||||
cwd: workspacePath,
|
||||
reason: "remove workspace: devDependencies from the isolated runtime install; the mock SDK supplies OpenClaw host imports",
|
||||
});
|
||||
}
|
||||
steps.push({
|
||||
kind: "install",
|
||||
command: installCommand(packageManager),
|
||||
@ -319,6 +329,7 @@ function workspaceSettings(options) {
|
||||
resultsRoot: repoRelative(options.resultsRoot ?? defaultWorkspacePlanOptions.resultsRoot),
|
||||
rootDir: path.resolve(options.rootDir ?? process.cwd()),
|
||||
syntheticProbeScript: options.syntheticProbeScript ?? defaultWorkspacePlanOptions.syntheticProbeScript,
|
||||
pruneWorkspaceDevDepsScript: options.pruneWorkspaceDevDepsScript,
|
||||
workspaceRoot: repoRelative(options.workspaceRoot ?? defaultWorkspacePlanOptions.workspaceRoot),
|
||||
};
|
||||
}
|
||||
@ -377,6 +388,12 @@ function hasHostLinkedOpenClawDependency(packageSummary) {
|
||||
].includes("openclaw");
|
||||
}
|
||||
|
||||
function hasWorkspaceProtocolDevDependencies(packageJson) {
|
||||
return Object.values(packageJson.devDependencies ?? {}).some(
|
||||
(value) => typeof value === "string" && value.startsWith("workspace:"),
|
||||
);
|
||||
}
|
||||
|
||||
function detectPackageManager(rootDir, packageDir, packageJson) {
|
||||
const declared = typeof packageJson.packageManager === "string" ? packageJson.packageManager.split("@")[0] : null;
|
||||
if (declared) {
|
||||
@ -458,15 +475,13 @@ function runCommand(packageManager, script) {
|
||||
}
|
||||
|
||||
function captureCommand(settings, fixtureId, entrypoint, workspacePath) {
|
||||
const loader = entrypoint.blockers.some((blocker) => blocker.code === "ts-loader-required") ? " --import tsx" : "";
|
||||
const script = helperScript(settings, workspacePath, settings.captureScript, "capture-cli.js");
|
||||
return `${settings.optInEnv} node${loader} ${script} ${entrypoint.specifier} --mock-sdk --output ${workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "capture")}`;
|
||||
return `${settings.optInEnv} node ${script} ${entrypoint.specifier} --mock-sdk --output ${workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "capture")}`;
|
||||
}
|
||||
|
||||
function syntheticProbeCommand(settings, fixtureId, entrypoint, workspacePath) {
|
||||
const loader = entrypoint.blockers.some((blocker) => blocker.code === "ts-loader-required") ? " --import tsx" : "";
|
||||
const script = helperScript(settings, workspacePath, settings.syntheticProbeScript, "synthetic-probes-cli.js");
|
||||
return `${settings.optInEnv} node${loader} ${script} --entrypoint ${entrypoint.specifier} --mock-sdk --output ${workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "synthetic")}`;
|
||||
return `${settings.optInEnv} node ${script} --entrypoint ${entrypoint.specifier} --mock-sdk --output ${workspaceArtifactPath(settings, fixtureId, entrypoint, workspacePath, "synthetic")}`;
|
||||
}
|
||||
|
||||
function helperScript(settings, workspacePath, configuredScript, helperFileName) {
|
||||
|
||||
@ -151,6 +151,7 @@ test("capture entrypoint can mock OpenClaw plugin SDK imports", async () => {
|
||||
'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";',
|
||||
"",
|
||||
@ -171,6 +172,7 @@ test("capture entrypoint can mock OpenClaw plugin SDK imports", async () => {
|
||||
"",
|
||||
"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() {} });",
|
||||
@ -192,3 +194,71 @@ test("capture entrypoint can mock OpenClaw plugin SDK imports", async () => {
|
||||
["registration:registerProvider", "registration:registerHttpRoute", "registration:registerTool"],
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
]);
|
||||
});
|
||||
|
||||
@ -40,11 +40,11 @@ test("platform probes classify loader and shell portability risks", () => {
|
||||
},
|
||||
{
|
||||
kind: "capture",
|
||||
command: "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 node --import tsx capture.mjs ./src/index.ts",
|
||||
command: "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 node capture.mjs ./src/index.ts --mock-sdk",
|
||||
},
|
||||
{
|
||||
kind: "synthetic-probe",
|
||||
command: "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 node --import tsx synthetic.mjs --entrypoint ./src/index.ts",
|
||||
command: "PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 node synthetic.mjs --entrypoint ./src/index.ts --mock-sdk",
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -124,7 +124,7 @@ test("platform probes separate executor-covered portability risks from residual
|
||||
assert.match(renderPlatformProbesMarkdown(report), /Covered Portability Findings/);
|
||||
});
|
||||
|
||||
test("platform probe validation requires jiti fallback and reflected tsx commands", () => {
|
||||
test("platform probe validation requires jiti fallback and reflected TypeScript loader commands", () => {
|
||||
const errors = validatePlatformProbes({
|
||||
mode: "plan-only",
|
||||
targets: ["linux", "macos", "windows", "container"],
|
||||
@ -137,11 +137,14 @@ test("platform probe validation requires jiti fallback and reflected tsx command
|
||||
id: "cold-import.extension:fixture:index",
|
||||
loaderPrimary: "tsx",
|
||||
captureUsesTsx: true,
|
||||
captureUsesTypeScriptLoader: true,
|
||||
syntheticUsesTsx: false,
|
||||
syntheticUsesMockSdk: false,
|
||||
syntheticUsesTypeScriptLoader: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.ok(errors.some((error) => error.includes("Jiti fallback")));
|
||||
assert.ok(errors.some((error) => error.includes("tsx loader strategy")));
|
||||
assert.ok(errors.some((error) => error.includes("TypeScript loader strategy")));
|
||||
});
|
||||
|
||||
@ -106,14 +106,14 @@ test("runtime capture records conversation binding resolved callbacks", async ()
|
||||
);
|
||||
});
|
||||
|
||||
test("runtime capture report classifies missing mocked SDK exports", async () => {
|
||||
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-missing-export-"));
|
||||
test("runtime capture report synthesizes newly imported mocked SDK exports", async () => {
|
||||
const rootDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-dynamic-export-"));
|
||||
await mkdir(path.join(rootDir, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(rootDir, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: "openclaw-missing-sdk-export",
|
||||
name: "openclaw-dynamic-sdk-export",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
openclaw: {
|
||||
@ -131,9 +131,10 @@ test("runtime capture report classifies missing mocked SDK exports", async () =>
|
||||
[
|
||||
'import { definitelyMissing } from "openclaw/plugin-sdk/plugin-entry";',
|
||||
"",
|
||||
"export default definitelyMissing({",
|
||||
" register() {},",
|
||||
"});",
|
||||
"export function register(api) {",
|
||||
" if (!definitelyMissing) throw new Error('expected dynamic mock export');",
|
||||
" api.registerTool({ name: 'fixture_tool', inputSchema: { type: 'object' }, run() {} });",
|
||||
"}",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
@ -142,17 +143,18 @@ test("runtime capture report classifies missing mocked SDK exports", async () =>
|
||||
const compatibilityReport = await inspectCompatibilityFixtureSet(config, { openclawPath: false });
|
||||
const captureReport = await buildRuntimeCaptureReport({ report: compatibilityReport, rootDir });
|
||||
|
||||
assert.equal(captureReport.summary.failedCount, 1);
|
||||
assert.equal(captureReport.results[0].status, "error");
|
||||
assert.equal(captureReport.results[0].failureClass, "missing-sdk-export");
|
||||
assert.equal(captureReport.results[0].missingExport, "definitelyMissing");
|
||||
assert.equal(captureReport.summary.failedCount, 0);
|
||||
assert.equal(captureReport.results[0].status, "captured");
|
||||
assert.deepEqual(captureReport.results[0].captured.map((entry) => `${entry.kind}:${entry.name}`), [
|
||||
"registration:registerTool",
|
||||
]);
|
||||
|
||||
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-missing-export-out-"));
|
||||
const outDir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-runtime-capture-dynamic-export-out-"));
|
||||
await writeRuntimeCaptureReport(captureReport, {
|
||||
jsonPath: path.join(outDir, "capture.json"),
|
||||
markdownPath: path.join(outDir, "capture.md"),
|
||||
});
|
||||
assert.match(await readFile(path.join(outDir, "capture.md"), "utf8"), /missing-sdk-export/);
|
||||
assert.match(await readFile(path.join(outDir, "capture.md"), "utf8"), /registerTool/);
|
||||
});
|
||||
|
||||
test("runtime capture report classifies registration execution failures", async () => {
|
||||
|
||||
@ -23,6 +23,7 @@ test("workspace plan maps blocked entrypoints to opt-in install/build/capture st
|
||||
packageManager: "npm@10.0.0",
|
||||
scripts: { build: "tsup" },
|
||||
dependencies: { "left-pad": "^1.3.0", openclaw: "^1.0.0" },
|
||||
devDependencies: { "@openclaw/plugin-sdk": "workspace:*" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@ -57,6 +58,7 @@ test("workspace plan maps blocked entrypoints to opt-in install/build/capture st
|
||||
assert.equal(plan.summary.artifactStepCount, 2);
|
||||
assert.equal(plan.summary.installStepCount, 1);
|
||||
assert.equal(plan.summary.auditStepCount, 1);
|
||||
assert.equal(plan.summary.pruneDevWorkspaceDependencyStepCount, 1);
|
||||
assert.equal(plan.summary.buildStepCount, 1);
|
||||
assert.equal(plan.summary.captureStepCount, 2);
|
||||
assert.equal(plan.summary.syntheticProbeStepCount, 2);
|
||||
@ -73,7 +75,14 @@ test("workspace plan maps blocked entrypoints to opt-in install/build/capture st
|
||||
assert.ok(entrypoint.requiredCapabilities.includes("sdk-alias-compat"));
|
||||
assert.ok(entrypoint.requiredCapabilities.includes("ts-loader"));
|
||||
assert.ok(entrypoint.steps.some((step) => step.kind === "install" && step.command === "npm install --ignore-scripts"));
|
||||
assert.ok(entrypoint.steps.some((step) => step.kind === "capture" && step.command.includes("node --import tsx capture.mjs")));
|
||||
assert.ok(
|
||||
entrypoint.steps.some(
|
||||
(step) => step.kind === "prune-dev-workspace-deps" && step.command.includes("prune-workspace-dev-deps-cli.js"),
|
||||
),
|
||||
);
|
||||
assert.ok(entrypoint.steps.some((step) => step.kind === "capture" && step.command.includes("node capture.mjs")));
|
||||
assert.ok(entrypoint.steps.some((step) => step.kind === "capture" && step.command.includes("--mock-sdk")));
|
||||
assert.ok(entrypoint.steps.every((step) => !step.command.includes("--import tsx")));
|
||||
assert.ok(entrypoint.steps.some((step) => step.kind === "synthetic-probe" && step.command.includes("synthetic.mjs")));
|
||||
const buildEntrypoint = plan.fixtures[0].entrypoints.find((item) => item.packageName === "build-fixture");
|
||||
assert.ok(buildEntrypoint);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user