Files
openclaw/test/scripts/bundled-plugin-build-entries.test.ts
2026-05-17 01:35:39 +08:00

208 lines
8.0 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
collectRootPackageExcludedExtensionDirs,
listBundledPluginBuildEntries,
listBundledPluginPackArtifacts,
} from "../../scripts/lib/bundled-plugin-build-entries.mjs";
import { expectNoNodeFsScans } from "../../src/test-utils/fs-scan-assertions.js";
function expectNoPrefixMatches(values: string[], prefix: string) {
expect(values.filter((value) => value.startsWith(prefix))).toEqual([]);
}
function expectSomePrefixMatch(values: string[], prefix: string) {
expect(values.filter((value) => value.startsWith(prefix))).not.toEqual([]);
}
function pickEntries(entries: Record<string, string>, keys: readonly string[]) {
return Object.fromEntries(keys.map((key) => [key, entries[key]]));
}
describe("bundled plugin build entries", () => {
const bundledChannelEntrySources = ["index.ts", "channel-entry.ts", "setup-entry.ts"];
const forEachBundledChannelEntry = (
visit: (params: { entryPath: string; entry: string; pluginId: string }) => void,
) => {
for (const dirent of fs.readdirSync("extensions", { withFileTypes: true })) {
if (!dirent.isDirectory()) {
continue;
}
for (const sourceEntry of bundledChannelEntrySources) {
const entryPath = path.join("extensions", dirent.name, sourceEntry);
if (!fs.existsSync(entryPath)) {
continue;
}
visit({
entryPath,
entry: fs.readFileSync(entryPath, "utf8"),
pluginId: dirent.name,
});
}
}
};
it("includes manifest-less runtime core support packages in dist build entries", () => {
const entries = listBundledPluginBuildEntries();
const expectedEntries = {
"extensions/image-generation-core/api": "extensions/image-generation-core/api.ts",
"extensions/image-generation-core/runtime-api":
"extensions/image-generation-core/runtime-api.ts",
"extensions/media-understanding-core/runtime-api":
"extensions/media-understanding-core/runtime-api.ts",
"extensions/speech-core/api": "extensions/speech-core/api.ts",
"extensions/speech-core/runtime-api": "extensions/speech-core/runtime-api.ts",
};
expect(pickEntries(entries, Object.keys(expectedEntries))).toStrictEqual(expectedEntries);
});
it("keeps the Matrix packaged runtime shim in bundled plugin build entries", () => {
const entries = listBundledPluginBuildEntries();
const expectedEntries = {
"extensions/matrix/plugin-entry.handlers.runtime":
"extensions/matrix/plugin-entry.handlers.runtime.ts",
};
expect(pickEntries(entries, Object.keys(expectedEntries))).toStrictEqual(expectedEntries);
});
it("keeps the Telegram ingress worker out of bundled plugin public-surface entries", () => {
const entries = listBundledPluginBuildEntries();
expect(entries["extensions/telegram/telegram-ingress-worker.runtime"]).toBeUndefined();
});
it("discovers repo plugin build entries without directory scans", () => {
const payload = expectNoNodeFsScans<{
artifacts: number;
entries: number;
}>(
`
const build = await import("./scripts/lib/bundled-plugin-build-entries.mjs");
const entries = build.listBundledPluginBuildEntries();
const artifacts = build.listBundledPluginPackArtifacts();
return {
artifacts: artifacts.length,
entries: Object.keys(entries).length,
};
`,
{ counters: ["readdirSync"] },
);
expect(payload.entries).toBeGreaterThan(0);
expect(payload.artifacts).toBeGreaterThan(0);
});
it("packs runtime core support packages without requiring plugin manifests", () => {
const artifacts = listBundledPluginPackArtifacts();
expect(artifacts).toContain("dist/extensions/image-generation-core/package.json");
expect(artifacts).toContain("dist/extensions/image-generation-core/runtime-api.js");
expect(artifacts).not.toContain("dist/extensions/image-generation-core/openclaw.plugin.json");
expect(artifacts).toContain("dist/extensions/media-understanding-core/runtime-api.js");
expect(artifacts).not.toContain(
"dist/extensions/media-understanding-core/openclaw.plugin.json",
);
expect(artifacts).toContain("dist/extensions/speech-core/runtime-api.js");
expect(artifacts).not.toContain("dist/extensions/speech-core/openclaw.plugin.json");
});
it("packs the Matrix packaged runtime shim", () => {
const artifacts = listBundledPluginPackArtifacts({ includeRootPackageExcludedDirs: true });
expect(artifacts).toContain("dist/extensions/matrix/plugin-entry.handlers.runtime.js");
});
it("keeps private QA bundles out of required npm pack artifacts", () => {
const artifacts = listBundledPluginPackArtifacts();
expectNoPrefixMatches(artifacts, "dist/extensions/qa-channel/");
expectNoPrefixMatches(artifacts, "dist/extensions/qa-lab/");
expectNoPrefixMatches(artifacts, "dist/extensions/qa-matrix/");
});
it("keeps explicitly downloadable plugins out of bundled package artifacts", () => {
const entries = listBundledPluginBuildEntries();
const artifacts = listBundledPluginPackArtifacts();
for (const pluginId of ["acpx", "googlechat", "line"]) {
expectSomePrefixMatch(Object.keys(entries), `extensions/${pluginId}/`);
expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`);
}
for (const pluginId of ["qqbot", "whatsapp"]) {
expectNoPrefixMatches(Object.keys(entries), `extensions/${pluginId}/`);
expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`);
}
});
it("keeps external-only providers out of bundled dist entries", () => {
const entries = listBundledPluginBuildEntries();
const artifacts = listBundledPluginPackArtifacts();
for (const pluginId of ["amazon-bedrock", "amazon-bedrock-mantle", "anthropic-vertex"]) {
expectNoPrefixMatches(Object.keys(entries), `extensions/${pluginId}/`);
expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`);
}
});
it("keeps externalized runtime-dependency plugins out of bundled dist entries", () => {
const entries = listBundledPluginBuildEntries();
const artifacts = listBundledPluginPackArtifacts();
for (const pluginId of ["openshell", "slack"]) {
expectNoPrefixMatches(Object.keys(entries), `extensions/${pluginId}/`);
expectNoPrefixMatches(artifacts, `dist/extensions/${pluginId}/`);
}
});
it("keeps bundled channel secret contracts on packed top-level sidecars", () => {
const artifacts = listBundledPluginPackArtifacts();
const excludedPackageDirs = collectRootPackageExcludedExtensionDirs();
const offenders: string[] = [];
const secretBackedPluginIds = new Set<string>();
forEachBundledChannelEntry(({ entryPath, entry, pluginId }) => {
if (!entry.includes('exportName: "channelSecrets"')) {
return;
}
secretBackedPluginIds.add(pluginId);
if (entry.includes("./src/secret-contract.js")) {
offenders.push(entryPath);
}
expect(entry).toContain('specifier: "./secret-contract-api.js"');
});
expect(offenders).toStrictEqual([]);
for (const pluginId of [...secretBackedPluginIds].toSorted()) {
if (excludedPackageDirs.has(pluginId)) {
continue;
}
const secretApiPath = path.join("extensions", pluginId, "secret-contract-api.ts");
expect(fs.readFileSync(secretApiPath, "utf8")).toContain("channelSecrets");
expect(artifacts).toContain(`dist/extensions/${pluginId}/secret-contract-api.js`);
}
});
it("keeps bundled channel entry metadata on packed top-level sidecars", () => {
const offenders: string[] = [];
forEachBundledChannelEntry(({ entryPath, entry }) => {
if (
!entry.includes("defineBundledChannelEntry") &&
!entry.includes("defineBundledChannelSetupEntry")
) {
return;
}
if (/specifier:\s*["']\.\/src\//u.test(entry)) {
offenders.push(entryPath);
}
});
expect(offenders).toStrictEqual([]);
});
});