mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:00:45 +00:00
refactor: reuse plugin metadata snapshots
This commit is contained in:
@@ -202,6 +202,77 @@ describe("model-pricing-cache", () => {
|
|||||||
expect(fetchImpl).not.toHaveBeenCalled();
|
expect(fetchImpl).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses a provided metadata registry view without rebuilding manifest metadata", async () => {
|
||||||
|
const manifestRegistry = {
|
||||||
|
diagnostics: [],
|
||||||
|
plugins: [
|
||||||
|
createManifestRecord({
|
||||||
|
id: "search-plugin",
|
||||||
|
contracts: { webSearchProviders: ["search-plugin"] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
"search-plugin": {
|
||||||
|
config: {
|
||||||
|
webSearch: {
|
||||||
|
model: "local-search/search-model",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"local-search": {
|
||||||
|
baseUrl: "http://127.0.0.1:43210/v1",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "search-model",
|
||||||
|
cost: { input: 0.2, output: 0.4 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpenClawConfig;
|
||||||
|
const fetchImpl = vi.fn<typeof fetch>();
|
||||||
|
|
||||||
|
await refreshGatewayModelPricingCache({
|
||||||
|
config,
|
||||||
|
fetchImpl,
|
||||||
|
pluginMetadataSnapshot: {
|
||||||
|
index: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
pluginId: "search-plugin",
|
||||||
|
origin: "global",
|
||||||
|
enabled: true,
|
||||||
|
enabledByDefault: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as never,
|
||||||
|
manifestRegistry,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(fetchImpl).not.toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
getCachedGatewayModelPricing({ provider: "local-search", model: "search-model" }),
|
||||||
|
).toEqual({
|
||||||
|
input: 0.2,
|
||||||
|
output: 0.4,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not load plugin manifests for pricing when plugins are globally disabled", async () => {
|
it("does not load plugin manifests for pricing when plugins are globally disabled", async () => {
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|||||||
@@ -13,18 +13,15 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
import { planManifestModelCatalogRows, type ModelCatalogCost } from "../model-catalog/index.js";
|
import { planManifestModelCatalogRows, type ModelCatalogCost } from "../model-catalog/index.js";
|
||||||
import { isInstalledPluginEnabled } from "../plugins/installed-plugin-index.js";
|
import { isInstalledPluginEnabled } from "../plugins/installed-plugin-index.js";
|
||||||
import { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js";
|
|
||||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||||
import type {
|
import type {
|
||||||
PluginManifestModelPricingModelIdTransform,
|
PluginManifestModelPricingModelIdTransform,
|
||||||
PluginManifestModelPricingProvider,
|
PluginManifestModelPricingProvider,
|
||||||
PluginManifestModelPricingSource,
|
PluginManifestModelPricingSource,
|
||||||
} from "../plugins/manifest.js";
|
} from "../plugins/manifest.js";
|
||||||
import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
|
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||||
import {
|
import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||||
loadPluginRegistrySnapshot,
|
import type { PluginRegistrySnapshot } from "../plugins/plugin-registry.js";
|
||||||
type PluginRegistrySnapshot,
|
|
||||||
} from "../plugins/plugin-registry.js";
|
|
||||||
import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js";
|
import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js";
|
||||||
import {
|
import {
|
||||||
clearGatewayModelPricingCacheState,
|
clearGatewayModelPricingCacheState,
|
||||||
@@ -400,15 +397,19 @@ function filterActiveManifestRegistry(params: {
|
|||||||
|
|
||||||
function resolveModelPricingManifestMetadata(params: {
|
function resolveModelPricingManifestMetadata(params: {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
env?: NodeJS.ProcessEnv;
|
||||||
|
workspaceDir?: string;
|
||||||
|
pluginMetadataSnapshot?: PluginMetadataRegistryView;
|
||||||
|
pluginLookUpTable?: PluginMetadataRegistryView;
|
||||||
manifestRegistry?: PluginManifestRegistry;
|
manifestRegistry?: PluginManifestRegistry;
|
||||||
}): ModelPricingManifestMetadata {
|
}): ModelPricingManifestMetadata {
|
||||||
if (params.pluginLookUpTable) {
|
const metadataSnapshot = params.pluginMetadataSnapshot ?? params.pluginLookUpTable;
|
||||||
|
if (metadataSnapshot) {
|
||||||
return {
|
return {
|
||||||
allRegistry: params.pluginLookUpTable.manifestRegistry,
|
allRegistry: metadataSnapshot.manifestRegistry,
|
||||||
activeRegistry: filterActiveManifestRegistry({
|
activeRegistry: filterActiveManifestRegistry({
|
||||||
registry: params.pluginLookUpTable.manifestRegistry,
|
registry: metadataSnapshot.manifestRegistry,
|
||||||
index: params.pluginLookUpTable.index,
|
index: metadataSnapshot.index,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -426,17 +427,16 @@ function resolveModelPricingManifestMetadata(params: {
|
|||||||
activeRegistry: emptyRegistry,
|
activeRegistry: emptyRegistry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const index = loadPluginRegistrySnapshot({ config: params.config });
|
const snapshot = loadPluginMetadataSnapshot({
|
||||||
const allRegistry = loadPluginManifestRegistryForInstalledIndex({
|
|
||||||
index,
|
|
||||||
config: params.config,
|
config: params.config,
|
||||||
includeDisabled: true,
|
env: params.env ?? process.env,
|
||||||
|
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
allRegistry,
|
allRegistry: snapshot.manifestRegistry,
|
||||||
activeRegistry: filterActiveManifestRegistry({
|
activeRegistry: filterActiveManifestRegistry({
|
||||||
registry: allRegistry,
|
registry: snapshot.manifestRegistry,
|
||||||
index,
|
index: snapshot.index,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -1102,8 +1102,11 @@ function collectSeededPricing(params: {
|
|||||||
|
|
||||||
export async function refreshGatewayModelPricingCache(params: {
|
export async function refreshGatewayModelPricingCache(params: {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
fetchImpl?: typeof fetch;
|
fetchImpl?: typeof fetch;
|
||||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
workspaceDir?: string;
|
||||||
|
pluginMetadataSnapshot?: PluginMetadataRegistryView;
|
||||||
|
pluginLookUpTable?: PluginMetadataRegistryView;
|
||||||
manifestRegistry?: PluginManifestRegistry;
|
manifestRegistry?: PluginManifestRegistry;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (!isGatewayModelPricingEnabled(params.config)) {
|
if (!isGatewayModelPricingEnabled(params.config)) {
|
||||||
@@ -1117,6 +1120,9 @@ export async function refreshGatewayModelPricingCache(params: {
|
|||||||
inFlightRefresh = (async () => {
|
inFlightRefresh = (async () => {
|
||||||
const manifestMetadata = resolveModelPricingManifestMetadata({
|
const manifestMetadata = resolveModelPricingManifestMetadata({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
|
env: params.env,
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
pluginMetadataSnapshot: params.pluginMetadataSnapshot,
|
||||||
pluginLookUpTable: params.pluginLookUpTable,
|
pluginLookUpTable: params.pluginLookUpTable,
|
||||||
manifestRegistry: params.manifestRegistry,
|
manifestRegistry: params.manifestRegistry,
|
||||||
});
|
});
|
||||||
@@ -1251,8 +1257,11 @@ export async function refreshGatewayModelPricingCache(params: {
|
|||||||
|
|
||||||
export function startGatewayModelPricingRefresh(params: {
|
export function startGatewayModelPricingRefresh(params: {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
fetchImpl?: typeof fetch;
|
fetchImpl?: typeof fetch;
|
||||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
workspaceDir?: string;
|
||||||
|
pluginMetadataSnapshot?: PluginMetadataRegistryView;
|
||||||
|
pluginLookUpTable?: PluginMetadataRegistryView;
|
||||||
manifestRegistry?: PluginManifestRegistry;
|
manifestRegistry?: PluginManifestRegistry;
|
||||||
}): () => void {
|
}): () => void {
|
||||||
if (!isGatewayModelPricingEnabled(params.config)) {
|
if (!isGatewayModelPricingEnabled(params.config)) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import { isVitestRuntimeEnv } from "../infra/env.js";
|
import { isVitestRuntimeEnv } from "../infra/env.js";
|
||||||
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
|
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
|
||||||
import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
|
import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||||
import type { ChannelHealthMonitor } from "./channel-health-monitor.js";
|
import type { ChannelHealthMonitor } from "./channel-health-monitor.js";
|
||||||
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
|
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
|
||||||
import { isGatewayModelPricingEnabled } from "./model-pricing-config.js";
|
import { isGatewayModelPricingEnabled } from "./model-pricing-config.js";
|
||||||
@@ -91,7 +91,7 @@ function recoverPendingSessionDeliveries(params: {
|
|||||||
|
|
||||||
function startGatewayModelPricingRefreshOnDemand(params: {
|
function startGatewayModelPricingRefreshOnDemand(params: {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
pluginLookUpTable?: PluginMetadataRegistryView;
|
||||||
log: GatewayRuntimeServiceLogger;
|
log: GatewayRuntimeServiceLogger;
|
||||||
}): () => void {
|
}): () => void {
|
||||||
if (!isGatewayModelPricingEnabled(params.config)) {
|
if (!isGatewayModelPricingEnabled(params.config)) {
|
||||||
@@ -125,7 +125,7 @@ export function startGatewayRuntimeServices(params: {
|
|||||||
cfgAtStart: OpenClawConfig;
|
cfgAtStart: OpenClawConfig;
|
||||||
channelManager: GatewayChannelManager;
|
channelManager: GatewayChannelManager;
|
||||||
log: GatewayRuntimeServiceLogger;
|
log: GatewayRuntimeServiceLogger;
|
||||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
pluginLookUpTable?: PluginMetadataRegistryView;
|
||||||
}): {
|
}): {
|
||||||
heartbeatRunner: HeartbeatRunner;
|
heartbeatRunner: HeartbeatRunner;
|
||||||
channelHealthMonitor: ChannelHealthMonitor | null;
|
channelHealthMonitor: ChannelHealthMonitor | null;
|
||||||
|
|||||||
66
src/plugins/manifest-contract-eligibility.test.ts
Normal file
66
src/plugins/manifest-contract-eligibility.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
getCurrentPluginMetadataSnapshot: vi.fn(),
|
||||||
|
loadPluginMetadataSnapshot: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./current-plugin-metadata-snapshot.js", () => ({
|
||||||
|
getCurrentPluginMetadataSnapshot: mocks.getCurrentPluginMetadataSnapshot,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./plugin-metadata-snapshot.js", () => ({
|
||||||
|
loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { loadManifestContractSnapshot } from "./manifest-contract-eligibility.js";
|
||||||
|
|
||||||
|
describe("loadManifestContractSnapshot", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined);
|
||||||
|
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||||
|
index: { plugins: [] },
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks the current metadata snapshot with env and workspace scope", () => {
|
||||||
|
const env = { HOME: "/home/snapshot" } as NodeJS.ProcessEnv;
|
||||||
|
const current = {
|
||||||
|
index: { plugins: [] },
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(current);
|
||||||
|
|
||||||
|
expect(loadManifestContractSnapshot({ config: {}, workspaceDir: "/workspace", env })).toBe(
|
||||||
|
current,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||||
|
config: {},
|
||||||
|
env,
|
||||||
|
workspaceDir: "/workspace",
|
||||||
|
});
|
||||||
|
expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the shared metadata snapshot loader", () => {
|
||||||
|
const env = { HOME: "/home/fallback" } as NodeJS.ProcessEnv;
|
||||||
|
const snapshot = {
|
||||||
|
index: { plugins: [{ pluginId: "demo" }] },
|
||||||
|
plugins: [{ id: "demo" }],
|
||||||
|
};
|
||||||
|
mocks.loadPluginMetadataSnapshot.mockReturnValue(snapshot);
|
||||||
|
|
||||||
|
expect(loadManifestContractSnapshot({ config: {}, env })).toEqual({
|
||||||
|
index: snapshot.index,
|
||||||
|
plugins: snapshot.plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||||
|
config: {},
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||||
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
|
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
|
||||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
|
||||||
import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js";
|
import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js";
|
||||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
|
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||||
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
|
import type {
|
||||||
|
PluginMetadataManifestView,
|
||||||
|
PluginMetadataSnapshot,
|
||||||
|
} from "./plugin-metadata-snapshot.types.js";
|
||||||
|
|
||||||
export function isManifestPluginAvailableForControlPlane(params: {
|
export function isManifestPluginAvailableForControlPlane(params: {
|
||||||
snapshot: Pick<PluginMetadataSnapshot, "index">;
|
snapshot: Pick<PluginMetadataSnapshot, "index">;
|
||||||
@@ -65,28 +67,23 @@ export function loadManifestContractSnapshot(params: {
|
|||||||
config?: OpenClawConfig;
|
config?: OpenClawConfig;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): Pick<PluginMetadataSnapshot, "index" | "plugins"> {
|
}): PluginMetadataManifestView {
|
||||||
|
const env = params.env ?? process.env;
|
||||||
const current = getCurrentPluginMetadataSnapshot({
|
const current = getCurrentPluginMetadataSnapshot({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
|
env,
|
||||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||||
});
|
});
|
||||||
if (current) {
|
if (current) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
const env = params.env ?? process.env;
|
const snapshot = loadPluginMetadataSnapshot({
|
||||||
const index = loadPluginRegistrySnapshot({
|
config: params.config ?? {},
|
||||||
config: params.config,
|
|
||||||
env,
|
env,
|
||||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
index,
|
index: snapshot.index,
|
||||||
plugins: loadPluginManifestRegistryForInstalledIndex({
|
plugins: snapshot.plugins,
|
||||||
index,
|
|
||||||
config: params.config,
|
|
||||||
env,
|
|
||||||
includeDisabled: true,
|
|
||||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
|
||||||
}).plugins,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { createPluginRegistryIdNormalizer } from "./plugin-registry-id-normalize
|
|||||||
import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry-snapshot.js";
|
import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry-snapshot.js";
|
||||||
export type {
|
export type {
|
||||||
LoadPluginMetadataSnapshotParams,
|
LoadPluginMetadataSnapshotParams,
|
||||||
|
PluginMetadataManifestView,
|
||||||
|
PluginMetadataRegistryView,
|
||||||
PluginMetadataSnapshot,
|
PluginMetadataSnapshot,
|
||||||
PluginMetadataSnapshotMetrics,
|
PluginMetadataSnapshotMetrics,
|
||||||
PluginMetadataSnapshotOwnerMaps,
|
PluginMetadataSnapshotOwnerMaps,
|
||||||
@@ -146,6 +148,12 @@ function buildPluginMetadataOwnerMaps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listPluginOriginsFromMetadataSnapshot(
|
||||||
|
snapshot: Pick<PluginMetadataSnapshot, "plugins">,
|
||||||
|
): ReadonlyMap<string, PluginManifestRecord["origin"]> {
|
||||||
|
return new Map(snapshot.plugins.map((record) => [record.id, record.origin]));
|
||||||
|
}
|
||||||
|
|
||||||
export function loadPluginMetadataSnapshot(
|
export function loadPluginMetadataSnapshot(
|
||||||
params: LoadPluginMetadataSnapshotParams,
|
params: LoadPluginMetadataSnapshotParams,
|
||||||
): PluginMetadataSnapshot {
|
): PluginMetadataSnapshot {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export type PluginMetadataSnapshot = {
|
|||||||
metrics: PluginMetadataSnapshotMetrics;
|
metrics: PluginMetadataSnapshotMetrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PluginMetadataRegistryView = Pick<PluginMetadataSnapshot, "index" | "manifestRegistry">;
|
||||||
|
|
||||||
|
export type PluginMetadataManifestView = Pick<PluginMetadataSnapshot, "index" | "plugins">;
|
||||||
|
|
||||||
export type LoadPluginMetadataSnapshotParams = {
|
export type LoadPluginMetadataSnapshotParams = {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import type { PluginManifestRecord } from "./manifest-registry.js";
|
|||||||
import type { ProviderPlugin } from "./types.js";
|
import type { ProviderPlugin } from "./types.js";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
loadPluginRegistrySnapshot: vi.fn(),
|
loadPluginMetadataSnapshot: vi.fn(),
|
||||||
loadPluginManifestRegistryForInstalledIndex: vi.fn(),
|
|
||||||
resolveDiscoveredProviderPluginIds: vi.fn(),
|
resolveDiscoveredProviderPluginIds: vi.fn(),
|
||||||
resolvePluginProviders: vi.fn(),
|
resolvePluginProviders: vi.fn(),
|
||||||
loadSource: vi.fn(),
|
loadSource: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./plugin-registry.js", () => ({
|
vi.mock("./plugin-metadata-snapshot.js", async (importOriginal) => {
|
||||||
loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot,
|
const actual = await importOriginal<typeof import("./plugin-metadata-snapshot.js")>();
|
||||||
}));
|
return {
|
||||||
|
...actual,
|
||||||
vi.mock("./manifest-registry-installed.js", () => ({
|
loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
|
||||||
loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistryForInstalledIndex,
|
};
|
||||||
}));
|
});
|
||||||
|
|
||||||
vi.mock("./providers.js", () => ({
|
vi.mock("./providers.js", () => ({
|
||||||
resolveDiscoveredProviderPluginIds: mocks.resolveDiscoveredProviderPluginIds,
|
resolveDiscoveredProviderPluginIds: mocks.resolveDiscoveredProviderPluginIds,
|
||||||
@@ -82,11 +81,13 @@ function createProvider(params: { id: string; mode: "static" | "catalog" }): Pro
|
|||||||
describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
|
|
||||||
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["deepseek"]);
|
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["deepseek"]);
|
||||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
|
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||||
plugins: [createManifestPlugin("deepseek")],
|
index: { plugins: [] },
|
||||||
diagnostics: [],
|
manifestRegistry: {
|
||||||
|
plugins: [createManifestPlugin("deepseek")],
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,20 +117,23 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
|||||||
"kilocode",
|
"kilocode",
|
||||||
"unused",
|
"unused",
|
||||||
]);
|
]);
|
||||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
|
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||||
plugins: [
|
index: { plugins: [] },
|
||||||
createManifestPlugin("codex"),
|
manifestRegistry: {
|
||||||
createManifestPlugin("deepseek"),
|
plugins: [
|
||||||
createManifestPluginWithoutDiscovery({
|
createManifestPlugin("codex"),
|
||||||
id: "kilocode",
|
createManifestPlugin("deepseek"),
|
||||||
providerAuthEnvVars: { kilocode: ["KILOCODE_API_KEY"] },
|
createManifestPluginWithoutDiscovery({
|
||||||
}),
|
id: "kilocode",
|
||||||
createManifestPluginWithoutDiscovery({
|
providerAuthEnvVars: { kilocode: ["KILOCODE_API_KEY"] },
|
||||||
id: "unused",
|
}),
|
||||||
providerAuthEnvVars: { unused: ["UNUSED_API_KEY"] },
|
createManifestPluginWithoutDiscovery({
|
||||||
}),
|
id: "unused",
|
||||||
],
|
providerAuthEnvVars: { unused: ["UNUSED_API_KEY"] },
|
||||||
diagnostics: [],
|
}),
|
||||||
|
],
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
mocks.loadSource.mockImplementation((modulePath: string) =>
|
mocks.loadSource.mockImplementation((modulePath: string) =>
|
||||||
modulePath.includes("/codex/")
|
modulePath.includes("/codex/")
|
||||||
@@ -150,27 +154,25 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shares one registry snapshot and manifest registry between provider id discovery and entry loading", () => {
|
it("shares one metadata snapshot between provider id discovery and entry loading", () => {
|
||||||
const registry = { plugins: [] };
|
const registry = { plugins: [] };
|
||||||
const manifestRegistry = {
|
const manifestRegistry = {
|
||||||
plugins: [createManifestPlugin("deepseek")],
|
plugins: [createManifestPlugin("deepseek")],
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
};
|
};
|
||||||
mocks.loadPluginRegistrySnapshot.mockReturnValue(registry);
|
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry);
|
index: registry,
|
||||||
|
manifestRegistry,
|
||||||
|
});
|
||||||
mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "catalog" }));
|
mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "catalog" }));
|
||||||
|
|
||||||
resolvePluginDiscoveryProvidersRuntime({ config: {}, env: {} as NodeJS.ProcessEnv });
|
resolvePluginDiscoveryProvidersRuntime({ config: {}, env: {} as NodeJS.ProcessEnv });
|
||||||
|
|
||||||
expect(mocks.loadPluginRegistrySnapshot).toHaveBeenCalledOnce();
|
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||||
expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith({
|
|
||||||
index: registry,
|
|
||||||
config: {},
|
config: {},
|
||||||
workspaceDir: undefined,
|
|
||||||
env: {},
|
env: {},
|
||||||
includeDisabled: true,
|
|
||||||
});
|
});
|
||||||
expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce();
|
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
|
||||||
expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledWith(
|
expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
registry,
|
registry,
|
||||||
@@ -203,8 +205,7 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.loadPluginRegistrySnapshot).not.toHaveBeenCalled();
|
expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||||
expect(mocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledWith(
|
expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
registry,
|
registry,
|
||||||
@@ -228,9 +229,12 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not fall back to full plugin loading when discovery entries are requested only", () => {
|
it("does not fall back to full plugin loading when discovery entries are requested only", () => {
|
||||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
|
mocks.loadPluginMetadataSnapshot.mockReturnValue({
|
||||||
plugins: [createManifestPluginWithoutDiscovery({ id: "deepseek" })],
|
index: { plugins: [] },
|
||||||
diagnostics: [],
|
manifestRegistry: {
|
||||||
|
plugins: [createManifestPluginWithoutDiscovery({ id: "deepseek" })],
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toEqual([]);
|
expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toEqual([]);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
|
||||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||||
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
|
import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js";
|
||||||
import { resolveDiscoveredProviderPluginIds } from "./providers.js";
|
import { resolveDiscoveredProviderPluginIds } from "./providers.js";
|
||||||
import { resolvePluginProviders } from "./providers.runtime.js";
|
import { resolvePluginProviders } from "./providers.runtime.js";
|
||||||
import { createPluginSourceLoader } from "./source-loader.js";
|
import { createPluginSourceLoader } from "./source-loader.js";
|
||||||
@@ -77,18 +76,17 @@ function resolveProviderDiscoveryEntryPlugins(params: {
|
|||||||
includeUntrustedWorkspacePlugins?: boolean;
|
includeUntrustedWorkspacePlugins?: boolean;
|
||||||
requireCompleteDiscoveryEntryCoverage?: boolean;
|
requireCompleteDiscoveryEntryCoverage?: boolean;
|
||||||
discoveryEntriesOnly?: boolean;
|
discoveryEntriesOnly?: boolean;
|
||||||
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "index" | "manifestRegistry">;
|
pluginMetadataSnapshot?: PluginMetadataRegistryView;
|
||||||
}): ProviderDiscoveryEntryResult {
|
}): ProviderDiscoveryEntryResult {
|
||||||
const registry = params.pluginMetadataSnapshot?.index ?? loadPluginRegistrySnapshot(params);
|
const metadataSnapshot =
|
||||||
const manifestRegistry =
|
params.pluginMetadataSnapshot ??
|
||||||
params.pluginMetadataSnapshot?.manifestRegistry ??
|
loadPluginMetadataSnapshot({
|
||||||
loadPluginManifestRegistryForInstalledIndex({
|
config: params.config ?? {},
|
||||||
index: registry,
|
env: params.env ?? process.env,
|
||||||
config: params.config,
|
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||||
workspaceDir: params.workspaceDir,
|
|
||||||
env: params.env,
|
|
||||||
includeDisabled: true,
|
|
||||||
});
|
});
|
||||||
|
const registry = metadataSnapshot.index;
|
||||||
|
const manifestRegistry = metadataSnapshot.manifestRegistry;
|
||||||
const pluginIds = resolveDiscoveredProviderPluginIds({
|
const pluginIds = resolveDiscoveredProviderPluginIds({
|
||||||
...params,
|
...params,
|
||||||
registry,
|
registry,
|
||||||
@@ -148,10 +146,10 @@ export function resolvePluginDiscoveryProvidersRuntime(params: {
|
|||||||
includeUntrustedWorkspacePlugins?: boolean;
|
includeUntrustedWorkspacePlugins?: boolean;
|
||||||
requireCompleteDiscoveryEntryCoverage?: boolean;
|
requireCompleteDiscoveryEntryCoverage?: boolean;
|
||||||
discoveryEntriesOnly?: boolean;
|
discoveryEntriesOnly?: boolean;
|
||||||
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "index" | "manifestRegistry">;
|
pluginMetadataSnapshot?: PluginMetadataRegistryView;
|
||||||
}): ProviderPlugin[] {
|
}): ProviderPlugin[] {
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const entryResult = resolveProviderDiscoveryEntryPlugins(params);
|
const entryResult = resolveProviderDiscoveryEntryPlugins({ ...params, env });
|
||||||
if (params.discoveryEntriesOnly === true) {
|
if (params.discoveryEntriesOnly === true) {
|
||||||
return entryResult.providers;
|
return entryResult.providers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
import type { ModelProviderConfig } from "../config/types.js";
|
import type { ModelProviderConfig } from "../config/types.js";
|
||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js";
|
||||||
import {
|
import {
|
||||||
listPluginContributionIds,
|
listPluginContributionIds,
|
||||||
loadPluginRegistrySnapshot,
|
loadPluginRegistrySnapshot,
|
||||||
@@ -43,7 +43,7 @@ export type ResolveRuntimePluginDiscoveryProvidersParams = {
|
|||||||
includeUntrustedWorkspacePlugins?: boolean;
|
includeUntrustedWorkspacePlugins?: boolean;
|
||||||
requireCompleteDiscoveryEntryCoverage?: boolean;
|
requireCompleteDiscoveryEntryCoverage?: boolean;
|
||||||
discoveryEntriesOnly?: boolean;
|
discoveryEntriesOnly?: boolean;
|
||||||
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "index" | "manifestRegistry">;
|
pluginMetadataSnapshot?: PluginMetadataRegistryView;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResolveInstalledPluginProviderContributionIdsParams = LoadPluginRegistryParams & {
|
export type ResolveInstalledPluginProviderContributionIdsParams = LoadPluginRegistryParams & {
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js";
|
export {
|
||||||
export { loadPluginRegistrySnapshot } from "../plugins/plugin-registry.js";
|
listPluginOriginsFromMetadataSnapshot,
|
||||||
|
loadPluginMetadataSnapshot,
|
||||||
|
} from "../plugins/plugin-metadata-snapshot.js";
|
||||||
|
|||||||
@@ -2,25 +2,32 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts";
|
import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts";
|
||||||
|
|
||||||
const manifestMocks = vi.hoisted(() => ({
|
const manifestMocks = vi.hoisted(() => ({
|
||||||
loadPluginManifestRegistryForInstalledIndex: vi.fn(),
|
listPluginOriginsFromMetadataSnapshot: vi.fn(
|
||||||
loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
|
(snapshot: { plugins: Array<{ id: string; origin: string }> }) =>
|
||||||
|
new Map(snapshot.plugins.map((record) => [record.id, record.origin])),
|
||||||
|
),
|
||||||
|
loadPluginMetadataSnapshot: vi.fn<() => { plugins: Array<{ id: string; origin: string }> }>(
|
||||||
|
() => ({
|
||||||
|
plugins: [],
|
||||||
|
}),
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./runtime-manifest.runtime.js", () => ({
|
vi.mock("./runtime-manifest.runtime.js", () => ({
|
||||||
loadPluginManifestRegistryForInstalledIndex:
|
listPluginOriginsFromMetadataSnapshot: manifestMocks.listPluginOriginsFromMetadataSnapshot,
|
||||||
manifestMocks.loadPluginManifestRegistryForInstalledIndex,
|
loadPluginMetadataSnapshot: manifestMocks.loadPluginMetadataSnapshot,
|
||||||
loadPluginRegistrySnapshot: manifestMocks.loadPluginRegistrySnapshot,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
|
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
|
||||||
|
|
||||||
describe("prepareSecretsRuntimeSnapshot loadable plugin origins", () => {
|
describe("prepareSecretsRuntimeSnapshot loadable plugin origins", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
manifestMocks.loadPluginManifestRegistryForInstalledIndex.mockReset();
|
manifestMocks.listPluginOriginsFromMetadataSnapshot.mockClear();
|
||||||
manifestMocks.loadPluginRegistrySnapshot.mockReset();
|
manifestMocks.loadPluginMetadataSnapshot.mockReset();
|
||||||
|
manifestMocks.loadPluginMetadataSnapshot.mockReturnValue({ plugins: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips manifest registry loading when plugin entries are absent", async () => {
|
it("skips metadata snapshot loading when plugin entries are absent", async () => {
|
||||||
await prepareSecretsRuntimeSnapshot({
|
await prepareSecretsRuntimeSnapshot({
|
||||||
config: asConfig({
|
config: asConfig({
|
||||||
models: {
|
models: {
|
||||||
@@ -36,7 +43,42 @@ describe("prepareSecretsRuntimeSnapshot loadable plugin origins", () => {
|
|||||||
includeAuthStoreRefs: false,
|
includeAuthStoreRefs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(manifestMocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled();
|
expect(manifestMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
|
||||||
expect(manifestMocks.loadPluginRegistrySnapshot).not.toHaveBeenCalled();
|
expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives loadable plugin origins from the shared metadata snapshot", async () => {
|
||||||
|
const snapshot = {
|
||||||
|
plugins: [{ id: "demo", origin: "workspace" }],
|
||||||
|
};
|
||||||
|
manifestMocks.loadPluginMetadataSnapshot.mockReturnValue(snapshot);
|
||||||
|
|
||||||
|
await prepareSecretsRuntimeSnapshot({
|
||||||
|
config: asConfig({
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
demo: {
|
||||||
|
config: {
|
||||||
|
apiKey: { source: "env", provider: "default", id: "DEMO_API_KEY" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
env: { HOME: "/home/demo", DEMO_API_KEY: "sk-demo" },
|
||||||
|
includeAuthStoreRefs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(manifestMocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({
|
||||||
|
config: expect.objectContaining({
|
||||||
|
plugins: expect.any(Object),
|
||||||
|
}),
|
||||||
|
workspaceDir: expect.any(String),
|
||||||
|
env: expect.objectContaining({
|
||||||
|
HOME: "/home/demo",
|
||||||
|
DEMO_API_KEY: "sk-demo",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).toHaveBeenCalledWith(snapshot);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,21 +140,14 @@ async function resolveLoadablePluginOrigins(params: {
|
|||||||
params.config,
|
params.config,
|
||||||
resolveDefaultAgentId(params.config),
|
resolveDefaultAgentId(params.config),
|
||||||
);
|
);
|
||||||
const { loadPluginManifestRegistryForInstalledIndex, loadPluginRegistrySnapshot } =
|
const { listPluginOriginsFromMetadataSnapshot, loadPluginMetadataSnapshot } =
|
||||||
await loadRuntimeManifestHelpers();
|
await loadRuntimeManifestHelpers();
|
||||||
const index = loadPluginRegistrySnapshot({
|
const snapshot = loadPluginMetadataSnapshot({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
env: params.env,
|
env: params.env,
|
||||||
});
|
});
|
||||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
return listPluginOriginsFromMetadataSnapshot(snapshot);
|
||||||
index,
|
|
||||||
config: params.config,
|
|
||||||
workspaceDir,
|
|
||||||
env: params.env,
|
|
||||||
includeDisabled: true,
|
|
||||||
});
|
|
||||||
return new Map(manifestRegistry.plugins.map((record) => [record.id, record.origin]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeSecretsRuntimeEnv(
|
function mergeSecretsRuntimeEnv(
|
||||||
|
|||||||
Reference in New Issue
Block a user