mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
test(plugins): cover required openclaw peer repair
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { execFile, execFileSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { installPluginFromNpmSpec } from "./install.js";
|
||||
|
||||
type PackedVersion = {
|
||||
archive: Buffer;
|
||||
integrity: string;
|
||||
peerDependencies?: Record<string, string>;
|
||||
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
||||
shasum: string;
|
||||
tarballName: string;
|
||||
version: string;
|
||||
@@ -19,6 +22,7 @@ const tempDirs: string[] = [];
|
||||
const servers: http.Server[] = [];
|
||||
const envKeys = ["NPM_CONFIG_REGISTRY", "npm_config_registry"] as const;
|
||||
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
afterEach(async () => {
|
||||
for (const server of servers.splice(0)) {
|
||||
@@ -43,11 +47,19 @@ async function makeTempDir(label: string): Promise<string> {
|
||||
|
||||
async function packPlugin(params: {
|
||||
packageName: string;
|
||||
peerDependencies?: Record<string, string>;
|
||||
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
||||
pluginId: string;
|
||||
version: string;
|
||||
rootDir: string;
|
||||
}): Promise<PackedVersion> {
|
||||
const packageDir = path.join(params.rootDir, `package-${params.version}`);
|
||||
const packageDir = path.join(params.rootDir, `package-${params.packageName}-${params.version}`);
|
||||
const peerDependenciesMeta = params.peerDependencies
|
||||
? (params.peerDependenciesMeta ??
|
||||
Object.fromEntries(
|
||||
Object.keys(params.peerDependencies).map((name) => [name, { optional: true }]),
|
||||
))
|
||||
: undefined;
|
||||
await fs.mkdir(path.join(packageDir, "dist"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(packageDir, "package.json"),
|
||||
@@ -57,6 +69,12 @@ async function packPlugin(params: {
|
||||
version: params.version,
|
||||
type: "module",
|
||||
openclaw: { extensions: ["./dist/index.js"] },
|
||||
...(params.peerDependencies
|
||||
? {
|
||||
peerDependencies: params.peerDependencies,
|
||||
...(peerDependenciesMeta ? { peerDependenciesMeta } : {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -92,12 +110,90 @@ async function packPlugin(params: {
|
||||
return {
|
||||
archive,
|
||||
integrity: `sha512-${crypto.createHash("sha512").update(archive).digest("base64")}`,
|
||||
...(params.peerDependencies ? { peerDependencies: params.peerDependencies } : {}),
|
||||
...(peerDependenciesMeta ? { peerDependenciesMeta } : {}),
|
||||
shasum: crypto.createHash("sha1").update(archive).digest("hex"),
|
||||
tarballName,
|
||||
version: params.version,
|
||||
};
|
||||
}
|
||||
|
||||
async function startStaticRegistry(
|
||||
packages: Array<{
|
||||
latest: string;
|
||||
packageName: string;
|
||||
versions: PackedVersion[];
|
||||
}>,
|
||||
): Promise<string> {
|
||||
const packageEntries = packages.map((pkg) => ({
|
||||
...pkg,
|
||||
encodedPackageName: encodeURIComponent(pkg.packageName).replace("%40", "@"),
|
||||
versionsByVersion: new Map(pkg.versions.map((entry) => [entry.version, entry])),
|
||||
}));
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
||||
const baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405, { "content-type": "text/plain" });
|
||||
response.end("method not allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const pkg of packageEntries) {
|
||||
if (url.pathname === `/${pkg.encodedPackageName}`) {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(
|
||||
`${JSON.stringify({
|
||||
name: pkg.packageName,
|
||||
"dist-tags": { latest: pkg.latest },
|
||||
versions: Object.fromEntries(
|
||||
[...pkg.versionsByVersion.entries()].map(([version, entry]) => [
|
||||
version,
|
||||
{
|
||||
name: pkg.packageName,
|
||||
version,
|
||||
...(entry.peerDependencies ? { peerDependencies: entry.peerDependencies } : {}),
|
||||
...(entry.peerDependenciesMeta
|
||||
? { peerDependenciesMeta: entry.peerDependenciesMeta }
|
||||
: {}),
|
||||
dist: {
|
||||
integrity: entry.integrity,
|
||||
shasum: entry.shasum,
|
||||
tarball: `${baseUrl}/${pkg.encodedPackageName}/-/${entry.tarballName}`,
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
})}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tarballPrefix = `/${pkg.encodedPackageName}/-/`;
|
||||
if (url.pathname.startsWith(tarballPrefix)) {
|
||||
const entry = [...pkg.versionsByVersion.values()].find((candidate) =>
|
||||
url.pathname.endsWith(`/${candidate.tarballName}`),
|
||||
);
|
||||
if (entry) {
|
||||
response.writeHead(200, {
|
||||
"content-length": String(entry.archive.length),
|
||||
"content-type": "application/octet-stream",
|
||||
});
|
||||
response.end(entry.archive);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end(`not found: ${url.pathname}`);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
servers.push(server);
|
||||
return `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
||||
}
|
||||
|
||||
async function startMutableRegistry(params: {
|
||||
packageName: string;
|
||||
initialLatest: string;
|
||||
@@ -135,6 +231,10 @@ async function startMutableRegistry(params: {
|
||||
{
|
||||
name: params.packageName,
|
||||
version,
|
||||
...(entry.peerDependencies ? { peerDependencies: entry.peerDependencies } : {}),
|
||||
...(entry.peerDependenciesMeta
|
||||
? { peerDependenciesMeta: entry.peerDependenciesMeta }
|
||||
: {}),
|
||||
dist: {
|
||||
integrity: entry.integrity,
|
||||
shasum: entry.shasum,
|
||||
@@ -173,6 +273,119 @@ async function startMutableRegistry(params: {
|
||||
}
|
||||
|
||||
describe("installPluginFromNpmSpec e2e", () => {
|
||||
it("repairs npm-installed root openclaw for required plugin peers", async () => {
|
||||
const rootDir = await makeTempDir("npm-plugin-required-peer-e2e");
|
||||
const npmRoot = path.join(rootDir, "managed-npm");
|
||||
const packageName = `required-peer-plugin-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
||||
const versions = [
|
||||
await packPlugin({
|
||||
packageName,
|
||||
peerDependencies: { openclaw: ">=2026.0.0" },
|
||||
peerDependenciesMeta: {},
|
||||
pluginId: packageName,
|
||||
version: "1.0.0",
|
||||
rootDir,
|
||||
}),
|
||||
];
|
||||
const openClawVersions = [
|
||||
await packPlugin({
|
||||
packageName: "openclaw",
|
||||
pluginId: "registry-openclaw-copy",
|
||||
version: "2026.0.0",
|
||||
rootDir,
|
||||
}),
|
||||
];
|
||||
const registry = await startStaticRegistry([
|
||||
{ packageName, latest: "1.0.0", versions },
|
||||
{ packageName: "openclaw", latest: "2026.0.0", versions: openClawVersions },
|
||||
]);
|
||||
process.env.NPM_CONFIG_REGISTRY = registry;
|
||||
process.env.npm_config_registry = registry;
|
||||
|
||||
const rawNpmRoot = path.join(rootDir, "raw-managed-npm");
|
||||
await fs.mkdir(rawNpmRoot, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(rawNpmRoot, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
dependencies: { [packageName]: "1.0.0" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
["install", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel=error"],
|
||||
{
|
||||
cwd: rawNpmRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
NPM_CONFIG_REGISTRY: registry,
|
||||
npm_config_registry: registry,
|
||||
},
|
||||
timeout: 120_000,
|
||||
},
|
||||
);
|
||||
const rawManifest = JSON.parse(
|
||||
await fs.readFile(path.join(rawNpmRoot, "package.json"), "utf8"),
|
||||
) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
expect(rawManifest.dependencies).toEqual({ [packageName]: "1.0.0" });
|
||||
const rawLock = JSON.parse(
|
||||
await fs.readFile(path.join(rawNpmRoot, "package-lock.json"), "utf8"),
|
||||
) as {
|
||||
packages?: Record<string, unknown>;
|
||||
};
|
||||
expect(rawLock.packages?.["node_modules/openclaw"]).toMatchObject({
|
||||
peer: true,
|
||||
version: "2026.0.0",
|
||||
});
|
||||
await expect(
|
||||
fs
|
||||
.lstat(path.join(rawNpmRoot, "node_modules", "openclaw"))
|
||||
.then((stat) => stat.isDirectory()),
|
||||
).resolves.toBe(true);
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: `${packageName}@1.0.0`,
|
||||
npmDir: npmRoot,
|
||||
trustedManagedNpmRoot: true,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(npmRoot, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
expect(manifest.dependencies).toEqual({ [packageName]: "1.0.0" });
|
||||
|
||||
const lock = JSON.parse(await fs.readFile(path.join(npmRoot, "package-lock.json"), "utf8")) as {
|
||||
packages?: Record<string, unknown>;
|
||||
};
|
||||
expect(lock.packages?.[""] as { dependencies?: Record<string, string> }).toMatchObject({
|
||||
dependencies: { [packageName]: "1.0.0" },
|
||||
});
|
||||
expect(lock.packages?.["node_modules/openclaw"]).toBeUndefined();
|
||||
|
||||
await expect(fs.lstat(path.join(npmRoot, "node_modules", "openclaw"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(
|
||||
fs
|
||||
.lstat(path.join(result.targetDir, "node_modules", "openclaw"))
|
||||
.then((stat) => stat.isSymbolicLink()),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("pins a mutable npm tag to the version resolved before install", async () => {
|
||||
const rootDir = await makeTempDir("npm-plugin-e2e");
|
||||
const npmRoot = path.join(rootDir, "managed-npm");
|
||||
|
||||
Reference in New Issue
Block a user