import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { isAtLeast, parseSemver } from "../../../src/infra/runtime-guard.js"; import { parseMinHostVersionRequirement } from "../../../src/plugins/min-host-version.js"; import { bundledPluginFile } from "../bundled-plugin-paths.js"; type PackageManifest = { dependencies?: Record; openclaw?: { install?: { minHostVersion?: string; }; }; }; type PackageManifestContractParams = { pluginId: string; runtimeDeps?: string[]; minHostVersionBaseline?: string; }; function readJson(relativePath: string): T { const absolutePath = path.resolve(process.cwd(), relativePath); return JSON.parse(fs.readFileSync(absolutePath, "utf8")) as T; } export function describePackageManifestContract(params: PackageManifestContractParams) { const packagePath = bundledPluginFile(params.pluginId, "package.json"); describe(`${params.pluginId} package manifest contract`, () => { if (params.runtimeDeps?.length) { for (const dependencyName of params.runtimeDeps) { it(`keeps ${dependencyName} plugin-local`, () => { const rootManifest = readJson("package.json"); const pluginManifest = readJson(packagePath); const pluginSpec = pluginManifest.dependencies?.[dependencyName]; const rootSpec = rootManifest.dependencies?.[dependencyName]; expect(pluginSpec).toBeTruthy(); expect(rootSpec).toBeUndefined(); }); } } const minHostVersionBaseline = params.minHostVersionBaseline; if (minHostVersionBaseline) { it("declares a parseable minHostVersion floor at or above the baseline", () => { const baseline = parseSemver(minHostVersionBaseline); expect(baseline).not.toBeNull(); if (!baseline) { return; } const manifest = readJson(packagePath); const requirement = parseMinHostVersionRequirement( manifest.openclaw?.install?.minHostVersion ?? null, ); expect( requirement, `${packagePath} should declare openclaw.install.minHostVersion`, ).not.toBeNull(); if (!requirement) { return; } const minimum = parseSemver(requirement.minimumLabel); expect(minimum, `${packagePath} should use a parseable semver floor`).not.toBeNull(); if (!minimum) { return; } expect( isAtLeast(minimum, baseline), `${packagePath} should require at least OpenClaw ${minHostVersionBaseline}`, ).toBe(true); }); } }); }