mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-15 23:24:03 -06:00
02e8470bb8
* refactor: burn SDK export collision debt * chore: regenerate collision debt baselines * fix: update durable delivery core import * fix: remove stale channel metadata type import * fix: preserve config write SDK parameter type * fix: preserve chat metadata SDK return type * chore: refresh plugin SDK API baseline * test: update plugin enable mock import * fix: remove duplicate status helper re-export * fix: preserve strict QA runtime availability errors
338 lines
13 KiB
TypeScript
338 lines
13 KiB
TypeScript
/** Covers non-activating memory registry handles and requesting-agent workspace ownership. */
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { MemorySearchResult } from "../memory-host-sdk/host/types.js";
|
|
import type { MemoryPluginRuntime } from "./registry-contribution-types.js";
|
|
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
|
import { getPluginRuntimeGatewayRequestScope } from "./runtime/gateway-request-scope.js";
|
|
|
|
type AuthorizeSearchHits = NonNullable<MemoryPluginRuntime["authorizeSearchHits"]>;
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
getMemoryRuntime: vi.fn(),
|
|
loadPluginRegistryHandle: vi.fn(),
|
|
resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)),
|
|
resolveAgentWorkspaceDir: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../agents/agent-scope.js", () => ({
|
|
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
|
}));
|
|
|
|
vi.mock("./loader.js", () => ({
|
|
loadPluginRegistryHandle: mocks.loadPluginRegistryHandle,
|
|
resolvePluginRegistryLoadCacheKey: mocks.resolvePluginRegistryLoadCacheKey,
|
|
}));
|
|
|
|
vi.mock("./memory-state.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("./memory-state.js")>();
|
|
return { ...actual, getMemoryRuntime: mocks.getMemoryRuntime };
|
|
});
|
|
|
|
import {
|
|
authorizeActiveMemorySearchHits,
|
|
closeActiveMemorySearchManagerCore,
|
|
closeActiveMemorySearchManagersCore,
|
|
getActiveMemorySearchManagerCore,
|
|
resolveActiveMemoryBackendConfig,
|
|
} from "./memory-runtime.js";
|
|
import { resetStandaloneMemoryRegistrySlot } from "./memory-runtime.test-support.js";
|
|
import { hasMemoryRuntime } from "./memory-state.js";
|
|
|
|
function createRuntime() {
|
|
return {
|
|
authorizeSearchHits: vi.fn<AuthorizeSearchHits>(async ({ hits }) => hits),
|
|
getMemorySearchManager: vi.fn(async () => ({ manager: null, error: "no index" })),
|
|
resolveMemoryBackendConfig: vi.fn(() => ({ backend: "builtin" as const })),
|
|
closeMemorySearchManager: vi.fn(async () => {}),
|
|
closeAllMemorySearchManagers: vi.fn(async () => {}),
|
|
} satisfies MemoryPluginRuntime;
|
|
}
|
|
|
|
type TestRegistry<T extends MemoryPluginRuntime> = {
|
|
registry: ReturnType<typeof createEmptyPluginRegistry>;
|
|
runtime: T;
|
|
};
|
|
|
|
function createRegistry(): TestRegistry<ReturnType<typeof createRuntime>>;
|
|
function createRegistry<T extends MemoryPluginRuntime>(runtime: T): TestRegistry<T>;
|
|
function createRegistry(
|
|
runtime: MemoryPluginRuntime = createRuntime(),
|
|
): TestRegistry<MemoryPluginRuntime> {
|
|
const registry = createEmptyPluginRegistry();
|
|
registry.memoryCapabilities.push({ pluginId: "memory-core", capability: { runtime } });
|
|
return { registry, runtime };
|
|
}
|
|
|
|
const memoryConfig = {
|
|
plugins: { slots: { memory: "memory-core" } },
|
|
} as never;
|
|
|
|
describe("memory runtime handles", () => {
|
|
beforeEach(() => {
|
|
resetStandaloneMemoryRegistrySlot();
|
|
mocks.getMemoryRuntime.mockReset().mockReturnValue(undefined);
|
|
mocks.loadPluginRegistryHandle.mockReset();
|
|
mocks.resolvePluginRegistryLoadCacheKey.mockClear();
|
|
mocks.resolveAgentWorkspaceDir
|
|
.mockReset()
|
|
.mockImplementation((_cfg, agentId: string) =>
|
|
agentId === "research" ? "/workspace/research" : "/workspace/main",
|
|
);
|
|
});
|
|
|
|
it("loads only the selected memory plugin into a non-activating handle", async () => {
|
|
const { registry, runtime } = createRegistry();
|
|
runtime.getMemorySearchManager.mockImplementationOnce(async () => {
|
|
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(registry);
|
|
return { manager: null, error: "no index" };
|
|
});
|
|
runtime.resolveMemoryBackendConfig.mockImplementationOnce(() => {
|
|
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(registry);
|
|
return { backend: "builtin" };
|
|
});
|
|
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
|
|
|
|
await expect(
|
|
getActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" }),
|
|
).resolves.toEqual({ manager: null, error: "no index" });
|
|
|
|
expect(mocks.loadPluginRegistryHandle).toHaveBeenCalledWith({
|
|
activate: false,
|
|
config: memoryConfig,
|
|
onlyPluginIds: ["memory-core"],
|
|
workspaceDir: "/workspace/main",
|
|
});
|
|
expect(runtime.getMemorySearchManager).toHaveBeenCalledWith({
|
|
cfg: memoryConfig,
|
|
agentId: "main",
|
|
});
|
|
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
|
backend: "builtin",
|
|
});
|
|
});
|
|
|
|
it("tracks standalone managers without activating config-only lookups and rearms reused handles", async () => {
|
|
const { registry, runtime } = createRegistry();
|
|
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
|
|
|
|
expect(hasMemoryRuntime()).toBe(false);
|
|
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
|
backend: "builtin",
|
|
});
|
|
expect(hasMemoryRuntime()).toBe(false);
|
|
|
|
await getActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" });
|
|
expect(hasMemoryRuntime()).toBe(true);
|
|
|
|
await closeActiveMemorySearchManagersCore();
|
|
expect(hasMemoryRuntime()).toBe(false);
|
|
|
|
await getActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" });
|
|
expect(hasMemoryRuntime()).toBe(true);
|
|
expect(mocks.loadPluginRegistryHandle).toHaveBeenCalledTimes(1);
|
|
|
|
await closeActiveMemorySearchManagersCore();
|
|
expect(runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(2);
|
|
expect(hasMemoryRuntime()).toBe(false);
|
|
});
|
|
|
|
it("retains standalone ownership across workspace replacement and per-agent cleanup", async () => {
|
|
const main = createRegistry();
|
|
const research = createRegistry();
|
|
mocks.loadPluginRegistryHandle
|
|
.mockReturnValueOnce(main.registry)
|
|
.mockReturnValueOnce(research.registry);
|
|
|
|
await getActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" });
|
|
await getActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "research" });
|
|
expect(hasMemoryRuntime()).toBe(true);
|
|
|
|
await closeActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" });
|
|
expect(hasMemoryRuntime()).toBe(true);
|
|
|
|
await closeActiveMemorySearchManagersCore();
|
|
expect(main.runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(1);
|
|
expect(research.runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(1);
|
|
expect(hasMemoryRuntime()).toBe(false);
|
|
});
|
|
|
|
it("retains standalone cleanup ownership when manager acquisition or teardown fails", async () => {
|
|
const { registry, runtime } = createRegistry();
|
|
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
|
|
runtime.getMemorySearchManager.mockRejectedValueOnce(
|
|
new Error("manager initialization failed"),
|
|
);
|
|
|
|
await expect(
|
|
getActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" }),
|
|
).rejects.toThrow("manager initialization failed");
|
|
expect(hasMemoryRuntime()).toBe(true);
|
|
|
|
runtime.closeAllMemorySearchManagers.mockRejectedValueOnce(
|
|
new Error("manager teardown failed"),
|
|
);
|
|
await expect(closeActiveMemorySearchManagersCore()).rejects.toThrow("manager teardown failed");
|
|
expect(hasMemoryRuntime()).toBe(true);
|
|
|
|
await closeActiveMemorySearchManagersCore();
|
|
expect(hasMemoryRuntime()).toBe(false);
|
|
});
|
|
|
|
it("keys the single slot by the requesting agent workspace", () => {
|
|
const main = createRegistry();
|
|
const research = createRegistry();
|
|
mocks.loadPluginRegistryHandle
|
|
.mockReturnValueOnce(main.registry)
|
|
.mockReturnValueOnce(research.registry);
|
|
|
|
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
|
backend: "builtin",
|
|
});
|
|
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
|
backend: "builtin",
|
|
});
|
|
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "research" })).toEqual({
|
|
backend: "builtin",
|
|
});
|
|
|
|
expect(mocks.resolveAgentWorkspaceDir).toHaveBeenNthCalledWith(1, memoryConfig, "main");
|
|
expect(mocks.resolveAgentWorkspaceDir).toHaveBeenLastCalledWith(memoryConfig, "research");
|
|
expect(mocks.loadPluginRegistryHandle).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it.each([
|
|
{ plugins: { enabled: false } },
|
|
{ plugins: { slots: { memory: "none" } } },
|
|
{ plugins: { slots: { memory: "memory-core" }, deny: ["memory-core"] } },
|
|
{
|
|
plugins: {
|
|
slots: { memory: "memory-core" },
|
|
entries: { "memory-core": { enabled: false } },
|
|
},
|
|
},
|
|
])("does not load a disabled memory selection", async (cfg) => {
|
|
await expect(
|
|
getActiveMemorySearchManagerCore({ cfg: cfg as never, agentId: "main" }),
|
|
).resolves.toEqual({ manager: null, error: "memory plugin unavailable" });
|
|
expect(mocks.loadPluginRegistryHandle).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("prefers an already-registered runtime", () => {
|
|
const runtime = createRuntime();
|
|
mocks.getMemoryRuntime.mockReturnValue(runtime);
|
|
|
|
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
|
|
backend: "builtin",
|
|
});
|
|
expect(mocks.loadPluginRegistryHandle).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("authorizes raw hits inside the selected plugin runtime scope", async () => {
|
|
const { registry, runtime } = createRegistry();
|
|
runtime.authorizeSearchHits.mockImplementationOnce(async ({ hits }) => {
|
|
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(registry);
|
|
return hits.filter((hit) => hit.source === "memory");
|
|
});
|
|
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
|
|
const hits: MemorySearchResult[] = [
|
|
{
|
|
source: "memory",
|
|
path: "memory.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 1,
|
|
snippet: "memory",
|
|
},
|
|
{
|
|
source: "sessions",
|
|
path: "sessions/private.jsonl",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 1,
|
|
snippet: "private",
|
|
},
|
|
];
|
|
|
|
await expect(
|
|
authorizeActiveMemorySearchHits({
|
|
cfg: memoryConfig,
|
|
agentId: "main",
|
|
requesterSessionKey: "agent:main:voice:15550001234",
|
|
sandboxed: false,
|
|
hits,
|
|
}),
|
|
).resolves.toEqual([hits[0]]);
|
|
});
|
|
|
|
it("fails closed on session hits when a memory runtime has no authorizer", async () => {
|
|
const runtimeWithoutAuthorizer = {
|
|
getMemorySearchManager: vi.fn(async () => ({ manager: null, error: "no index" })),
|
|
resolveMemoryBackendConfig: vi.fn(() => ({ backend: "builtin" as const })),
|
|
closeMemorySearchManager: vi.fn(async () => {}),
|
|
closeAllMemorySearchManagers: vi.fn(async () => {}),
|
|
} satisfies MemoryPluginRuntime;
|
|
mocks.loadPluginRegistryHandle.mockReturnValue(
|
|
createRegistry(runtimeWithoutAuthorizer).registry,
|
|
);
|
|
const hits: MemorySearchResult[] = [
|
|
{
|
|
source: "memory",
|
|
path: "memory.md",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 1,
|
|
snippet: "memory",
|
|
},
|
|
{
|
|
source: "sessions",
|
|
path: "sessions/private.jsonl",
|
|
startLine: 1,
|
|
endLine: 1,
|
|
score: 1,
|
|
snippet: "private",
|
|
},
|
|
];
|
|
|
|
await expect(
|
|
authorizeActiveMemorySearchHits({
|
|
cfg: memoryConfig,
|
|
agentId: "main",
|
|
requesterSessionKey: "agent:main:voice:15550001234",
|
|
sandboxed: false,
|
|
hits,
|
|
}),
|
|
).resolves.toEqual([hits[0]]);
|
|
});
|
|
|
|
it("closes managers through current and retired workspace handles without reloading", async () => {
|
|
const main = createRegistry();
|
|
const research = createRegistry();
|
|
for (const owner of [main, research]) {
|
|
owner.runtime.closeMemorySearchManager.mockImplementationOnce(async () => {
|
|
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(owner.registry);
|
|
});
|
|
owner.runtime.closeAllMemorySearchManagers.mockImplementationOnce(async () => {
|
|
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(owner.registry);
|
|
});
|
|
}
|
|
mocks.loadPluginRegistryHandle
|
|
.mockReturnValueOnce(main.registry)
|
|
.mockReturnValueOnce(research.registry);
|
|
resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" });
|
|
resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "research" });
|
|
mocks.loadPluginRegistryHandle.mockClear();
|
|
|
|
await closeActiveMemorySearchManagerCore({ cfg: memoryConfig, agentId: "main" });
|
|
await closeActiveMemorySearchManagersCore(memoryConfig);
|
|
|
|
for (const { runtime } of [main, research]) {
|
|
expect(runtime.closeMemorySearchManager).toHaveBeenCalledWith({
|
|
cfg: memoryConfig,
|
|
agentId: "main",
|
|
});
|
|
expect(runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(1);
|
|
}
|
|
expect(mocks.loadPluginRegistryHandle).not.toHaveBeenCalled();
|
|
});
|
|
});
|