Files
openclaw/src/plugins/memory-runtime.test.ts
T
Peter Steinberger 02e8470bb8 refactor: burn SDK export collision debt (#121888)
* 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
2026-08-11 05:06:11 -07:00

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();
});
});