mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(plugins): index install ledger source facts
This commit is contained in:
@@ -156,7 +156,7 @@ describe("installed plugin index", () => {
|
||||
origin: "global",
|
||||
rootDir: fixture.rootDir,
|
||||
enabled: true,
|
||||
sourceFacts: {
|
||||
packageInstall: {
|
||||
defaultChoice: "npm",
|
||||
npm: {
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
@@ -191,6 +191,7 @@ describe("installed plugin index", () => {
|
||||
expect(index.plugins[0]?.manifestHash).toMatch(/^[a-f0-9]{64}$/u);
|
||||
expect(index.plugins[0]?.packageJsonHash).toMatch(/^[a-f0-9]{64}$/u);
|
||||
expect(index.plugins[0]?.packageJsonPath).toBe(path.join(fixture.rootDir, "package.json"));
|
||||
expect(index.plugins[0]?.installRecord).toBeUndefined();
|
||||
|
||||
const contributions = resolveInstalledPluginContributions(index);
|
||||
expect(contributions.providers.get("demo")).toEqual(["demo"]);
|
||||
@@ -198,6 +199,98 @@ describe("installed plugin index", () => {
|
||||
expect(contributions.contracts.get("tools")).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("records the config install ledger separately from package install intent", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
|
||||
const index = loadInstalledPluginIndex({
|
||||
candidates: [fixture.candidate],
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@latest",
|
||||
installPath: "plugins/demo",
|
||||
resolvedName: "@vendor/demo-plugin",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@vendor/demo-plugin@1.2.3",
|
||||
integrity: "sha512-installed",
|
||||
shasum: "abc123",
|
||||
resolvedAt: "2026-04-25T11:00:00.000Z",
|
||||
installedAt: "2026-04-25T11:01:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
installRecord: {
|
||||
source: "npm",
|
||||
spec: "@vendor/demo-plugin@latest",
|
||||
installPath: "plugins/demo",
|
||||
resolvedName: "@vendor/demo-plugin",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@vendor/demo-plugin@1.2.3",
|
||||
integrity: "sha512-installed",
|
||||
shasum: "abc123",
|
||||
resolvedAt: "2026-04-25T11:00:00.000Z",
|
||||
installedAt: "2026-04-25T11:01:00.000Z",
|
||||
},
|
||||
packageInstall: {
|
||||
npm: {
|
||||
spec: "@vendor/demo-plugin@1.2.3",
|
||||
expectedIntegrity: "sha512-demo",
|
||||
pinState: "exact-with-integrity",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(index.plugins[0]?.installRecordHash).toMatch(/^[a-f0-9]{64}$/u);
|
||||
});
|
||||
|
||||
it("treats install ledger changes as source invalidation", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const previous = loadInstalledPluginIndex({
|
||||
candidates: [fixture.candidate],
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo-plugin",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@vendor/demo-plugin@1.2.3",
|
||||
integrity: "sha512-old",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
const current = loadInstalledPluginIndex({
|
||||
candidates: [fixture.candidate],
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo-plugin",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@vendor/demo-plugin@1.2.3",
|
||||
integrity: "sha512-new",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([
|
||||
"source-changed",
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks disabled plugins without dropping their cold contributions", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
|
||||
@@ -235,6 +328,16 @@ describe("installed plugin index", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const previous = loadInstalledPluginIndex({
|
||||
candidates: [fixture.candidate],
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedVersion: "1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: hermeticEnv({ OPENCLAW_VERSION: "2026.4.25" }),
|
||||
});
|
||||
|
||||
@@ -255,6 +358,16 @@ describe("installed plugin index", () => {
|
||||
packageVersion: "1.2.4",
|
||||
},
|
||||
],
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedVersion: "1.2.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: hermeticEnv({ OPENCLAW_VERSION: "2026.4.26" }),
|
||||
}),
|
||||
compatRegistryVersion: "different-compat-registry",
|
||||
@@ -263,6 +376,7 @@ describe("installed plugin index", () => {
|
||||
expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([
|
||||
"compat-registry-changed",
|
||||
"host-contract-changed",
|
||||
"source-changed",
|
||||
"stale-manifest",
|
||||
"stale-package",
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { resolveCompatibilityHostVersion } from "../version.js";
|
||||
import { listPluginCompatRecords, type PluginCompatCode } from "./compat/registry.js";
|
||||
import {
|
||||
@@ -43,11 +44,36 @@ export type InstalledPluginIndexContributions = {
|
||||
contracts: readonly string[];
|
||||
};
|
||||
|
||||
export type InstalledPluginInstallRecordInfo = Pick<
|
||||
PluginInstallRecord,
|
||||
| "source"
|
||||
| "spec"
|
||||
| "sourcePath"
|
||||
| "installPath"
|
||||
| "version"
|
||||
| "resolvedName"
|
||||
| "resolvedVersion"
|
||||
| "resolvedSpec"
|
||||
| "integrity"
|
||||
| "shasum"
|
||||
| "resolvedAt"
|
||||
| "installedAt"
|
||||
| "clawhubUrl"
|
||||
| "clawhubPackage"
|
||||
| "clawhubFamily"
|
||||
| "clawhubChannel"
|
||||
| "marketplaceName"
|
||||
| "marketplaceSource"
|
||||
| "marketplacePlugin"
|
||||
>;
|
||||
|
||||
export type InstalledPluginIndexRecord = {
|
||||
pluginId: string;
|
||||
packageName?: string;
|
||||
packageVersion?: string;
|
||||
sourceFacts?: PluginInstallSourceInfo;
|
||||
installRecord?: InstalledPluginInstallRecordInfo;
|
||||
installRecordHash?: string;
|
||||
packageInstall?: PluginInstallSourceInfo;
|
||||
manifestPath: string;
|
||||
manifestHash: string;
|
||||
packageJsonPath?: string;
|
||||
@@ -215,6 +241,50 @@ function describePackageInstallSource(
|
||||
});
|
||||
}
|
||||
|
||||
function setInstallStringField<Key extends keyof Omit<InstalledPluginInstallRecordInfo, "source">>(
|
||||
target: InstalledPluginInstallRecordInfo,
|
||||
key: Key,
|
||||
value: PluginInstallRecord[Key],
|
||||
): void {
|
||||
if (typeof value !== "string") {
|
||||
return;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
if (normalized) {
|
||||
target[key] = normalized as InstalledPluginInstallRecordInfo[Key];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInstallRecord(
|
||||
record: PluginInstallRecord | undefined,
|
||||
): InstalledPluginInstallRecordInfo | undefined {
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized: InstalledPluginInstallRecordInfo = {
|
||||
source: record.source,
|
||||
};
|
||||
setInstallStringField(normalized, "spec", record.spec);
|
||||
setInstallStringField(normalized, "sourcePath", record.sourcePath);
|
||||
setInstallStringField(normalized, "installPath", record.installPath);
|
||||
setInstallStringField(normalized, "version", record.version);
|
||||
setInstallStringField(normalized, "resolvedName", record.resolvedName);
|
||||
setInstallStringField(normalized, "resolvedVersion", record.resolvedVersion);
|
||||
setInstallStringField(normalized, "resolvedSpec", record.resolvedSpec);
|
||||
setInstallStringField(normalized, "integrity", record.integrity);
|
||||
setInstallStringField(normalized, "shasum", record.shasum);
|
||||
setInstallStringField(normalized, "resolvedAt", record.resolvedAt);
|
||||
setInstallStringField(normalized, "installedAt", record.installedAt);
|
||||
setInstallStringField(normalized, "clawhubUrl", record.clawhubUrl);
|
||||
setInstallStringField(normalized, "clawhubPackage", record.clawhubPackage);
|
||||
setInstallStringField(normalized, "clawhubFamily", record.clawhubFamily);
|
||||
setInstallStringField(normalized, "clawhubChannel", record.clawhubChannel);
|
||||
setInstallStringField(normalized, "marketplaceName", record.marketplaceName);
|
||||
setInstallStringField(normalized, "marketplaceSource", record.marketplaceSource);
|
||||
setInstallStringField(normalized, "marketplacePlugin", record.marketplacePlugin);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildCandidateLookup(
|
||||
candidates: readonly PluginCandidate[],
|
||||
): Map<string, PluginCandidate> {
|
||||
@@ -288,7 +358,8 @@ function buildInstalledPluginIndex(
|
||||
const plugins = registry.plugins.map((record): InstalledPluginIndexRecord => {
|
||||
const candidate = candidateByRootDir.get(record.rootDir);
|
||||
const packageJsonPath = resolvePackageJsonPath(candidate);
|
||||
const sourceFacts = describePackageInstallSource(candidate);
|
||||
const installRecord = normalizeInstallRecord(params.config?.plugins?.installs?.[record.id]);
|
||||
const packageInstall = describePackageInstallSource(candidate);
|
||||
const manifestHash =
|
||||
safeHashFile({
|
||||
filePath: record.manifestPath,
|
||||
@@ -328,8 +399,12 @@ function buildInstalledPluginIndex(
|
||||
if (candidate?.packageVersion) {
|
||||
indexRecord.packageVersion = candidate.packageVersion;
|
||||
}
|
||||
if (sourceFacts) {
|
||||
indexRecord.sourceFacts = sourceFacts;
|
||||
if (installRecord) {
|
||||
indexRecord.installRecord = installRecord;
|
||||
indexRecord.installRecordHash = hashJson(installRecord);
|
||||
}
|
||||
if (packageInstall) {
|
||||
indexRecord.packageInstall = packageInstall;
|
||||
}
|
||||
if (packageJsonPath) {
|
||||
indexRecord.packageJsonPath = packageJsonPath;
|
||||
@@ -462,7 +537,8 @@ export function diffInstalledPluginIndexInvalidationReasons(
|
||||
}
|
||||
if (
|
||||
previousPlugin.rootDir !== currentPlugin.rootDir ||
|
||||
previousPlugin.manifestPath !== currentPlugin.manifestPath
|
||||
previousPlugin.manifestPath !== currentPlugin.manifestPath ||
|
||||
previousPlugin.installRecordHash !== currentPlugin.installRecordHash
|
||||
) {
|
||||
reasons.add("source-changed");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user