mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 01:51:39 -06:00
fix(plugins): align registry freshness inspection (#125048)
This commit is contained in:
committed by
GitHub
parent
e3595c50ad
commit
fecb223e5b
@@ -14,11 +14,9 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { inspectPersistedInstalledPluginIndexInstallRecordsSync } from "../../../plugins/installed-plugin-index-record-state.js";
|
||||
import { loadInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
readPersistedInstalledPluginIndexSync,
|
||||
resolveInstalledPluginIndexStorePath,
|
||||
writePersistedInstalledPluginIndex,
|
||||
type InstalledPluginIndexStoreInspection,
|
||||
type InstalledPluginIndexStoreOptions,
|
||||
} from "../../../plugins/installed-plugin-index-store.js";
|
||||
import {
|
||||
@@ -58,7 +56,6 @@ type PluginRegistryInstallMigrationResult =
|
||||
status: "migrated";
|
||||
migrated: true;
|
||||
preflight: PluginRegistryInstallMigrationPreflight;
|
||||
inspection: InstalledPluginIndexStoreInspection;
|
||||
current: InstalledPluginIndex;
|
||||
};
|
||||
|
||||
@@ -321,7 +318,6 @@ export async function migratePluginRegistryForInstall(
|
||||
config,
|
||||
installRecords,
|
||||
};
|
||||
const inspection = await inspectPersistedInstalledPluginIndex(migrationParams);
|
||||
const candidateIndex = loadInstalledPluginIndex({
|
||||
...migrationParams,
|
||||
});
|
||||
@@ -341,7 +337,6 @@ export async function migratePluginRegistryForInstall(
|
||||
status: "migrated",
|
||||
migrated: true,
|
||||
preflight,
|
||||
inspection,
|
||||
current,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ export function diffInstalledPluginIndexInvalidationReasons(
|
||||
if (
|
||||
previousPlugin.rootDir !== currentPlugin.rootDir ||
|
||||
previousPlugin.manifestPath !== currentPlugin.manifestPath ||
|
||||
previousPlugin.source !== currentPlugin.source ||
|
||||
previousPlugin.setupSource !== currentPlugin.setupSource ||
|
||||
resolveInstalledPluginIndexInstallOwner(previousPlugin) !==
|
||||
resolveInstalledPluginIndexInstallOwner(currentPlugin) ||
|
||||
isInstalledPluginIndexInstallOwnerAmbiguous(previousPlugin) !==
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
writePersistedInstalledPluginIndexInstallRecordsWithLease,
|
||||
} from "./installed-plugin-index-records.js";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
readPersistedInstalledPluginIndex,
|
||||
refreshPersistedInstalledPluginIndex,
|
||||
resolveInstalledPluginIndexStorePath,
|
||||
@@ -669,13 +668,12 @@ describe("installed plugin index persistence", () => {
|
||||
};
|
||||
await writePersistedInstalledPluginIndex(legacy, { stateDir });
|
||||
|
||||
const inspection = await inspectPersistedInstalledPluginIndex({
|
||||
const inspection = loadPluginRegistrySnapshotWithMetadata({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env,
|
||||
});
|
||||
expect(inspection.state).toBe("stale");
|
||||
expect(inspection.refreshReasons).toEqual(["migration"]);
|
||||
expect(inspection.source).toBe("derived");
|
||||
|
||||
const refreshed = await refreshPersistedInstalledPluginIndex({
|
||||
reason: "policy-changed",
|
||||
@@ -735,85 +733,6 @@ describe("installed plugin index persistence", () => {
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("inspects missing, fresh, and stale persisted index state without loading runtime", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const candidate = createCandidate(pluginDir);
|
||||
const env = {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
};
|
||||
|
||||
const missingInspect = await inspectPersistedInstalledPluginIndex({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env,
|
||||
});
|
||||
expect(missingInspect.state).toBe("missing");
|
||||
expect(missingInspect.refreshReasons).toEqual(["missing"]);
|
||||
expect(missingInspect.persisted).toBeNull();
|
||||
expectPluginIds(missingInspect.current, ["demo"]);
|
||||
|
||||
const current = await refreshPersistedInstalledPluginIndex({
|
||||
reason: "manual",
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env,
|
||||
});
|
||||
|
||||
const freshInspect = await inspectPersistedInstalledPluginIndex({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env,
|
||||
});
|
||||
expect(freshInspect.state).toBe("fresh");
|
||||
expect(freshInspect.refreshReasons).toEqual([]);
|
||||
expect(freshInspect.persisted).toEqual(current);
|
||||
expectPluginFields(freshInspect.current, "demo", { enabled: true });
|
||||
|
||||
const policyInspect = await inspectPersistedInstalledPluginIndex({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env,
|
||||
});
|
||||
expect(policyInspect.state).toBe("stale");
|
||||
expect(policyInspect.refreshReasons).toEqual(["policy-changed"]);
|
||||
expect(policyInspect.persisted).toEqual(current);
|
||||
expectPluginFields(policyInspect.current, "demo", { enabled: false });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["demo", "demo-next"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const staleManifestInspect = await inspectPersistedInstalledPluginIndex({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env,
|
||||
});
|
||||
expect(staleManifestInspect.state).toBe("stale");
|
||||
expect(staleManifestInspect.refreshReasons).toEqual(["stale-manifest"]);
|
||||
expect(staleManifestInspect.persisted).toEqual(current);
|
||||
expectPluginIds(staleManifestInspect.current, ["demo"]);
|
||||
});
|
||||
|
||||
it("refreshes and persists a rebuilt index without loading plugin runtime", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
|
||||
@@ -33,19 +33,15 @@ import {
|
||||
type InstalledPluginIndexStoreOptions,
|
||||
} from "./installed-plugin-index-store-path.js";
|
||||
import {
|
||||
diffInstalledPluginIndexInvalidationReasons,
|
||||
extractPluginInstallRecordsFromInstalledPluginIndex,
|
||||
hasInstalledPluginIndexWorkspaceScopeMismatch,
|
||||
hasMissingConfigPathActivationMetadata,
|
||||
INSTALLED_PLUGIN_INDEX_WARNING,
|
||||
INSTALLED_PLUGIN_INDEX_VERSION,
|
||||
INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION,
|
||||
loadInstalledPluginIndex,
|
||||
resolveInstalledPluginIndexPolicyHash,
|
||||
refreshInstalledPluginIndex,
|
||||
type InstalledPluginIndex,
|
||||
type InstalledPluginIndexRefreshReason,
|
||||
type LoadInstalledPluginIndexParams,
|
||||
type RefreshInstalledPluginIndexParams,
|
||||
} from "./installed-plugin-index.js";
|
||||
import { hasMissingInstalledPluginOwnerMetadata } from "./installed-plugin-package-ownership.js";
|
||||
@@ -56,16 +52,6 @@ export {
|
||||
type InstalledPluginIndexStoreOptions,
|
||||
} from "./installed-plugin-index-store-path.js";
|
||||
|
||||
/** Freshness state for the persisted installed plugin index. */
|
||||
type InstalledPluginIndexStoreState = "missing" | "fresh" | "stale";
|
||||
|
||||
export type InstalledPluginIndexStoreInspection = {
|
||||
state: InstalledPluginIndexStoreState;
|
||||
refreshReasons: readonly InstalledPluginIndexRefreshReason[];
|
||||
persisted: InstalledPluginIndex | null;
|
||||
current: InstalledPluginIndex;
|
||||
};
|
||||
|
||||
export type InstalledPluginIndexWriteLease = {
|
||||
assertOwnedInTransaction(database: DatabaseSync): void;
|
||||
};
|
||||
@@ -564,33 +550,6 @@ function refreshPersistedPolicyState(
|
||||
};
|
||||
}
|
||||
|
||||
export async function inspectPersistedInstalledPluginIndex(
|
||||
params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {},
|
||||
): Promise<InstalledPluginIndexStoreInspection> {
|
||||
const persisted = await readPersistedInstalledPluginIndex(params);
|
||||
const current = loadInstalledPluginIndex({
|
||||
...params,
|
||||
installRecords:
|
||||
params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted),
|
||||
});
|
||||
if (!persisted) {
|
||||
return {
|
||||
state: "missing",
|
||||
refreshReasons: ["missing"],
|
||||
persisted: null,
|
||||
current,
|
||||
};
|
||||
}
|
||||
|
||||
const refreshReasons = diffInstalledPluginIndexInvalidationReasons(persisted, current);
|
||||
return {
|
||||
state: refreshReasons.length > 0 ? "stale" : "fresh",
|
||||
refreshReasons,
|
||||
persisted,
|
||||
current,
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshPersistedInstalledPluginIndex(
|
||||
params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions,
|
||||
): Promise<InstalledPluginIndex> {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import {
|
||||
readPersistedInstalledPluginIndex,
|
||||
refreshPersistedInstalledPluginIndex,
|
||||
writePersistedInstalledPluginIndex,
|
||||
} from "./installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
|
||||
import {
|
||||
inspectPluginRegistry,
|
||||
loadPluginRegistrySnapshotWithMetadata,
|
||||
refreshPluginRegistry,
|
||||
} from "./plugin-registry.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
function makeTempDir(): string {
|
||||
return makeTrackedTempDir("openclaw-plugin-registry-inspection", tempDirs);
|
||||
}
|
||||
|
||||
function hermeticEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
};
|
||||
}
|
||||
|
||||
function createCandidate(rootDir: string): PluginCandidate {
|
||||
const source = path.join(rootDir, "index.ts");
|
||||
fs.writeFileSync(source, "export default { register() {} };\n", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({ id: "demo", name: "Demo", configSchema: { type: "object" } }),
|
||||
"utf8",
|
||||
);
|
||||
return { idHint: "demo", source, rootDir, origin: "global" };
|
||||
}
|
||||
|
||||
function createEmptyIndex(stateDir: string): InstalledPluginIndex {
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin registry inspection", () => {
|
||||
it("derives without persisted install records when persisted reads are disabled", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = makeTempDir();
|
||||
const candidate = createCandidate(pluginDir);
|
||||
await writePersistedInstalledPluginIndex(createEmptyIndex(stateDir), { stateDir });
|
||||
|
||||
const result = loadPluginRegistrySnapshotWithMetadata({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env: hermeticEnv(),
|
||||
preferPersisted: false,
|
||||
});
|
||||
|
||||
expect(result.source).toBe("derived");
|
||||
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
|
||||
expect(result.snapshot.installRecords).not.toHaveProperty("missing");
|
||||
});
|
||||
|
||||
it("reports missing, fresh, policy, and manifest freshness from the snapshot selector", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = makeTempDir();
|
||||
const candidate = createCandidate(pluginDir);
|
||||
const env = hermeticEnv();
|
||||
const config = {};
|
||||
|
||||
const missing = await inspectPluginRegistry({ stateDir, candidates: [candidate], config, env });
|
||||
expect(missing.state).toBe("missing");
|
||||
expect(missing.refreshReasons).toEqual(["missing"]);
|
||||
|
||||
await refreshPluginRegistry({
|
||||
reason: "manual",
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
config,
|
||||
env,
|
||||
});
|
||||
const fresh = await inspectPluginRegistry({ stateDir, candidates: [candidate], config, env });
|
||||
expect(fresh.state).toBe("fresh");
|
||||
expect(fresh.refreshReasons).toEqual([]);
|
||||
|
||||
const policy = await inspectPluginRegistry({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
config: { plugins: { entries: { demo: { enabled: false } } } },
|
||||
env,
|
||||
});
|
||||
expect(policy.state).toBe("stale");
|
||||
expect(policy.refreshReasons).toEqual(["policy-changed"]);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["demo-next"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const manifest = await inspectPluginRegistry({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
config,
|
||||
env,
|
||||
});
|
||||
expect(manifest.state).toBe("stale");
|
||||
expect(manifest.refreshReasons).toEqual(["stale-manifest"]);
|
||||
});
|
||||
|
||||
it("agrees with snapshot selection when a packaged runtime entry changes", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = makeTempDir();
|
||||
const sourceCandidate = createCandidate(pluginDir);
|
||||
const env = hermeticEnv();
|
||||
await refreshPluginRegistry({
|
||||
reason: "manual",
|
||||
stateDir,
|
||||
candidates: [sourceCandidate],
|
||||
env,
|
||||
});
|
||||
const builtSource = path.join(pluginDir, "index.js");
|
||||
fs.writeFileSync(builtSource, "export default { register() {} };\n", "utf8");
|
||||
fs.rmSync(sourceCandidate.source);
|
||||
const builtCandidate = { ...sourceCandidate, source: builtSource };
|
||||
|
||||
const snapshot = loadPluginRegistrySnapshotWithMetadata({
|
||||
stateDir,
|
||||
candidates: [builtCandidate],
|
||||
env,
|
||||
});
|
||||
const inspection = await inspectPluginRegistry({
|
||||
stateDir,
|
||||
candidates: [builtCandidate],
|
||||
env,
|
||||
});
|
||||
|
||||
expect(snapshot.source).toBe("derived");
|
||||
expect(snapshot.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
|
||||
"persisted-registry-stale-source",
|
||||
]);
|
||||
expect(inspection.state).toBe("stale");
|
||||
expect(inspection.refreshReasons).toEqual(["source-changed"]);
|
||||
expect(inspection.current.plugins[0]?.source).toBe(builtSource);
|
||||
});
|
||||
|
||||
it("uses the configured system-agent workspace for the freshness verdict", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const workspaceDir = makeTempDir();
|
||||
const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
createCandidate(pluginDir);
|
||||
const env = { ...hermeticEnv(), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
|
||||
const config = {
|
||||
agents: {
|
||||
ownership: "explicit" as const,
|
||||
defaults: { systemAgent: { agentId: "main" } },
|
||||
entries: { main: { workspace: workspaceDir } },
|
||||
},
|
||||
};
|
||||
await refreshPersistedInstalledPluginIndex({
|
||||
reason: "manual",
|
||||
stateDir,
|
||||
config,
|
||||
env,
|
||||
});
|
||||
|
||||
const listSelection = loadPluginRegistrySnapshotWithMetadata({
|
||||
stateDir,
|
||||
workspaceDir,
|
||||
config,
|
||||
env,
|
||||
});
|
||||
const inspection = await inspectPluginRegistry({ stateDir, config, env });
|
||||
|
||||
expect(listSelection.source).toBe("derived");
|
||||
expect(listSelection.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
|
||||
"persisted-registry-stale-source",
|
||||
]);
|
||||
expect(inspection.state).toBe("stale");
|
||||
expect(inspection.refreshReasons).toEqual(["source-changed"]);
|
||||
expect(inspection.current.workspaceDir).toBe(workspaceDir);
|
||||
expect(inspection.current.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
|
||||
|
||||
await refreshPluginRegistry({ reason: "manual", stateDir, config, env });
|
||||
const repaired = await inspectPluginRegistry({ stateDir, config, env });
|
||||
expect(repaired.state).toBe("fresh");
|
||||
expect(repaired.refreshReasons).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves install records when refreshing the persisted registry", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
await writePersistedInstalledPluginIndex(createEmptyIndex(stateDir), { stateDir });
|
||||
|
||||
await refreshPluginRegistry({ reason: "manual", stateDir, candidates: [], env: hermeticEnv() });
|
||||
|
||||
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
|
||||
expect(persisted?.installRecords.missing).toMatchObject({
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
});
|
||||
expect(persisted?.plugins).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -18,13 +18,12 @@ import { safeFileSignature, safeHashFile } from "./installed-plugin-index-hash.j
|
||||
import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js";
|
||||
import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
readPersistedInstalledPluginIndexSync,
|
||||
refreshPersistedInstalledPluginIndex,
|
||||
type InstalledPluginIndexStoreInspection,
|
||||
type InstalledPluginIndexStoreOptions,
|
||||
} from "./installed-plugin-index-store.js";
|
||||
import {
|
||||
diffInstalledPluginIndexInvalidationReasons,
|
||||
extractPluginInstallRecordsFromInstalledPluginIndex,
|
||||
getInstalledPluginRecord,
|
||||
hasInstalledPluginIndexWorkspaceScopeMismatch,
|
||||
@@ -124,7 +123,6 @@ function resolvePluginRegistryContent(
|
||||
|
||||
export type PluginRegistrySnapshot = InstalledPluginIndex;
|
||||
export type PluginRegistryRecord = InstalledPluginIndexRecord;
|
||||
type PluginRegistryInspection = InstalledPluginIndexStoreInspection;
|
||||
export type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js";
|
||||
type PluginRegistrySnapshotDiagnosticCode =
|
||||
| "persisted-registry-missing"
|
||||
@@ -156,6 +154,26 @@ type GetPluginRecordParams = LoadPluginRegistryParams & {
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
function resolveControlPlaneRegistryParams<T extends LoadInstalledPluginIndexParams>(params: T): T {
|
||||
if (!params.config) {
|
||||
return params;
|
||||
}
|
||||
const workspace = resolvePluginControlPlaneWorkspace({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
const diagnostics = appendPluginControlPlaneWorkspaceDiagnostic(
|
||||
params.diagnostics ?? [],
|
||||
workspace,
|
||||
);
|
||||
return {
|
||||
...params,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
...(workspace.workspaceDir !== undefined ? { workspaceDir: workspace.workspaceDir } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function canReuseCurrentPluginMetadataSnapshot(params: LoadPluginRegistryParams): boolean {
|
||||
return (
|
||||
params.allowCurrent !== false &&
|
||||
@@ -619,10 +637,41 @@ export function isPluginEnabled(params: GetPluginRecordParams): boolean {
|
||||
return isInstalledPluginEnabled(resolveSnapshot(params), params.pluginId, params.config);
|
||||
}
|
||||
|
||||
export function inspectPluginRegistry(
|
||||
export async function inspectPluginRegistry(
|
||||
params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {},
|
||||
): Promise<PluginRegistryInspection> {
|
||||
return inspectPersistedInstalledPluginIndex(params);
|
||||
) {
|
||||
const inspectionParams = resolveControlPlaneRegistryParams(params);
|
||||
const persisted = readPersistedInstalledPluginIndexSync(inspectionParams);
|
||||
// Inspection and runtime selection share one verdict so runtime cannot reject "fresh".
|
||||
const result = loadPluginRegistrySnapshotWithMetadata({
|
||||
...inspectionParams,
|
||||
allowCurrent: false,
|
||||
});
|
||||
if (!persisted) {
|
||||
return {
|
||||
state: "missing" as const,
|
||||
refreshReasons: ["missing"],
|
||||
persisted: null,
|
||||
current: result.snapshot,
|
||||
};
|
||||
}
|
||||
const fresh = result.source === "persisted";
|
||||
const refreshReasons = fresh
|
||||
? []
|
||||
: [...diffInstalledPluginIndexInvalidationReasons(persisted, result.snapshot)];
|
||||
if (!fresh && refreshReasons.length === 0) {
|
||||
refreshReasons.push(
|
||||
result.diagnostics.some((diagnostic) => diagnostic.code === "persisted-registry-stale-policy")
|
||||
? "policy-changed"
|
||||
: "source-changed",
|
||||
);
|
||||
}
|
||||
return {
|
||||
state: fresh ? ("fresh" as const) : ("stale" as const),
|
||||
refreshReasons,
|
||||
persisted,
|
||||
current: result.snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
export function refreshPluginRegistry(
|
||||
@@ -631,15 +680,5 @@ export function refreshPluginRegistry(
|
||||
if (!params.config) {
|
||||
return refreshPersistedInstalledPluginIndex(params);
|
||||
}
|
||||
const workspace = resolvePluginControlPlaneWorkspace({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
const refreshParams = {
|
||||
...params,
|
||||
diagnostics: appendPluginControlPlaneWorkspaceDiagnostic(params.diagnostics ?? [], workspace),
|
||||
...(workspace.workspaceDir !== undefined ? { workspaceDir: workspace.workspaceDir } : {}),
|
||||
};
|
||||
return refreshPersistedInstalledPluginIndex(refreshParams);
|
||||
return refreshPersistedInstalledPluginIndex(resolveControlPlaneRegistryParams(params));
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { recordPluginCandidateInstallOwner } from "./candidate-install-owner.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import {
|
||||
readPersistedInstalledPluginIndex,
|
||||
writePersistedInstalledPluginIndex,
|
||||
} from "./installed-plugin-index-store.js";
|
||||
import { writePersistedInstalledPluginIndex } from "./installed-plugin-index-store.js";
|
||||
import {
|
||||
resolveInstalledPluginIndexPolicyHash,
|
||||
type InstalledPluginIndex,
|
||||
@@ -21,13 +18,11 @@ import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.
|
||||
import {
|
||||
createPluginRegistryIdNormalizer,
|
||||
getPluginRecord,
|
||||
inspectPluginRegistry,
|
||||
isPluginEnabled,
|
||||
listPluginContributionIds,
|
||||
loadPluginRegistrySnapshot,
|
||||
loadPluginRegistrySnapshotWithMetadata,
|
||||
normalizePluginsConfigWithRegistry,
|
||||
refreshPluginRegistry,
|
||||
resolveManifestContractOwnerPluginId,
|
||||
resolveManifestContractPluginIds,
|
||||
resolvePluginContributionOwners,
|
||||
@@ -981,94 +976,4 @@ describe("plugin registry facade", () => {
|
||||
expect(first.snapshot.hostContractVersion).toBe("2026.4.25");
|
||||
expect(second.snapshot.hostContractVersion).toBe("2026.4.26");
|
||||
});
|
||||
|
||||
it("derives a fresh registry without persisted install records when caller disables persisted reads", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const rootDir = makeTempDir();
|
||||
const candidate = createCandidate(rootDir);
|
||||
await writePersistedInstalledPluginIndex(
|
||||
createIndex("persisted", {
|
||||
installRecords: {
|
||||
persisted: {
|
||||
source: "npm",
|
||||
spec: "persisted-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "persisted"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ stateDir },
|
||||
);
|
||||
|
||||
const result = loadPluginRegistrySnapshotWithMetadata({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env: hermeticEnv(),
|
||||
preferPersisted: false,
|
||||
});
|
||||
|
||||
expect(result.source).toBe("derived");
|
||||
expectSnapshotPluginIds(result.snapshot, ["demo"]);
|
||||
expect(result.snapshot.installRecords).not.toHaveProperty("persisted");
|
||||
});
|
||||
|
||||
it("exposes explicit persisted registry inspect and refresh operations", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const candidate = createCandidate(pluginDir);
|
||||
const env = hermeticEnv();
|
||||
|
||||
const missingInspect = await inspectPluginRegistry({ stateDir, candidates: [candidate], env });
|
||||
expect(missingInspect.state).toBe("missing");
|
||||
expect(missingInspect.refreshReasons).toEqual(["missing"]);
|
||||
expect(missingInspect.persisted).toBeNull();
|
||||
expect(missingInspect.current.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
|
||||
|
||||
await refreshPluginRegistry({
|
||||
reason: "manual",
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
env,
|
||||
});
|
||||
|
||||
const freshInspect = await inspectPluginRegistry({ stateDir, candidates: [candidate], env });
|
||||
expect(freshInspect.state).toBe("fresh");
|
||||
expect(freshInspect.refreshReasons).toEqual([]);
|
||||
expect(freshInspect.persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("preserves install records when refreshing the persisted registry", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
await writePersistedInstalledPluginIndex(
|
||||
createIndex("missing", {
|
||||
installRecords: {
|
||||
missing: {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}),
|
||||
{ stateDir },
|
||||
);
|
||||
|
||||
await refreshPluginRegistry({
|
||||
reason: "manual",
|
||||
stateDir,
|
||||
candidates: [],
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
|
||||
if (!persisted) {
|
||||
throw new Error("Expected persisted plugin index");
|
||||
}
|
||||
expectInstallRecord(persisted.installRecords, "missing", {
|
||||
source: "npm",
|
||||
spec: "missing-plugin@1.0.0",
|
||||
installPath: path.join(stateDir, "plugins", "missing"),
|
||||
});
|
||||
expect(persisted.plugins).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user