Files
openclaw/extensions/memory-core/src/tools.test.ts
Peter Steinberger 694ca50e97 Revert "refactor: move runtime state to SQLite"
This reverts commit f91de52f0d.
2026-05-13 13:33:38 +01:00

277 lines
8.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import {
getMemorySearchManagerMockConfigs,
getMemorySearchManagerMockParams,
resetMemoryToolMockState,
setMemoryBackend,
setMemorySearchImpl,
} from "./memory-tool-manager-mock.js";
import { createMemorySearchTool } from "./tools.js";
import { MemoryGetSchema, MemorySearchSchema } from "./tools.shared.js";
import {
asOpenClawConfig,
createMemorySearchToolOrThrow,
expectUnavailableMemorySearchDetails,
} from "./tools.test-helpers.js";
const sessionStore = vi.hoisted(() => ({
"agent:main:main": {
sessionId: "thread-1",
updatedAt: 1,
sessionFile: "/tmp/sessions/thread-1.jsonl",
},
}));
vi.mock("openclaw/plugin-sdk/session-transcript-hit", async (importOriginal) => {
const actual =
await importOriginal<typeof import("openclaw/plugin-sdk/session-transcript-hit")>();
return {
...actual,
loadCombinedSessionStoreForGateway: vi.fn(() => ({
storePath: "(test)",
store: sessionStore,
})),
};
});
describe("memory tool schemas", () => {
it("uses flat corpus enums for provider tool compatibility", () => {
const searchCorpus = MemorySearchSchema.properties.corpus as {
anyOf?: unknown;
enum?: unknown;
};
const getCorpus = MemoryGetSchema.properties.corpus as {
anyOf?: unknown;
enum?: unknown;
};
expect(searchCorpus.anyOf).toBeUndefined();
expect(searchCorpus.enum).toEqual(["memory", "wiki", "all", "sessions"]);
expect(getCorpus.anyOf).toBeUndefined();
expect(getCorpus.enum).toEqual(["memory", "wiki", "all"]);
});
});
describe("memory_search unavailable payloads", () => {
beforeEach(() => {
resetMemoryToolMockState({ searchImpl: async () => [] });
});
it("returns explicit unavailable metadata for quota failures", async () => {
setMemorySearchImpl(async () => {
throw new Error("openai embeddings failed: 429 insufficient_quota");
});
const tool = createMemorySearchToolOrThrow();
const result = await tool.execute("quota", { query: "hello" });
expectUnavailableMemorySearchDetails(result.details, {
error: "openai embeddings failed: 429 insufficient_quota",
warning: "Memory search is unavailable because the embedding provider quota is exhausted.",
action: "Top up or switch embedding provider, then retry memory_search.",
});
});
it("returns explicit unavailable metadata for non-quota failures", async () => {
setMemorySearchImpl(async () => {
throw new Error("embedding provider timeout");
});
const tool = createMemorySearchToolOrThrow();
const result = await tool.execute("generic", { query: "hello" });
expectUnavailableMemorySearchDetails(result.details, {
error: "embedding provider timeout",
warning: "Memory search is unavailable due to an embedding/provider error.",
action: "Check embedding provider configuration and retry memory_search.",
});
});
it("returns structured search debug metadata for qmd results", async () => {
setMemoryBackend("qmd");
setMemorySearchImpl(async (opts) => {
opts?.onDebug?.({
backend: "qmd",
configuredMode: opts.qmdSearchModeOverride ?? "query",
effectiveMode: "query",
fallback: "unsupported-search-flags",
});
return [
{
path: "MEMORY.md",
startLine: 1,
endLine: 2,
score: 0.9,
snippet: "ramen",
source: "memory",
},
];
});
const tool = createMemorySearchToolOrThrow({
config: {
plugins: {
entries: {
"active-memory": {
config: {
qmd: {
searchMode: "search",
},
},
},
},
},
memory: {
backend: "qmd",
qmd: {
searchMode: "query",
limits: {
maxInjectedChars: 1000,
},
},
},
},
agentSessionKey: "agent:main:main:active-memory:debug",
});
const result = await tool.execute("debug", { query: "favorite food" });
const details = result.details as {
mode?: unknown;
debug?: {
backend?: unknown;
configuredMode?: unknown;
effectiveMode?: unknown;
fallback?: unknown;
hits?: unknown;
searchMs?: number;
};
};
expect(details.mode).toBe("query");
expect(details.debug?.backend).toBe("qmd");
expect(details.debug?.configuredMode).toBe("search");
expect(details.debug?.effectiveMode).toBe("query");
expect(details.debug?.fallback).toBe("unsupported-search-flags");
expect(details.debug?.hits).toBe(1);
expect(details.debug?.searchMs).toBeGreaterThanOrEqual(0);
});
});
describe("memory_search corpus labels", () => {
beforeEach(() => {
resetMemoryToolMockState({ searchImpl: async () => [] });
});
it("uses explicit plugin context agent over synthetic active-memory session keys", async () => {
const tool = createMemorySearchToolOrThrow({
config: asOpenClawConfig({
agents: {
list: [
{ id: "main", default: true, memorySearch: { enabled: false } },
{ id: "recall", memorySearch: { enabled: true } },
],
},
}),
agentId: "recall",
agentSessionKey: "explicit:user-session:active-memory:abc123",
});
await tool.execute("recall", { query: "favorite food" });
expect(getMemorySearchManagerMockParams().at(-1)?.agentId).toBe("recall");
});
it("re-resolves config when executing a previously created tool", async () => {
const startupConfig = asOpenClawConfig({
agents: {
defaults: {
memorySearch: {
provider: "ollama",
model: "nomic-embed-text",
},
},
list: [{ id: "main", default: true }],
},
memory: {
backend: "builtin",
},
});
const patchedConfig = asOpenClawConfig({
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
},
},
list: [{ id: "main", default: true }],
},
memory: {
backend: "builtin",
},
});
let liveConfig = startupConfig;
const tool = createMemorySearchTool({
config: startupConfig,
getConfig: () => liveConfig,
});
if (!tool) {
throw new Error("tool missing");
}
liveConfig = patchedConfig;
await tool.execute("patched-config", { query: "provider switch" });
expect(getMemorySearchManagerMockConfigs()).toEqual([patchedConfig]);
});
it("preserves source corpus labels for memory and session transcript hits", async () => {
setMemorySearchImpl(async () => [
{
path: "MEMORY.md",
startLine: 3,
endLine: 4,
score: 0.95,
snippet: "Durable memory note",
source: "memory" as const,
},
{
path: "sessions/thread-1.jsonl",
startLine: 1,
endLine: 2,
score: 0.9,
snippet: "Thread transcript note",
source: "sessions" as const,
},
]);
const tool = createMemorySearchToolOrThrow({
config: {
agents: { list: [{ id: "main", default: true }] },
memory: { citations: "off" },
tools: { sessions: { visibility: "all" } },
},
agentSessionKey: "agent:main:main",
});
const result = await tool.execute("mixed", { query: "thread note" });
const details = result.details as { results: Array<{ corpus: string; path: string }> };
expect(details.results).toEqual([
{
corpus: "memory",
path: "MEMORY.md",
startLine: 3,
endLine: 4,
score: 0.95,
snippet: "Durable memory note",
source: "memory",
},
{
corpus: "sessions",
path: "sessions/thread-1.jsonl",
startLine: 1,
endLine: 2,
score: 0.9,
snippet: "Thread transcript note",
source: "sessions",
},
]);
});
});