openclaw/extensions/memory-powermem/index.test.ts

197 lines
6.8 KiB
TypeScript

/**
* Memory (PowerMem) plugin tests.
* Config parsing, plugin registration, and tool behavior with mocked fetch.
*/
import { describe, test, expect, beforeEach, vi } from "vitest";
import { powerMemConfigSchema } from "./config.js";
import { default as memoryPlugin } from "./index.js";
describe("memory-powermem plugin", () => {
test("plugin metadata", () => {
expect(memoryPlugin.id).toBe("memory-powermem");
expect(memoryPlugin.name).toBe("Memory (PowerMem)");
expect(memoryPlugin.kind).toBe("memory");
expect(memoryPlugin.configSchema).toBeDefined();
expect(memoryPlugin.register).toBeInstanceOf(Function);
});
test("config schema parses valid config", () => {
const config = powerMemConfigSchema.parse({
baseUrl: "http://localhost:8000",
autoCapture: true,
autoRecall: true,
inferOnAdd: true,
});
expect(config.baseUrl).toBe("http://localhost:8000");
expect(config.autoCapture).toBe(true);
expect(config.autoRecall).toBe(true);
expect(config.inferOnAdd).toBe(true);
});
test("config schema strips trailing slash from baseUrl", () => {
const config = powerMemConfigSchema.parse({
baseUrl: "http://localhost:8000/",
});
expect(config.baseUrl).toBe("http://localhost:8000");
});
test("config schema rejects missing baseUrl", () => {
expect(() => powerMemConfigSchema.parse({})).toThrow("baseUrl is required");
expect(() => powerMemConfigSchema.parse({ baseUrl: "" })).toThrow(
"baseUrl is required",
);
});
test("config schema resolves env vars", () => {
process.env.TEST_POWERMEM_URL = "http://127.0.0.1:8000";
const config = powerMemConfigSchema.parse({
baseUrl: "${TEST_POWERMEM_URL}",
});
expect(config.baseUrl).toBe("http://127.0.0.1:8000");
delete process.env.TEST_POWERMEM_URL;
});
test("config schema uses default user/agent when not set", () => {
const config = powerMemConfigSchema.parse({ baseUrl: "http://localhost:8000" });
expect(config.userId).toBeUndefined();
expect(config.agentId).toBeUndefined();
});
test("plugin registers tools, CLI, service, and hooks", async () => {
const registeredTools: { tool: unknown; opts: unknown }[] = [];
const registeredClis: { registrar: unknown; opts: unknown }[] = [];
const registeredServices: unknown[] = [];
const registeredHooks: Record<string, unknown[]> = {};
const mockApi = {
pluginConfig: {
baseUrl: "http://localhost:8000",
autoCapture: false,
autoRecall: false,
},
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
registerTool: (tool: unknown, opts: unknown) => {
registeredTools.push({ tool, opts });
},
registerCli: (registrar: unknown, opts: unknown) => {
registeredClis.push({ registrar, opts });
},
registerService: (service: unknown) => {
registeredServices.push(service);
},
on: (hookName: string, handler: unknown) => {
if (!registeredHooks[hookName]) registeredHooks[hookName] = [];
registeredHooks[hookName].push(handler);
},
resolvePath: (p: string) => p,
};
await memoryPlugin.register(mockApi as never);
expect(registeredTools.length).toBe(3);
expect(registeredTools.map((t) => (t.opts as { name?: string })?.name)).toContain(
"memory_recall",
);
expect(registeredTools.map((t) => (t.opts as { name?: string })?.name)).toContain(
"memory_store",
);
expect(registeredTools.map((t) => (t.opts as { name?: string })?.name)).toContain(
"memory_forget",
);
expect(registeredClis.length).toBe(1);
expect(registeredServices.length).toBe(1);
});
test("memory_recall returns error when fetch fails", async () => {
const registeredTools: { tool: { execute: (id: string, params: unknown) => Promise<unknown> }; opts: unknown }[] = [];
const mockApi = {
pluginConfig: { baseUrl: "http://localhost:8000" },
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
registerTool: (tool: unknown, opts: unknown) => {
registeredTools.push({ tool: tool as { execute: (id: string, params: unknown) => Promise<unknown> }, opts });
},
registerCli: () => {},
registerService: () => {},
on: () => {},
resolvePath: (p: string) => p,
};
await memoryPlugin.register(mockApi as never);
const recallTool = registeredTools.find(
(t) => (t.opts as { name?: string })?.name === "memory_recall",
)?.tool;
expect(recallTool).toBeDefined();
// No fetch mock: will fail (or hit real localhost). Use mock to force error.
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
const result = await recallTool!.execute("call-1", {
query: "user preferences",
limit: 5,
});
expect(result).toBeDefined();
const content = (result as { content?: { text?: string }[] })?.content;
expect(Array.isArray(content)).toBe(true);
expect((content as { text: string }[])?.[0]?.text).toContain("Memory search failed");
globalThis.fetch = originalFetch;
});
test("memory_recall returns memories when fetch returns results", async () => {
const registeredTools: { tool: { execute: (id: string, params: unknown) => Promise<unknown> }; opts: unknown }[] = [];
const mockApi = {
pluginConfig: { baseUrl: "http://localhost:8000" },
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
registerTool: (tool: unknown, opts: unknown) => {
registeredTools.push({ tool: tool as { execute: (id: string, params: unknown) => Promise<unknown> }, opts });
},
registerCli: () => {},
registerService: () => {},
on: () => {},
resolvePath: (p: string) => p,
};
await memoryPlugin.register(mockApi as never);
const recallTool = registeredTools.find(
(t) => (t.opts as { name?: string })?.name === "memory_recall",
)?.tool;
expect(recallTool).toBeDefined();
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
text: () =>
Promise.resolve(
JSON.stringify({
success: true,
data: {
results: [
{ memory_id: 1, content: "User likes coffee", score: 0.95 },
],
},
}),
),
} as Response);
const result = await recallTool!.execute("call-1", {
query: "coffee",
limit: 5,
});
expect((result as { details?: { count: number } })?.details?.count).toBe(1);
expect((result as { details?: { memories?: { text: string }[] } })?.details?.memories?.[0]?.text).toBe(
"User likes coffee",
);
globalThis.fetch = originalFetch;
});
});