mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(plugins): snapshot embedding provider registrations
This commit is contained in:
@@ -5,7 +5,10 @@ import {
|
||||
registerVirtualTestPlugin,
|
||||
} from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getRegisteredEmbeddingProvider } from "../embedding-providers.js";
|
||||
import {
|
||||
getRegisteredEmbeddingProvider,
|
||||
type EmbeddingProviderAdapter,
|
||||
} from "../embedding-providers.js";
|
||||
|
||||
describe("embedding provider registration", () => {
|
||||
it("keeps public SDK helpers read-only so plugins cannot bypass manifest ownership", () => {
|
||||
@@ -68,6 +71,74 @@ describe("embedding provider registration", () => {
|
||||
expect(registry.registry.plugins[0]?.embeddingProviderIds).toContain("embedding-owner");
|
||||
});
|
||||
|
||||
it("snapshots adapter fields before runtime lookup", async () => {
|
||||
let idReads = 0;
|
||||
let defaultModelReads = 0;
|
||||
let createReads = 0;
|
||||
let formatSetupErrorReads = 0;
|
||||
const events: string[] = [];
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
|
||||
registerVirtualTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
id: "volatile-embedding-owner",
|
||||
name: "Volatile Embedding Owner",
|
||||
contracts: {
|
||||
embeddingProviders: ["volatile-embedding"],
|
||||
},
|
||||
register(api) {
|
||||
api.registerEmbeddingProvider({
|
||||
marker: "original",
|
||||
get id() {
|
||||
idReads += 1;
|
||||
if (idReads > 1) {
|
||||
throw new Error("embedding id getter re-read");
|
||||
}
|
||||
return " volatile-embedding ";
|
||||
},
|
||||
get defaultModel() {
|
||||
defaultModelReads += 1;
|
||||
if (defaultModelReads > 1) {
|
||||
throw new Error("embedding defaultModel getter re-read");
|
||||
}
|
||||
return "embedding-model";
|
||||
},
|
||||
get create() {
|
||||
createReads += 1;
|
||||
if (createReads > 1) {
|
||||
throw new Error("embedding create getter re-read");
|
||||
}
|
||||
return async function (this: { marker?: string }) {
|
||||
events.push(`create:${this.marker ?? "missing"}`);
|
||||
return { provider: null };
|
||||
};
|
||||
},
|
||||
get formatSetupError() {
|
||||
formatSetupErrorReads += 1;
|
||||
if (formatSetupErrorReads > 1) {
|
||||
throw new Error("embedding formatSetupError getter re-read");
|
||||
}
|
||||
return function (this: { marker?: string }, err: unknown) {
|
||||
return `${this.marker}:${String(err)}`;
|
||||
};
|
||||
},
|
||||
} as EmbeddingProviderAdapter & { marker: string });
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.registry.diagnostics).toEqual([]);
|
||||
const provider = getRegisteredEmbeddingProvider("volatile-embedding");
|
||||
expect(provider?.adapter.defaultModel).toBe("embedding-model");
|
||||
await expect(provider?.adapter.create({} as never)).resolves.toEqual({ provider: null });
|
||||
expect(provider?.adapter.formatSetupError?.("boom")).toBe("original:boom");
|
||||
expect(events).toEqual(["create:original"]);
|
||||
expect(idReads).toBe(1);
|
||||
expect(defaultModelReads).toBe(1);
|
||||
expect(createReads).toBe(1);
|
||||
expect(formatSetupErrorReads).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects duplicate provider ids", () => {
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
registerVirtualTestPlugin,
|
||||
} from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getRegisteredMemoryEmbeddingProvider } from "../memory-embedding-providers.js";
|
||||
import {
|
||||
getRegisteredMemoryEmbeddingProvider,
|
||||
type MemoryEmbeddingProviderAdapter,
|
||||
} from "../memory-embedding-providers.js";
|
||||
import { createPluginRecord } from "../status.test-helpers.js";
|
||||
|
||||
describe("memory embedding provider registration", () => {
|
||||
@@ -57,6 +60,109 @@ describe("memory embedding provider registration", () => {
|
||||
expect(provider?.ownerPluginId).toBe("external-vector");
|
||||
});
|
||||
|
||||
it("skips inactive dual-kind memory adapters before reading adapter fields", () => {
|
||||
let idReads = 0;
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
|
||||
registerVirtualTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
id: "inactive-dual-memory",
|
||||
name: "Inactive Dual Memory",
|
||||
kind: ["memory", "context-engine"],
|
||||
register(api) {
|
||||
api.registerMemoryEmbeddingProvider({
|
||||
get id() {
|
||||
idReads += 1;
|
||||
throw new Error("inactive memory embedding id getter read");
|
||||
},
|
||||
create: async () => ({ provider: null }),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
expect(getRegisteredMemoryEmbeddingProvider("inactive-dual-memory")).toBeUndefined();
|
||||
expect(idReads).toBe(0);
|
||||
expect(registry.registry.diagnostics).toEqual([
|
||||
{
|
||||
pluginId: "inactive-dual-memory",
|
||||
level: "warn",
|
||||
source: "/virtual/inactive-dual-memory/index.ts",
|
||||
message:
|
||||
"dual-kind plugin not selected for memory slot; skipping memory embedding provider registration",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("snapshots adapter fields before memory embedding runtime lookup", async () => {
|
||||
let idReads = 0;
|
||||
let defaultModelReads = 0;
|
||||
let createReads = 0;
|
||||
let shouldContinueReads = 0;
|
||||
const events: string[] = [];
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
|
||||
registerVirtualTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
id: "volatile-memory-vector",
|
||||
name: "Volatile Memory Vector",
|
||||
contracts: {
|
||||
memoryEmbeddingProviders: ["volatile-memory-vector"],
|
||||
},
|
||||
register(api) {
|
||||
api.registerMemoryEmbeddingProvider({
|
||||
marker: "original",
|
||||
get id() {
|
||||
idReads += 1;
|
||||
if (idReads > 1) {
|
||||
throw new Error("memory embedding id getter re-read");
|
||||
}
|
||||
return " volatile-memory-vector ";
|
||||
},
|
||||
get defaultModel() {
|
||||
defaultModelReads += 1;
|
||||
if (defaultModelReads > 1) {
|
||||
throw new Error("memory embedding defaultModel getter re-read");
|
||||
}
|
||||
return "memory-model";
|
||||
},
|
||||
get create() {
|
||||
createReads += 1;
|
||||
if (createReads > 1) {
|
||||
throw new Error("memory embedding create getter re-read");
|
||||
}
|
||||
return async function (this: { marker?: string }) {
|
||||
events.push(`create:${this.marker ?? "missing"}`);
|
||||
return { provider: null };
|
||||
};
|
||||
},
|
||||
get shouldContinueAutoSelection() {
|
||||
shouldContinueReads += 1;
|
||||
if (shouldContinueReads > 1) {
|
||||
throw new Error("memory embedding shouldContinue getter re-read");
|
||||
}
|
||||
return function (this: { marker?: string }) {
|
||||
events.push(`continue:${this.marker ?? "missing"}`);
|
||||
return true;
|
||||
};
|
||||
},
|
||||
} as MemoryEmbeddingProviderAdapter & { marker: string });
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.registry.diagnostics).toEqual([]);
|
||||
const provider = getRegisteredMemoryEmbeddingProvider("volatile-memory-vector");
|
||||
expect(provider?.adapter.defaultModel).toBe("memory-model");
|
||||
await expect(provider?.adapter.create({} as never)).resolves.toEqual({ provider: null });
|
||||
expect(provider?.adapter.shouldContinueAutoSelection?.(new Error("boom"))).toBe(true);
|
||||
expect(events).toEqual(["create:original", "continue:original"]);
|
||||
expect(idReads).toBe(1);
|
||||
expect(defaultModelReads).toBe(1);
|
||||
expect(createReads).toBe(1);
|
||||
expect(shouldContinueReads).toBe(1);
|
||||
});
|
||||
|
||||
it("records the owning memory plugin id for registered adapters", () => {
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
|
||||
|
||||
@@ -1473,7 +1473,10 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
record: PluginRecord,
|
||||
adapter: EmbeddingProviderAdapter,
|
||||
) => {
|
||||
const id = adapter.id.trim();
|
||||
const id = readProviderLikeId(record, "embedding provider", adapter);
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!id) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
@@ -1512,15 +1515,25 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const snapshot = snapshotProviderLike(
|
||||
record,
|
||||
"embedding provider",
|
||||
adapter,
|
||||
id,
|
||||
embeddingProviderAdapterSnapshotFields,
|
||||
);
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
if (registryParams.activateGlobalSideEffects !== false) {
|
||||
registerEmbeddingProvider(adapter, {
|
||||
registerEmbeddingProvider(snapshot, {
|
||||
ownerPluginId: record.id,
|
||||
});
|
||||
}
|
||||
registry.embeddingProviders.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider: adapter,
|
||||
provider: snapshot,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
@@ -1833,6 +1846,26 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
"apply",
|
||||
] as const;
|
||||
|
||||
const embeddingProviderAdapterSnapshotFields = [
|
||||
"defaultModel",
|
||||
"transport",
|
||||
"authProviderId",
|
||||
"create",
|
||||
"formatSetupError",
|
||||
] as const;
|
||||
|
||||
const memoryEmbeddingProviderAdapterSnapshotFields = [
|
||||
"defaultModel",
|
||||
"transport",
|
||||
"authProviderId",
|
||||
"autoSelectPriority",
|
||||
"allowExplicitWhenConfiguredAuto",
|
||||
"supportsMultimodalEmbeddings",
|
||||
"create",
|
||||
"formatSetupError",
|
||||
"shouldContinueAutoSelection",
|
||||
] as const;
|
||||
|
||||
const registerSpeechProvider = (record: PluginRecord, provider: SpeechProviderPlugin) => {
|
||||
const registered = registerUniqueProviderLike({
|
||||
record,
|
||||
@@ -4333,18 +4366,43 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
!(record.contracts?.memoryEmbeddingProviders ?? []).includes(adapter.id)
|
||||
}
|
||||
const id = readProviderLikeId(record, "memory embedding provider", adapter);
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!hasKind(record.kind, "memory") &&
|
||||
!(record.contracts?.memoryEmbeddingProviders ?? []).includes(id)
|
||||
) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: ${adapter.id}`,
|
||||
message: `plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: ${id}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existing = getRegisteredMemoryEmbeddingProvider(adapter.id);
|
||||
if (!id) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "memory embedding provider registration missing id",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const snapshot = snapshotProviderLike(
|
||||
record,
|
||||
"memory embedding provider",
|
||||
adapter,
|
||||
id,
|
||||
memoryEmbeddingProviderAdapterSnapshotFields,
|
||||
);
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
const existing = getRegisteredMemoryEmbeddingProvider(id);
|
||||
if (existing) {
|
||||
const ownerDetail = existing.ownerPluginId
|
||||
? ` (owner: ${existing.ownerPluginId})`
|
||||
@@ -4353,17 +4411,17 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `memory embedding provider already registered: ${adapter.id}${ownerDetail}`,
|
||||
message: `memory embedding provider already registered: ${id}${ownerDetail}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
registerMemoryEmbeddingProvider(adapter, {
|
||||
registerMemoryEmbeddingProvider(snapshot, {
|
||||
ownerPluginId: record.id,
|
||||
});
|
||||
registry.memoryEmbeddingProviders.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
provider: adapter,
|
||||
provider: snapshot,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user