mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 12:04:45 +00:00
Summary: - The PR updates memory-core `memory_search` result shaping to surface `corpus` from each hit's `source`, adds ... session corpus-label coverage, adds a changelog entry, and includes a small tempdir test assertion cleanup. - Reproducibility: yes. Current main has a high-confidence source-level reproduction: session hits keep `sourc ... the final mapper hard-codes `corpus: "memory"`; the PR body also supplies live Gateway before/after output. Automerge notes: - PR branch already contained follow-up commit before automerge: test(memory): clarify corpus label regression - PR branch already contained follow-up commit before automerge: fix(memory): type session corpus results - PR branch already contained follow-up commit before automerge: fix(memory): preserve session corpus labels - PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-7189… Validation: - ClawSweeper review passed for head02d0db0861. - Required merge gates passed before the squash merge. Prepared head SHA:02d0db0861Review: https://github.com/openclaw/openclaw/pull/71898#issuecomment-4340800992 Co-authored-by: Ruben Cuevas <hi@rubencu.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
253 lines
7.1 KiB
TypeScript
253 lines
7.1 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 {
|
|
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_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" });
|
|
expect(result.details).toMatchObject({
|
|
mode: "query",
|
|
debug: {
|
|
backend: "qmd",
|
|
configuredMode: "search",
|
|
effectiveMode: "query",
|
|
fallback: "unsupported-search-flags",
|
|
hits: 1,
|
|
},
|
|
});
|
|
expect(
|
|
(result.details as { debug?: { searchMs?: number } }).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",
|
|
},
|
|
]);
|
|
});
|
|
});
|