mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
// Built plugin control-plane module checks cover native require(esm) acceptance.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
collectBuiltDoctorContractClosureViolations,
|
|
listBuiltPluginControlPlaneModules,
|
|
probeBuiltPluginControlPlaneModules,
|
|
verifyBuiltPluginControlPlaneModules,
|
|
} from "../../scripts/check-built-plugin-control-plane-modules.mts";
|
|
import {
|
|
resolveRuntimeWorkerArgv,
|
|
resolveRuntimeWorkerUrl,
|
|
} from "../../src/infra/runtime-worker-url.js";
|
|
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
|
|
import { toolingMtsEntrypoints } from "./tooling-mts-runtime.test-support.mts";
|
|
|
|
const roots: string[] = [];
|
|
const testNodeExecPath = resolveTestNodeExecPath();
|
|
|
|
function makeRoot(extension = ".js"): string {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-control-plane-"));
|
|
roots.push(rootDir);
|
|
fs.writeFileSync(path.join(rootDir, "package.json"), '{"type":"module"}\n');
|
|
if (extension === ".cjs") {
|
|
write(rootDir, "extensions/demo/openclaw.plugin.json", '{"id":"demo"}');
|
|
write(rootDir, "extensions/demo/index.ts", "export {};\n");
|
|
write(
|
|
rootDir,
|
|
"extensions/demo/package.json",
|
|
JSON.stringify({
|
|
openclaw: {
|
|
extensions: ["./index.ts"],
|
|
build: { bundledDist: false, runtimeFormat: "cjs" },
|
|
release: { publishToNpm: true },
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
return rootDir;
|
|
}
|
|
|
|
function write(rootDir: string, relativePath: string, source: string): void {
|
|
const filePath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, source);
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const rootDir of roots.splice(0)) {
|
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("built plugin control-plane module loads", () => {
|
|
it("keeps the native compiler unstarted for checker imports and runtime inventory checks", () => {
|
|
const rootDir = makeRoot();
|
|
const checkerUrl = resolveRuntimeWorkerUrl(toolingMtsEntrypoints.controlPlane);
|
|
const result = spawnSync(
|
|
testNodeExecPath,
|
|
[
|
|
...resolveRuntimeWorkerArgv(checkerUrl, testNodeExecPath).slice(0, -1),
|
|
"--input-type=module",
|
|
"--eval",
|
|
`import assert from "node:assert/strict";
|
|
import childProcess from "node:child_process";
|
|
import { syncBuiltinESMExports } from "node:module";
|
|
const spawn = childProcess.spawn;
|
|
let compilerStarts = 0;
|
|
childProcess.spawn = function(executable, args, ...rest) {
|
|
if (args?.includes("--api")) compilerStarts++;
|
|
return Reflect.apply(spawn, this, [executable, args, ...rest]);
|
|
};
|
|
syncBuiltinESMExports();
|
|
assert.equal(compilerStarts, 0, "native compiler started before the checker");
|
|
await import(${JSON.stringify(checkerUrl.href)});
|
|
assert.equal(compilerStarts, 0, "native compiler started by the checker import");
|
|
const { listCoreRuntimePostBuildOutputs } = await import(${JSON.stringify(resolveRuntimeWorkerUrl(toolingMtsEntrypoints.runtimePostbuild).href)});
|
|
listCoreRuntimePostBuildOutputs({ rootDir: ${JSON.stringify(rootDir)} });
|
|
assert.equal(compilerStarts, 0, "native compiler started by runtime postbuild inventory checks");
|
|
await import(${JSON.stringify(resolveRuntimeWorkerUrl(toolingMtsEntrypoints.runNode).href)});
|
|
assert.equal(compilerStarts, 0, "native compiler started by the development runner import");
|
|
const { API } = await import("typescript/unstable/sync");
|
|
const api = new API();
|
|
try {
|
|
api.parseConfigFile("tsconfig.json");
|
|
assert.equal(compilerStarts, 1, "the compiler process observation must detect an actual start");
|
|
} finally {
|
|
api.close();
|
|
}
|
|
`,
|
|
],
|
|
{
|
|
cwd: process.cwd(),
|
|
env: { ...process.env, NODE_OPTIONS: undefined },
|
|
encoding: "utf8",
|
|
timeout: 30_000,
|
|
},
|
|
);
|
|
expect(result.error, result.stderr).toBeUndefined();
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it.each([".js", ".cjs"])(
|
|
"lists exact %s contracts and channel legacy setup references",
|
|
(extension) => {
|
|
const rootDir = makeRoot(extension);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/doctor-contract-api${extension}`,
|
|
"export const ok = true;\n",
|
|
);
|
|
write(rootDir, `dist/extensions/demo/contract-api${extension}`, "export const ok = true;\n");
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/provider-contract-api${extension}`,
|
|
"export const ignored = true;\n",
|
|
);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/setup-entry${extension}`,
|
|
[
|
|
"const setup = {",
|
|
` legacyStateMigrations: { specifier: "./legacy-state-migrations-api${extension}" },`,
|
|
` legacySessionSurface: { specifier: "./legacy-session-surface-api${extension}" },`,
|
|
"};",
|
|
"export default setup;",
|
|
].join("\n"),
|
|
);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/legacy-state-migrations-api${extension}`,
|
|
"export {};\n",
|
|
);
|
|
write(rootDir, `dist/extensions/demo/legacy-session-surface-api${extension}`, "export {};\n");
|
|
|
|
expect(listBuiltPluginControlPlaneModules({ rootDir })).toEqual([
|
|
{
|
|
pluginId: "demo",
|
|
kind: "contract",
|
|
relativePath: `dist/extensions/demo/contract-api${extension}`,
|
|
},
|
|
{
|
|
pluginId: "demo",
|
|
kind: "doctor-contract",
|
|
relativePath: `dist/extensions/demo/doctor-contract-api${extension}`,
|
|
},
|
|
{
|
|
pluginId: "demo",
|
|
kind: "channel-legacy-session-surface",
|
|
relativePath: `dist/extensions/demo/legacy-session-surface-api${extension}`,
|
|
},
|
|
{
|
|
pluginId: "demo",
|
|
kind: "channel-legacy-state-migrations",
|
|
relativePath: `dist/extensions/demo/legacy-state-migrations-api${extension}`,
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([".js", ".cjs"])("accepts synchronously requireable %s artifacts", (extension) => {
|
|
const rootDir = makeRoot(extension);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/doctor-contract-api${extension}`,
|
|
extension === ".cjs" ? "exports.ok = true;\n" : "export const ok = true;\n",
|
|
);
|
|
|
|
expect(() => verifyBuiltPluginControlPlaneModules({ rootDir })).not.toThrow();
|
|
});
|
|
|
|
it("reports plugin, kind, path, and native require error", () => {
|
|
const rootDir = makeRoot();
|
|
write(
|
|
rootDir,
|
|
"dist/extensions/demo/doctor-contract-api.js",
|
|
"await Promise.resolve();\nexport const ok = true;\n",
|
|
);
|
|
|
|
expect(() => verifyBuiltPluginControlPlaneModules({ rootDir })).toThrow(
|
|
/demo \(doctor-contract\) dist\/extensions\/demo\/doctor-contract-api\.js:.*ERR_REQUIRE_ASYNC_MODULE/s,
|
|
);
|
|
});
|
|
|
|
it("bounds a stalled native require child", () => {
|
|
const rootDir = makeRoot();
|
|
write(rootDir, "dist/extensions/demo/doctor-contract-api.js", "while (true) {}\n");
|
|
const modules = listBuiltPluginControlPlaneModules({ rootDir });
|
|
|
|
expect(() => probeBuiltPluginControlPlaneModules(modules, { rootDir, timeoutMs: 100 })).toThrow(
|
|
/timed out|ETIMEDOUT/u,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("built doctor contract closures", () => {
|
|
it.each([".js", ".cjs"])(
|
|
"checks shared and cyclic %s chunks once while reporting each doctor contract",
|
|
(extension) => {
|
|
const rootDir = makeRoot(extension);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/doctor-contract-api${extension}`,
|
|
extension === ".cjs"
|
|
? 'module.exports = require("../../token-chunk.cjs");'
|
|
: 'export * from "../../token-chunk.js";',
|
|
);
|
|
write(
|
|
rootDir,
|
|
`dist/token-chunk${extension}`,
|
|
extension === ".cjs"
|
|
? 'module.exports = require("./exec-chunk.cjs");'
|
|
: 'export * from "./exec-chunk.js";',
|
|
);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/other/doctor-contract-api${extension}`,
|
|
extension === ".cjs"
|
|
? 'module.exports = require("../../token-chunk.cjs");'
|
|
: 'export * from "../../token-chunk.js";',
|
|
);
|
|
write(
|
|
rootDir,
|
|
`dist/exec-chunk${extension}`,
|
|
extension === ".cjs"
|
|
? 'require("./token-chunk.cjs"); const exec = require("execa"); exports.rule = exec;'
|
|
: 'import "./token-chunk.js"; import "execa"; export const rule = 1;',
|
|
);
|
|
// Both formats use the same closure check; select the synthetic second plugin explicitly.
|
|
const modules = ["demo", "other"].map((pluginId) => ({
|
|
pluginId,
|
|
kind: "doctor-contract",
|
|
relativePath: `dist/extensions/${pluginId}/doctor-contract-api${extension}`,
|
|
}));
|
|
const readFile = vi.spyOn(fs, "readFileSync");
|
|
expect(collectBuiltDoctorContractClosureViolations(modules, { rootDir })).toEqual([
|
|
{
|
|
pluginId: "demo",
|
|
kind: "doctor-contract",
|
|
relativePath: `dist/extensions/demo/doctor-contract-api${extension}`,
|
|
dependency: "execa",
|
|
importerPath: `dist/exec-chunk${extension}`,
|
|
},
|
|
{
|
|
pluginId: "other",
|
|
kind: "doctor-contract",
|
|
relativePath: `dist/extensions/other/doctor-contract-api${extension}`,
|
|
dependency: "execa",
|
|
importerPath: `dist/exec-chunk${extension}`,
|
|
},
|
|
]);
|
|
for (const chunk of ["token-chunk", "exec-chunk"]) {
|
|
expect(
|
|
readFile.mock.calls.filter(
|
|
([file]) => file === path.join(rootDir, `dist/${chunk}${extension}`),
|
|
),
|
|
).toHaveLength(1);
|
|
}
|
|
write(rootDir, `dist/exec-chunk${extension}`, "");
|
|
expect(collectBuiltDoctorContractClosureViolations(modules, { rootDir })).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it.each([".js", ".cjs"])(
|
|
"ignores lazy %s edges and non-doctor contract surfaces",
|
|
(extension) => {
|
|
const rootDir = makeRoot(extension);
|
|
// A dynamic import is never paid at enumeration time, and the general contract
|
|
// surface may legitimately spawn commands (matrix probes its SDK packages).
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/doctor-contract-api${extension}`,
|
|
extension === ".cjs"
|
|
? 'exports.load = () => require("execa");'
|
|
: 'export const load = () => import("execa");',
|
|
);
|
|
write(
|
|
rootDir,
|
|
`dist/extensions/demo/contract-api${extension}`,
|
|
extension === ".cjs"
|
|
? 'require("execa"); exports.a = 1;'
|
|
: 'import "execa"; export const a = 1;',
|
|
);
|
|
|
|
expect(
|
|
collectBuiltDoctorContractClosureViolations(
|
|
listBuiltPluginControlPlaneModules({ rootDir }),
|
|
{
|
|
rootDir,
|
|
},
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
});
|