openclaw/src/plugin-sdk/package-contract-guardrails.test.ts
Vincent Koc fbd88e2c8f
Main recovery: restore formatter and contract checks (#49570)
* Extensions: fix oxfmt drift on main

* Plugins: restore runtime barrel exports on main

* Config: restore web search compatibility types

* Telegram: align test harness with reply runtime

* Plugin SDK: fix channel config accessor generics

* CLI: remove redundant search provider casts

* Tests: restore main typecheck coverage

* Lobster: fix test import formatting

* Extensions: route bundled seams through plugin-sdk

* Tests: use extension env helper for xai

* Image generation: fix main oxfmt drift

* Config: restore latest main compatibility checks

* Plugin SDK: align guardrail tests with lint

* Telegram: type native command skill mock
2026-03-18 00:30:01 -07:00

146 lines
4.9 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { pluginSdkEntrypoints } from "./entrypoints.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const REFERENCE_SCAN_ROOTS = ["src", "extensions", "scripts", "test", "docs"] as const;
const PLUGIN_SDK_SUBPATH_PATTERN = /openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)\b/g;
function collectPluginSdkPackageExports(): string[] {
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
exports?: Record<string, unknown>;
};
const exports = packageJson.exports ?? {};
const subpaths: string[] = [];
for (const key of Object.keys(exports)) {
if (key === "./plugin-sdk") {
subpaths.push("index");
continue;
}
if (!key.startsWith("./plugin-sdk/")) {
continue;
}
subpaths.push(key.slice("./plugin-sdk/".length));
}
return subpaths.toSorted();
}
function collectPluginSdkSourceNames(): string[] {
const pluginSdkDir = resolve(REPO_ROOT, "src", "plugin-sdk");
return readdirSync(pluginSdkDir, { withFileTypes: true })
.filter(
(entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts"),
)
.map((entry) => entry.name.slice(0, -".ts".length))
.toSorted();
}
function collectTextFiles(rootRelativeDir: string): string[] {
const rootDir = resolve(REPO_ROOT, rootRelativeDir);
const files: string[] = [];
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile()) {
continue;
}
if (
/\.(?:[cm]?ts|[cm]?js|tsx|jsx|md|mdx|json)$/u.test(entry.name) &&
!entry.name.endsWith(".snap")
) {
files.push(fullPath);
}
}
}
return files;
}
function collectPluginSdkSubpathReferences() {
const references: Array<{ file: string; subpath: string }> = [];
for (const rootRelativeDir of REFERENCE_SCAN_ROOTS) {
for (const fullPath of collectTextFiles(rootRelativeDir)) {
const source = readFileSync(fullPath, "utf8");
for (const match of source.matchAll(PLUGIN_SDK_SUBPATH_PATTERN)) {
const subpath = match[1];
if (!subpath) {
continue;
}
references.push({
file: relative(REPO_ROOT, fullPath).replaceAll("\\", "/"),
subpath,
});
}
}
}
return references;
}
describe("plugin-sdk package contract guardrails", () => {
it("keeps package.json exports aligned with built plugin-sdk entrypoints", () => {
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].toSorted());
});
it("keeps repo openclaw/plugin-sdk/<name> references on exported built subpaths", () => {
const entrypoints = new Set(pluginSdkEntrypoints);
const exports = new Set(collectPluginSdkPackageExports());
const failures: string[] = [];
for (const reference of collectPluginSdkSubpathReferences()) {
const missingFrom: string[] = [];
if (!entrypoints.has(reference.subpath)) {
missingFrom.push("scripts/lib/plugin-sdk-entrypoints.json");
}
if (!exports.has(reference.subpath)) {
missingFrom.push("package.json exports");
}
if (missingFrom.length === 0) {
continue;
}
failures.push(
`${reference.file} references openclaw/plugin-sdk/${reference.subpath}, but ${reference.subpath} is missing from ${missingFrom.join(" and ")}`,
);
}
expect(failures).toEqual([]);
});
it("does not leave referenced src/plugin-sdk source names stranded outside the public contract", () => {
const exported = new Set(pluginSdkEntrypoints);
const references = collectPluginSdkSubpathReferences();
const failures: string[] = [];
for (const sourceName of collectPluginSdkSourceNames()) {
if (exported.has(sourceName) || sourceName === "compat" || sourceName === "index") {
continue;
}
const matchingRefs = references.filter((reference) => reference.subpath === sourceName);
if (matchingRefs.length === 0) {
continue;
}
failures.push(
`src/plugin-sdk/${sourceName}.ts is referenced as openclaw/plugin-sdk/${sourceName} in ${matchingRefs
.map((reference) => reference.file)
.toSorted()
.join(", ")}, but ${sourceName} is not exported as a public plugin-sdk subpath`,
);
}
expect(failures).toEqual([]);
});
});