Compare commits
1 Commits
main
...
codex/gene
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a63b92f0fc |
3
.github/workflows/check.yml
vendored
3
.github/workflows/check.yml
vendored
@ -36,6 +36,7 @@ jobs:
|
||||
- run: node scripts/run-contract-smoke.mjs --strict --openclaw ./openclaw
|
||||
- run: node scripts/inspect-fixtures.mjs --check
|
||||
- run: npm run plugin-inspector:smoke
|
||||
- run: node scripts/check-generated-surface-fixture.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/generate-report.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/capture-contracts.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/synthetic-probes.mjs --check --openclaw ./openclaw
|
||||
@ -55,6 +56,7 @@ jobs:
|
||||
node scripts/cold-import-readiness.mjs --openclaw ./openclaw
|
||||
node scripts/workspace-plan.mjs --openclaw ./openclaw
|
||||
node scripts/platform-probes.mjs --openclaw ./openclaw
|
||||
node scripts/check-generated-surface-fixture.mjs --openclaw ./openclaw
|
||||
node scripts/import-loop-profile.mjs
|
||||
node scripts/profile-contract-runtime.mjs --openclaw ./openclaw
|
||||
node scripts/compare-runtime-profile.mjs
|
||||
@ -87,6 +89,7 @@ jobs:
|
||||
- run: node scripts/sync-fixtures.mjs --check
|
||||
- run: node scripts/inspect-fixtures.mjs --check
|
||||
- run: npm run plugin-inspector:smoke
|
||||
- run: node scripts/check-generated-surface-fixture.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/generate-report.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/cold-import-readiness.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/workspace-plan.mjs --check --openclaw ./openclaw
|
||||
|
||||
2
.github/workflows/openclaw-ref-compat.yml
vendored
2
.github/workflows/openclaw-ref-compat.yml
vendored
@ -105,6 +105,7 @@ jobs:
|
||||
- run: node scripts/sync-fixtures.mjs --check
|
||||
- run: node scripts/run-contract-smoke.mjs --strict --openclaw ./openclaw
|
||||
- run: node scripts/inspect-fixtures.mjs --check
|
||||
- run: node scripts/check-generated-surface-fixture.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/generate-report.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/capture-contracts.mjs --check --openclaw ./openclaw
|
||||
- run: node scripts/synthetic-probes.mjs --check --openclaw ./openclaw
|
||||
@ -127,6 +128,7 @@ jobs:
|
||||
node scripts/cold-import-readiness.mjs --openclaw ./openclaw
|
||||
node scripts/workspace-plan.mjs --openclaw ./openclaw
|
||||
node scripts/platform-probes.mjs --openclaw ./openclaw
|
||||
node scripts/check-generated-surface-fixture.mjs --openclaw ./openclaw
|
||||
node scripts/import-loop-profile.mjs
|
||||
node scripts/profile-contract-runtime.mjs --openclaw ./openclaw --runs "${PROFILE_RUNS}"
|
||||
node scripts/compare-runtime-profile.mjs ${{ inputs.strict_perf && '--strict' || '' }}
|
||||
|
||||
1249
reports/crabpot-generated-surface.json
Normal file
1249
reports/crabpot-generated-surface.json
Normal file
File diff suppressed because it is too large
Load Diff
35
reports/crabpot-generated-surface.md
Normal file
35
reports/crabpot-generated-surface.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Crabpot Generated Surface Fixture
|
||||
|
||||
Generated: 2026-04-28T09:40:41.745Z
|
||||
Status: PASS
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Value |
|
||||
| --------------------------- | ----- |
|
||||
| Expected hooks | 34 |
|
||||
| Expected registrars | 48 |
|
||||
| Expected direct callbacks | 1 |
|
||||
| Expected SDK exports | 291 |
|
||||
| Expected manifest contracts | 17 |
|
||||
| Static hooks | 34 |
|
||||
| Static registrars | 48 |
|
||||
| Static SDK imports | 291 |
|
||||
| Static manifest contracts | 17 |
|
||||
| Runtime hooks | 34 |
|
||||
| Runtime registrars | 48 |
|
||||
| Runtime direct callbacks | 1 |
|
||||
| Missing static surface | 0 |
|
||||
| Missing runtime surface | 0 |
|
||||
|
||||
## Missing Static Surface
|
||||
|
||||
_none_
|
||||
|
||||
## Missing Runtime Surface
|
||||
|
||||
_none_
|
||||
|
||||
## Errors
|
||||
|
||||
_none_
|
||||
603
scripts/check-generated-surface-fixture.mjs
Normal file
603
scripts/check-generated-surface-fixture.mjs
Normal file
@ -0,0 +1,603 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { readManifest, repoRoot } from "./manifest-lib.mjs";
|
||||
import { loadPluginInspector, resolvePluginInspectorCliInvocation } from "./plugin-inspector-source.mjs";
|
||||
|
||||
const defaultPluginRoot = path.join(repoRoot, ".crabpot/generated-surface-plugin");
|
||||
const defaultReportJsonPath = path.join(repoRoot, "reports/crabpot-generated-surface.json");
|
||||
const defaultReportMarkdownPath = path.join(repoRoot, "reports/crabpot-generated-surface.md");
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const report = await buildGeneratedSurfaceReport(args);
|
||||
|
||||
if (args.write) {
|
||||
await writeGeneratedSurfaceReport(report, args);
|
||||
}
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
console.log(
|
||||
`generated surface: ${report.status.toUpperCase()} (${report.summary.missingStaticCount} static missing, ${report.summary.missingRuntimeCount} runtime missing)`,
|
||||
);
|
||||
}
|
||||
|
||||
if (report.errors.length > 0) {
|
||||
throw new Error(report.errors.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
json: false,
|
||||
openclawPath: undefined,
|
||||
pluginRoot: defaultPluginRoot,
|
||||
reportJsonPath: defaultReportJsonPath,
|
||||
reportMarkdownPath: defaultReportMarkdownPath,
|
||||
write: true,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--check") {
|
||||
args.write = false;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--json") {
|
||||
args.json = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--openclaw") {
|
||||
args.openclawPath = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--plugin-root") {
|
||||
args.pluginRoot = path.resolve(repoRoot, argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--report-json") {
|
||||
args.reportJsonPath = path.resolve(repoRoot, argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--report-md") {
|
||||
args.reportMarkdownPath = path.resolve(repoRoot, argv[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--write") {
|
||||
args.write = true;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export async function buildGeneratedSurfaceReport(options = {}) {
|
||||
const manifest = await readManifest();
|
||||
const pluginInspector = await loadPluginInspector();
|
||||
const targetOpenClaw = await pluginInspector.readOpenClawTargetSurface({
|
||||
configuredPath: options.openclawPath,
|
||||
manifest,
|
||||
rootDir: repoRoot,
|
||||
});
|
||||
const errors = [];
|
||||
|
||||
if (targetOpenClaw.status !== "ok") {
|
||||
errors.push(`target OpenClaw unavailable: ${targetOpenClaw.status} (${targetOpenClaw.configuredPath ?? "not configured"})`);
|
||||
return emptyReport({ targetOpenClaw, errors, options });
|
||||
}
|
||||
|
||||
const expected = await expectedSurface(targetOpenClaw);
|
||||
validateTargetSurface(expected, errors);
|
||||
|
||||
const pluginRoot = options.pluginRoot ?? defaultPluginRoot;
|
||||
await generateSurfacePlugin(pluginRoot, { expected, targetOpenClaw });
|
||||
|
||||
const staticResult = runPluginInspector(pluginRoot, { runtime: false });
|
||||
const runtimeResult = runPluginInspector(pluginRoot, { runtime: true });
|
||||
const staticReport = staticResult.status === 0
|
||||
? await readJson(path.join(pluginRoot, "reports/plugin-inspector-report.json"))
|
||||
: { fixtures: [] };
|
||||
const runtimeReport = runtimeResult.status === 0
|
||||
? await readJson(path.join(pluginRoot, "reports/plugin-inspector-runtime-capture.json"))
|
||||
: { results: [] };
|
||||
const observed = observedSurface({ staticReport, runtimeReport });
|
||||
const missing = missingSurface(expected, observed);
|
||||
|
||||
for (const failure of [...staticResult.failures, ...runtimeResult.failures]) {
|
||||
errors.push(failure);
|
||||
}
|
||||
for (const [section, values] of Object.entries(missing.static)) {
|
||||
for (const value of values) {
|
||||
errors.push(`static ${section} missing: ${value}`);
|
||||
}
|
||||
}
|
||||
for (const [section, values] of Object.entries(missing.runtime)) {
|
||||
for (const value of values) {
|
||||
errors.push(`runtime ${section} missing: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
status: errors.length === 0 ? "pass" : "fail",
|
||||
pluginRoot: path.relative(repoRoot, pluginRoot),
|
||||
targetOpenClaw: {
|
||||
status: targetOpenClaw.status,
|
||||
configuredPath: targetOpenClaw.configuredPath,
|
||||
hookNameCount: targetOpenClaw.hookNameCount,
|
||||
apiRegistrarCount: targetOpenClaw.apiRegistrarCount,
|
||||
sdkExportCount: targetOpenClaw.sdkExportCount,
|
||||
manifestContractFieldCount: targetOpenClaw.manifestContractFieldCount,
|
||||
directCallbackCount: expected.directCallbacks.length,
|
||||
},
|
||||
summary: {
|
||||
expectedHookCount: expected.hooks.length,
|
||||
expectedRegistrarCount: expected.registrars.length,
|
||||
expectedDirectCallbackCount: expected.directCallbacks.length,
|
||||
expectedSdkExportCount: expected.sdkExports.length,
|
||||
expectedManifestContractCount: expected.manifestContracts.length,
|
||||
staticHookCount: observed.static.hooks.length,
|
||||
staticRegistrarCount: observed.static.registrars.length,
|
||||
staticSdkImportCount: observed.static.sdkImports.length,
|
||||
staticManifestContractCount: observed.static.manifestContracts.length,
|
||||
runtimeHookCount: observed.runtime.hooks.length,
|
||||
runtimeRegistrarCount: observed.runtime.registrars.length,
|
||||
runtimeDirectCallbackCount: observed.runtime.directCallbacks.length,
|
||||
missingStaticCount: countMissing(missing.static),
|
||||
missingRuntimeCount: countMissing(missing.runtime),
|
||||
},
|
||||
expected,
|
||||
observed,
|
||||
missing,
|
||||
inspector: {
|
||||
static: staticResult,
|
||||
runtime: runtimeResult,
|
||||
},
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
function emptyReport({ targetOpenClaw, errors, options }) {
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
status: "fail",
|
||||
pluginRoot: path.relative(repoRoot, options.pluginRoot ?? defaultPluginRoot),
|
||||
targetOpenClaw,
|
||||
summary: {
|
||||
expectedHookCount: 0,
|
||||
expectedRegistrarCount: 0,
|
||||
expectedDirectCallbackCount: 0,
|
||||
expectedSdkExportCount: 0,
|
||||
expectedManifestContractCount: 0,
|
||||
staticHookCount: 0,
|
||||
staticRegistrarCount: 0,
|
||||
staticSdkImportCount: 0,
|
||||
staticManifestContractCount: 0,
|
||||
runtimeHookCount: 0,
|
||||
runtimeRegistrarCount: 0,
|
||||
runtimeDirectCallbackCount: 0,
|
||||
missingStaticCount: 0,
|
||||
missingRuntimeCount: 0,
|
||||
},
|
||||
expected: emptySurface(),
|
||||
observed: { static: emptySurface(), runtime: emptyRuntimeSurface() },
|
||||
missing: { static: emptySurface(), runtime: emptyRuntimeSurface() },
|
||||
inspector: { static: null, runtime: null },
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
async function expectedSurface(targetOpenClaw) {
|
||||
return {
|
||||
hooks: targetOpenClaw.hookNames ?? [],
|
||||
registrars: targetOpenClaw.apiRegistrars ?? [],
|
||||
directCallbacks: await readDirectCallbackMethods(targetOpenClaw),
|
||||
sdkExports: targetOpenClaw.sdkExports ?? [],
|
||||
manifestContracts: targetOpenClaw.manifestContractFields ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function validateTargetSurface(expected, errors) {
|
||||
for (const [section, values] of Object.entries(expected)) {
|
||||
if (section !== "directCallbacks" && (!Array.isArray(values) || values.length === 0)) {
|
||||
errors.push(`target OpenClaw has no parsed ${section}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSurfacePlugin(pluginRoot, { expected, targetOpenClaw }) {
|
||||
await rm(pluginRoot, { recursive: true, force: true });
|
||||
await mkdir(path.join(pluginRoot, "src"), { recursive: true });
|
||||
|
||||
await writeFile(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
`${JSON.stringify(packageJson(targetOpenClaw), null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(pluginRoot, "plugin-inspector.config.json"),
|
||||
`${JSON.stringify(inspectorConfig(expected), null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
`${JSON.stringify(pluginManifest(expected, targetOpenClaw), null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(path.join(pluginRoot, "src/index.js"), renderIndex(), "utf8");
|
||||
await writeFile(path.join(pluginRoot, "src/generated-hooks.js"), renderHooks(expected.hooks), "utf8");
|
||||
await writeFile(path.join(pluginRoot, "src/generated-registrars.js"), renderRegistrars(expected.registrars), "utf8");
|
||||
await writeFile(path.join(pluginRoot, "src/generated-direct-callbacks.js"), renderDirectCallbacks(expected.directCallbacks), "utf8");
|
||||
await writeFile(path.join(pluginRoot, "src/generated-sdk-imports.ts"), renderSdkImports(expected.sdkExports), "utf8");
|
||||
}
|
||||
|
||||
function packageJson(targetOpenClaw) {
|
||||
return {
|
||||
name: "crabpot-generated-openclaw-surface",
|
||||
version: "0.0.0",
|
||||
private: true,
|
||||
type: "module",
|
||||
openclaw: {
|
||||
extensions: ["./src/index.js"],
|
||||
compat: {
|
||||
pluginApi: targetOpenClaw.configuredPath ?? "target-openclaw",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function inspectorConfig(expected) {
|
||||
const expect = Object.fromEntries(
|
||||
[
|
||||
["hooks", expected.hooks],
|
||||
["registrations", expected.registrars],
|
||||
["manifestContracts", expected.manifestContracts],
|
||||
].filter(([, values]) => values.length > 0),
|
||||
);
|
||||
return {
|
||||
version: 1,
|
||||
plugin: {
|
||||
id: "crabpot-generated-openclaw-surface",
|
||||
priority: "high",
|
||||
seams: ["generated-surface", "plugin-inspector-coverage"],
|
||||
sourceRoot: ".",
|
||||
expect,
|
||||
},
|
||||
capture: {
|
||||
runtime: true,
|
||||
mockSdk: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pluginManifest(expected, targetOpenClaw) {
|
||||
return {
|
||||
id: "crabpot-generated-openclaw-surface",
|
||||
name: "Crabpot Generated OpenClaw Surface",
|
||||
version: "0.0.0",
|
||||
description: `Generated surface fixture for ${targetOpenClaw.configuredPath ?? "target OpenClaw"}.`,
|
||||
enabledByDefault: false,
|
||||
configSchema: { type: "object", additionalProperties: true },
|
||||
contracts: Object.fromEntries(
|
||||
expected.manifestContracts.map((field) => [field, [`crabpot-generated-${kebab(field)}`]]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function renderIndex() {
|
||||
return `import { registerGeneratedHooks } from "./generated-hooks.js";
|
||||
import { registerGeneratedRegistrars } from "./generated-registrars.js";
|
||||
import { registerGeneratedDirectCallbacks } from "./generated-direct-callbacks.js";
|
||||
|
||||
export function register(api) {
|
||||
registerGeneratedHooks(api);
|
||||
registerGeneratedRegistrars(api);
|
||||
registerGeneratedDirectCallbacks(api);
|
||||
}
|
||||
|
||||
export default { register };
|
||||
`;
|
||||
}
|
||||
|
||||
function renderHooks(hooks) {
|
||||
return `// Generated by Crabpot. Do not edit by hand.
|
||||
export function registerGeneratedHooks(api) {
|
||||
${hooks.map((hook) => ` api.on(${JSON.stringify(hook)}, async () => ({ ok: true, hook: ${JSON.stringify(hook)} }));`).join("\n")}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDirectCallbacks(directCallbacks) {
|
||||
return `// Generated by Crabpot. Do not edit by hand.
|
||||
export function registerGeneratedDirectCallbacks(api) {
|
||||
${directCallbacks.map((callback) => ` api.${callback}(async () => ({ ok: true, callback: ${JSON.stringify(callback)} }));`).join("\n")}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRegistrars(registrars) {
|
||||
return `// Generated by Crabpot. Do not edit by hand.
|
||||
export function registerGeneratedRegistrars(api) {
|
||||
${registrars.map((registrar) => ` api.${registrar}(payloadFor(${JSON.stringify(registrar)}));`).join("\n")}
|
||||
}
|
||||
|
||||
function payloadFor(registrar) {
|
||||
const id = "crabpot-generated-" + kebab(registrar.replace(/^register/, "") || "registration");
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
command: id,
|
||||
description: "Generated Crabpot probe for " + registrar,
|
||||
method: "POST",
|
||||
path: "/" + id,
|
||||
inputSchema: objectSchema(),
|
||||
schema: objectSchema(),
|
||||
configSchema: objectSchema(),
|
||||
handler: async () => ({ ok: true, registrar }),
|
||||
run: async () => ({ ok: true, registrar }),
|
||||
execute: async () => ({ ok: true, registrar }),
|
||||
start: async () => undefined,
|
||||
stop: async () => undefined,
|
||||
setup: async () => ({ ok: true, registrar }),
|
||||
probe: async () => ({ enabled: false, reason: "generated fixture" }),
|
||||
render: async () => "generated fixture",
|
||||
create: async () => ({ ok: true, registrar }),
|
||||
transform: async (value) => value,
|
||||
migrate: async (value) => value,
|
||||
speak: async () => ({ audio: new Uint8Array(), mimeType: "audio/wav" }),
|
||||
synthesize: async () => ({ audio: new Uint8Array(), mimeType: "audio/wav" }),
|
||||
send: async () => ({ ok: true }),
|
||||
receive: async () => ({ ok: true }),
|
||||
};
|
||||
}
|
||||
|
||||
function objectSchema() {
|
||||
return { type: "object", additionalProperties: true };
|
||||
}
|
||||
|
||||
function kebab(value) {
|
||||
return String(value).replace(/[A-Z]/g, (letter, index) => (index === 0 ? "" : "-") + letter.toLowerCase()).replace(/^-/, "");
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSdkImports(sdkExports) {
|
||||
return `// Generated by Crabpot. Do not edit by hand.
|
||||
${sdkExports.map((specifier, index) => `import type * as sdk${index} from ${JSON.stringify(specifier)};`).join("\n")}
|
||||
|
||||
export type CrabpotGeneratedSdkSurface =
|
||||
${sdkExports.map((_, index) => ` | typeof sdk${index}`).join("\n")};
|
||||
`;
|
||||
}
|
||||
|
||||
function runPluginInspector(pluginRoot, { runtime }) {
|
||||
const invocation = resolvePluginInspectorCliInvocation();
|
||||
const commandArgs = [
|
||||
...invocation.args,
|
||||
"check",
|
||||
"--config",
|
||||
"plugin-inspector.config.json",
|
||||
"--no-openclaw",
|
||||
"--out",
|
||||
"reports",
|
||||
...(runtime ? ["--runtime", "--mock-sdk"] : ["--no-runtime"]),
|
||||
];
|
||||
const result = spawnSync(invocation.command, commandArgs, {
|
||||
cwd: pluginRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
...(runtime ? { PLUGIN_INSPECTOR_EXECUTE_ISOLATED: "1" } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
command: displayCommand(invocation.command, commandArgs),
|
||||
status: result.status,
|
||||
stdout: normalizeOutputPaths(result.stdout),
|
||||
stderr: normalizeOutputPaths(result.stderr),
|
||||
failures: result.status === 0
|
||||
? []
|
||||
: [`plugin-inspector ${runtime ? "runtime" : "static"} failed with ${result.status}: ${normalizeOutputPaths(result.stderr || result.stdout)}`],
|
||||
};
|
||||
}
|
||||
|
||||
function displayCommand(command, commandArgs) {
|
||||
const displayName = command === process.execPath ? "node" : normalizeOutputPaths(command);
|
||||
return [displayName, ...commandArgs.map((arg) => normalizeOutputPaths(arg))].join(" ");
|
||||
}
|
||||
|
||||
function normalizeOutputPaths(value) {
|
||||
const workspaceParent = path.dirname(repoRoot);
|
||||
return String(value ?? "")
|
||||
.split(repoRoot)
|
||||
.join(".")
|
||||
.split(workspaceParent)
|
||||
.join("..");
|
||||
}
|
||||
|
||||
function observedSurface({ staticReport, runtimeReport }) {
|
||||
const fixture = staticReport.fixtures?.[0] ?? {};
|
||||
const captured = runtimeReport.results?.flatMap((result) => result.captured ?? []) ?? [];
|
||||
return {
|
||||
static: {
|
||||
hooks: unique(fixture.hooks ?? []),
|
||||
registrars: unique(fixture.registrations ?? []),
|
||||
sdkExports: unique((fixture.sdkImports ?? []).map((item) => item.specifier ?? item).filter(Boolean)),
|
||||
sdkImports: unique((fixture.sdkImports ?? []).map((item) => item.specifier ?? item).filter(Boolean)),
|
||||
manifestContracts: unique(fixture.manifestContracts ?? []),
|
||||
},
|
||||
runtime: {
|
||||
hooks: unique(captured.filter((item) => item.kind === "hook" && !/^on[A-Z]/.test(item.name ?? "")).map((item) => item.name)),
|
||||
registrars: unique(captured.filter((item) => item.kind === "registration").map((item) => item.name)),
|
||||
directCallbacks: unique(captured.filter((item) => item.kind === "hook" && /^on[A-Z]/.test(item.name ?? "")).map((item) => item.name)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function missingSurface(expected, observed) {
|
||||
return {
|
||||
static: {
|
||||
hooks: expected.hooks.filter((item) => !observed.static.hooks.includes(item)),
|
||||
registrars: expected.registrars.filter((item) => !observed.static.registrars.includes(item)),
|
||||
sdkExports: expected.sdkExports.filter((item) => !observed.static.sdkExports.includes(item)),
|
||||
manifestContracts: expected.manifestContracts.filter((item) => !observed.static.manifestContracts.includes(item)),
|
||||
},
|
||||
runtime: {
|
||||
hooks: expected.hooks.filter((item) => !observed.runtime.hooks.includes(item)),
|
||||
registrars: expected.registrars.filter((item) => !observed.runtime.registrars.includes(item)),
|
||||
directCallbacks: expected.directCallbacks.filter((item) => !observed.runtime.directCallbacks.includes(item)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function writeGeneratedSurfaceReport(report, options = {}) {
|
||||
await mkdir(path.dirname(options.reportJsonPath ?? defaultReportJsonPath), { recursive: true });
|
||||
await mkdir(path.dirname(options.reportMarkdownPath ?? defaultReportMarkdownPath), { recursive: true });
|
||||
await writeFile(options.reportJsonPath ?? defaultReportJsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
await writeFile(options.reportMarkdownPath ?? defaultReportMarkdownPath, `${renderMarkdown(report)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function renderMarkdown(report) {
|
||||
return [
|
||||
"# Crabpot Generated Surface Fixture",
|
||||
"",
|
||||
`Generated: ${report.generatedAt}`,
|
||||
`Status: ${report.status.toUpperCase()}`,
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
markdownTable(
|
||||
[
|
||||
["Expected hooks", report.summary.expectedHookCount],
|
||||
["Expected registrars", report.summary.expectedRegistrarCount],
|
||||
["Expected direct callbacks", report.summary.expectedDirectCallbackCount],
|
||||
["Expected SDK exports", report.summary.expectedSdkExportCount],
|
||||
["Expected manifest contracts", report.summary.expectedManifestContractCount],
|
||||
["Static hooks", report.summary.staticHookCount],
|
||||
["Static registrars", report.summary.staticRegistrarCount],
|
||||
["Static SDK imports", report.summary.staticSdkImportCount],
|
||||
["Static manifest contracts", report.summary.staticManifestContractCount],
|
||||
["Runtime hooks", report.summary.runtimeHookCount],
|
||||
["Runtime registrars", report.summary.runtimeRegistrarCount],
|
||||
["Runtime direct callbacks", report.summary.runtimeDirectCallbackCount],
|
||||
["Missing static surface", report.summary.missingStaticCount],
|
||||
["Missing runtime surface", report.summary.missingRuntimeCount],
|
||||
],
|
||||
["Metric", "Value"],
|
||||
),
|
||||
"",
|
||||
"## Missing Static Surface",
|
||||
"",
|
||||
missingMarkdown(report.missing.static),
|
||||
"",
|
||||
"## Missing Runtime Surface",
|
||||
"",
|
||||
missingMarkdown(report.missing.runtime),
|
||||
"",
|
||||
"## Errors",
|
||||
"",
|
||||
report.errors.length > 0 ? report.errors.map((error) => `- ${error}`).join("\n") : "_none_",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function missingMarkdown(surface) {
|
||||
const rows = Object.entries(surface).flatMap(([section, values]) => values.map((value) => [section, value]));
|
||||
return markdownTable(rows, ["Section", "Value"]);
|
||||
}
|
||||
|
||||
function markdownTable(rows, headers) {
|
||||
if (rows.length === 0) {
|
||||
return "_none_";
|
||||
}
|
||||
const allRows = [headers, ...rows.map((row) => row.map(String))];
|
||||
const widths = headers.map((_, index) => Math.max(...allRows.map((row) => row[index].length)));
|
||||
const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
|
||||
return [
|
||||
renderRow(headers),
|
||||
renderRow(widths.map((width) => "-".repeat(width))),
|
||||
...rows.map((row) => renderRow(row.map(String))),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function readJson(filePath) {
|
||||
return JSON.parse(await readFile(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function emptySurface() {
|
||||
return {
|
||||
hooks: [],
|
||||
registrars: [],
|
||||
sdkExports: [],
|
||||
manifestContracts: [],
|
||||
directCallbacks: [],
|
||||
};
|
||||
}
|
||||
|
||||
function emptyRuntimeSurface() {
|
||||
return {
|
||||
hooks: [],
|
||||
registrars: [],
|
||||
directCallbacks: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function readDirectCallbackMethods(targetOpenClaw) {
|
||||
if (!targetOpenClaw.configuredPath) {
|
||||
return [];
|
||||
}
|
||||
const typesPath = path.resolve(repoRoot, targetOpenClaw.configuredPath, "src/plugins/types.ts");
|
||||
if (!existsSync(typesPath)) {
|
||||
return [];
|
||||
}
|
||||
const source = await readFile(typesPath, "utf8");
|
||||
return parseDirectCallbackMethods(source);
|
||||
}
|
||||
|
||||
function parseDirectCallbackMethods(source) {
|
||||
const body = readExportedTypeBody(source, "OpenClawPluginApi");
|
||||
if (!body) {
|
||||
return [];
|
||||
}
|
||||
return unique(
|
||||
[...body.matchAll(/^\s*([A-Za-z][A-Za-z0-9]*)\??:\s*(?:<[^;]+>\s*)?\(/gm)]
|
||||
.map((match) => match[1])
|
||||
.filter((name) => /^on[A-Z]/.test(name)),
|
||||
).sort();
|
||||
}
|
||||
|
||||
function readExportedTypeBody(source, typeName) {
|
||||
const marker = `export type ${typeName} = {`;
|
||||
const start = source.indexOf(marker);
|
||||
if (start === -1) {
|
||||
return "";
|
||||
}
|
||||
const bodyStart = start + marker.length;
|
||||
const end = source.indexOf("\n};", bodyStart);
|
||||
return end === -1 ? "" : source.slice(bodyStart, end);
|
||||
}
|
||||
|
||||
function countMissing(surface) {
|
||||
return Object.values(surface).reduce((count, values) => count + values.length, 0);
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values)].sort();
|
||||
}
|
||||
|
||||
function kebab(value) {
|
||||
return String(value).replace(/[A-Z]/g, (letter, index) => (index === 0 ? "" : "-") + letter.toLowerCase()).replace(/^-/, "");
|
||||
}
|
||||
@ -4,18 +4,24 @@ import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { repoRoot } from "./manifest-lib.mjs";
|
||||
|
||||
export const pluginInspectorRef = "85f360548e9064e7d1799fc84e08501a1176ee34";
|
||||
export const pluginInspectorPackage = "@openclaw/plugin-inspector@0.3.1";
|
||||
export const pluginInspectorRef = "98e5d77981c5360ead214ed3e8d4eb45175e5141";
|
||||
export const pluginInspectorPackage = "@openclaw/plugin-inspector@0.3.2";
|
||||
|
||||
export async function loadPluginInspector() {
|
||||
return import(pathToFileURL(resolvePluginInspectorSourcePath()).href);
|
||||
const publicApi = await import(pathToFileURL(resolvePluginInspectorSourcePath()).href);
|
||||
if (typeof publicApi.inspectPlugin === "function" && typeof publicApi.inspectSourceText === "function") {
|
||||
return publicApi;
|
||||
}
|
||||
|
||||
const advancedApiPath = path.join(resolvePluginInspectorRoot(), "src", "advanced.js");
|
||||
return import(pathToFileURL(advancedApiPath).href);
|
||||
}
|
||||
|
||||
export async function loadPluginInspectorPublicApi() {
|
||||
return import(pathToFileURL(resolvePluginInspectorPublicApiPath()).href);
|
||||
}
|
||||
|
||||
export function resolvePluginInspectorCliInvocation() {
|
||||
export function resolvePluginInspectorCliInvocation(options = {}) {
|
||||
if (process.env.CRABPOT_PLUGIN_INSPECTOR_BIN) {
|
||||
return {
|
||||
command: process.env.CRABPOT_PLUGIN_INSPECTOR_BIN,
|
||||
@ -23,7 +29,8 @@ export function resolvePluginInspectorCliInvocation() {
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.CRABPOT_PLUGIN_INSPECTOR_CLI !== "source") {
|
||||
const useSource = options.preferSource || process.env.CRABPOT_PLUGIN_INSPECTOR_CLI === "source";
|
||||
if (!useSource) {
|
||||
return {
|
||||
command: npmCommand(),
|
||||
args: ["exec", "--yes", "--package", pluginInspectorPackage, "--", "plugin-inspector"],
|
||||
|
||||
@ -17,6 +17,7 @@ test("manual OpenClaw ref workflow accepts branch tag or SHA inputs", async () =
|
||||
assert.match(workflow, /ref: \$\{\{ env\.TARGET_REF \}\}/);
|
||||
assert.match(workflow, /node scripts\/check-contract-coverage\.mjs --openclaw \.\/openclaw/);
|
||||
assert.match(workflow, /node scripts\/platform-probes\.mjs --check --openclaw \.\/openclaw/);
|
||||
assert.match(workflow, /node scripts\/check-generated-surface-fixture\.mjs --check --openclaw \.\/openclaw/);
|
||||
assert.match(workflow, /node scripts\/import-loop-profile\.mjs --check/);
|
||||
});
|
||||
|
||||
@ -50,6 +51,7 @@ test("default check workflow uploads policy and summary reports", async () => {
|
||||
assert.match(workflow, /node scripts\/platform-probes\.mjs/);
|
||||
assert.match(workflow, /node scripts\/import-loop-profile\.mjs/);
|
||||
assert.match(workflow, /npm run plugin-inspector:smoke/);
|
||||
assert.match(workflow, /node scripts\/check-generated-surface-fixture\.mjs --check --openclaw \.\/openclaw/);
|
||||
assert.match(workflow, /node scripts\/check-ci-policy\.mjs/);
|
||||
assert.match(workflow, /node scripts\/write-ci-summary\.mjs/);
|
||||
assert.match(workflow, /node scripts\/update-readme-summary\.mjs/);
|
||||
|
||||
77
test/generated-surface-fixture.test.mjs
Normal file
77
test/generated-surface-fixture.test.mjs
Normal file
@ -0,0 +1,77 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { buildGeneratedSurfaceReport } from "../scripts/check-generated-surface-fixture.mjs";
|
||||
|
||||
test("generated surface fixture verifies target OpenClaw surface", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "crabpot-generated-surface-"));
|
||||
const openclawRoot = path.join(tempDir, "openclaw");
|
||||
const pluginRoot = path.join(tempDir, "generated-plugin");
|
||||
await mkdir(path.join(openclawRoot, "src/plugins/compat"), { recursive: true });
|
||||
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "openclaw",
|
||||
exports: {
|
||||
"./plugin-sdk": "./dist/plugin-sdk/index.js",
|
||||
"./plugin-sdk/runtime": "./dist/plugin-sdk/runtime.js",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "src/plugins/compat/registry.ts"),
|
||||
'export const records = [{ code: "fixture-record", status: "active" }];\n',
|
||||
);
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "src/plugins/hook-types.ts"),
|
||||
'export const PLUGIN_HOOK_NAMES = ["before_tool_call", "llm_input"] as const;\n',
|
||||
);
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "src/plugins/api-builder.ts"),
|
||||
"export function buildPluginApi() { return { registerTool() {}, registerService() {} }; }\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "src/plugins/types.ts"),
|
||||
[
|
||||
"export type OpenClawPluginApi = {",
|
||||
" registerTool: (tool: unknown) => void;",
|
||||
" onConversationBindingResolved: (handler: () => void) => void;",
|
||||
" on: (name: string, handler: () => void) => void;",
|
||||
"};",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "src/plugins/captured-registration.ts"),
|
||||
"export const captured = { registerTool() {}, registerService() {} };\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(openclawRoot, "src/plugins/manifest.ts"),
|
||||
"export type PluginManifest = {\n id: string;\n contracts?: PluginManifestContracts;\n};\nexport type PluginManifestContracts = {\n tools?: string[];\n speechProviders?: string[];\n};\n",
|
||||
);
|
||||
|
||||
try {
|
||||
const report = await buildGeneratedSurfaceReport({
|
||||
openclawPath: openclawRoot,
|
||||
pluginRoot,
|
||||
});
|
||||
|
||||
assert.equal(report.status, "pass");
|
||||
assert.deepEqual(report.expected.hooks, ["before_tool_call", "llm_input"]);
|
||||
assert.deepEqual(report.expected.registrars, ["registerService", "registerTool"]);
|
||||
assert.deepEqual(report.expected.directCallbacks, ["onConversationBindingResolved"]);
|
||||
assert.deepEqual(report.expected.sdkExports, ["openclaw/plugin-sdk", "openclaw/plugin-sdk/runtime"]);
|
||||
assert.deepEqual(report.expected.manifestContracts, ["speechProviders", "tools"]);
|
||||
assert.equal(report.summary.missingStaticCount, 0);
|
||||
assert.equal(report.summary.missingRuntimeCount, 0);
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user