import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
import type { OpenClawConfig } from "../api.js" ;
import { compileMemoryWikiVault } from "./compile.js" ;
import type { MemoryWikiPluginConfig } from "./config.js" ;
import { renderWikiMarkdown } from "./markdown.js" ;
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js" ;
import { createMemoryWikiTestHarness } from "./test-helpers.js" ;
const { getActiveMemorySearchManagerMock, resolveDefaultAgentIdMock, resolveSessionAgentIdMock } =
vi.hoisted(() => ({
getActiveMemorySearchManagerMock: vi.fn(),
resolveDefaultAgentIdMock: vi.fn(() => "main" ),
resolveSessionAgentIdMock: vi.fn(({ sessionKey }: { sessionKey?: string }) =>
sessionKey === "agent:secondary:thread" ? "secondary" : "main" ,
),
}));
vi.mock("openclaw/plugin-sdk/memory-host-search" , () => ({
getActiveMemorySearchManager: getActiveMemorySearchManagerMock,
}));
vi.mock("openclaw/plugin-sdk/memory-host-core" , () => ({
resolveDefaultAgentId: resolveDefaultAgentIdMock,
resolveSessionAgentId: resolveSessionAgentIdMock,
}));
const { createVault } = createMemoryWikiTestHarness();
let suiteRoot = "" ;
let caseIndex = 0 ;
beforeEach(() => {
getActiveMemorySearchManagerMock.mockReset();
getActiveMemorySearchManagerMock.mockResolvedValue({ manager: null , error: "unavailable" });
resolveDefaultAgentIdMock.mockClear();
resolveSessionAgentIdMock.mockClear();
});
beforeAll(async () => {
suiteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-suite-" ));
});
afterAll(async () => {
if (suiteRoot) {
await fs.rm(suiteRoot, { recursive: true , force: true });
}
});
async function createQueryVault(options?: {
config?: MemoryWikiPluginConfig;
initialize?: boolean ;
}) {
return createVault({
prefix: "memory-wiki-query-" ,
rootDir: path.join(suiteRoot, `case -${caseIndex++}`),
initialize: options?.initialize,
config: options?.config,
});
}
function createAppConfig(): OpenClawConfig {
return {
agents: {
list: [{ id: "main" , default : true }],
},
} as OpenClawConfig;
}
function createMemoryManager(overrides?: {
searchResults?: Array<{
path: string;
startLine: number;
endLine: number;
score: number;
snippet: string;
source: "memory" | "sessions" ;
citation?: string;
}>;
readResult?: { text: string; path: string };
}) {
return {
search: vi.fn().mockResolvedValue(overrides?.searchResults ?? []),
readFile: vi.fn().mockImplementation(async () => {
if (!overrides?.readResult) {
throw new Error("missing" );
}
return overrides.readResult;
}),
status: vi.fn().mockReturnValue({ backend: "builtin" , provider: "builtin" }),
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
probeVectorAvailability: vi.fn().mockResolvedValue(false ),
close: vi.fn().mockResolvedValue(undefined),
};
}
describe("searchMemoryWiki" , () => {
it("finds wiki pages by title and body" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "sources" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: { pageType: "source" , id: "source.alpha" , title: "Alpha Source" },
body: "# Alpha Source\n\nalpha body text\n" ,
}),
"utf8" ,
);
const results = await searchMemoryWiki({ config, query: "alpha" });
expect(results).toHaveLength(1 );
expect(results[0 ]?.corpus).toBe("wiki" );
expect(results[0 ]?.path).toBe("sources/alpha.md" );
expect(getActiveMemorySearchManagerMock).not.toHaveBeenCalled();
});
it("finds wiki pages by structured claim text and surfaces the claim as the snippet" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "entities" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: {
pageType: "entity" ,
id: "entity.alpha" ,
title: "Alpha" ,
claims: [
{
id: "claim.alpha.postgres" ,
text: "Alpha uses PostgreSQL for production writes." ,
status: "supported" ,
confidence: 0 .91 ,
evidence: [{ sourceId: "source.alpha" , lines: "12-18" }],
},
],
},
body: "# Alpha\n\nsummary without the query phrase\n" ,
}),
"utf8" ,
);
const results = await searchMemoryWiki({ config, query: "postgresql" });
expect(results).toHaveLength(1 );
expect(results[0 ]).toMatchObject({
corpus: "wiki" ,
path: "entities/alpha.md" ,
snippet: "Alpha uses PostgreSQL for production writes." ,
});
});
it("ranks fresh supported claims ahead of stale contested claims" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "entities" , "alpha-fresh.md" ),
renderWikiMarkdown({
frontmatter: {
pageType: "entity" ,
id: "entity.alpha.fresh" ,
title: "Alpha Fresh" ,
updatedAt: "2026-04-01T00:00:00.000Z" ,
claims: [
{
id: "claim.alpha.db.fresh" ,
text: "Alpha uses PostgreSQL for production writes." ,
status: "supported" ,
confidence: 0 .91 ,
evidence: [
{
sourceId: "source.alpha" ,
lines: "4-7" ,
updatedAt: "2026-04-01T00:00:00.000Z" ,
},
],
},
],
},
body: "# Alpha Fresh\n\nsummary without the keyword\n" ,
}),
"utf8" ,
);
await fs.writeFile(
path.join(rootDir, "entities" , "alpha-stale.md" ),
renderWikiMarkdown({
frontmatter: {
pageType: "entity" ,
id: "entity.alpha.stale" ,
title: "Alpha Stale" ,
updatedAt: "2025-10-01T00:00:00.000Z" ,
claims: [
{
id: "claim.alpha.db.stale" ,
text: "Alpha uses PostgreSQL for production writes." ,
status: "contested" ,
confidence: 0 .92 ,
evidence: [
{
sourceId: "source.alpha.old" ,
lines: "1-2" ,
updatedAt: "2025-10-01T00:00:00.000Z" ,
},
],
},
],
},
body: "# Alpha Stale\n\nsummary without the keyword\n" ,
}),
"utf8" ,
);
const results = await searchMemoryWiki({ config, query: "postgresql" });
expect(results).toHaveLength(2 );
expect(results[0 ]?.path).toBe("entities/alpha-fresh.md" );
expect(results[1 ]?.path).toBe("entities/alpha-stale.md" );
});
it("surfaces bridge provenance for imported source pages" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "sources" , "bridge-alpha.md" ),
renderWikiMarkdown({
frontmatter: {
pageType: "source" ,
id: "source.bridge.alpha" ,
title: "Bridge Alpha" ,
sourceType: "memory-bridge" ,
sourcePath: "/tmp/workspace/MEMORY.md" ,
bridgeRelativePath: "MEMORY.md" ,
bridgeWorkspaceDir: "/tmp/workspace" ,
updatedAt: "2026-04-05T12:00:00.000Z" ,
},
body: "# Bridge Alpha\n\nalpha bridge body\n" ,
}),
"utf8" ,
);
const results = await searchMemoryWiki({ config, query: "alpha" });
expect(results).toHaveLength(1 );
expect(results[0 ]).toMatchObject({
corpus: "wiki" ,
sourceType: "memory-bridge" ,
sourcePath: "/tmp/workspace/MEMORY.md" ,
provenanceLabel: "bridge: MEMORY.md" ,
updatedAt: "2026-04-05T12:00:00.000Z" ,
});
});
it("includes active memory results when shared search and all corpora are enabled" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "shared" , corpus: "all" },
},
});
await fs.writeFile(
path.join(rootDir, "sources" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: { pageType: "source" , id: "source.alpha" , title: "Alpha Source" },
body: "# Alpha Source\n\nalpha body text\n" ,
}),
"utf8" ,
);
const manager = createMemoryManager({
searchResults: [
{
path: "MEMORY.md" ,
startLine: 4 ,
endLine: 8 ,
score: 42 ,
snippet: "alpha durable memory" ,
source: "memory" ,
citation: "MEMORY.md#L4-L8" ,
},
],
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
const results = await searchMemoryWiki({
config,
appConfig: createAppConfig(),
query: "alpha" ,
maxResults: 5 ,
});
expect(results).toHaveLength(2 );
expect(results.some((result) => result.corpus === "wiki" )).toBe(true );
expect(results.some((result) => result.corpus === "memory" )).toBe(true );
expect(manager.search).toHaveBeenCalledWith("alpha" , { maxResults: 5 });
expect(getActiveMemorySearchManagerMock).toHaveBeenCalledWith({
cfg: createAppConfig(),
agentId: "main" ,
});
});
it("uses the active session agent for shared memory search" , async () => {
const { config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "shared" , corpus: "memory" },
},
});
const manager = createMemoryManager({
searchResults: [
{
path: "memory/2026-04-07.md" ,
startLine: 1 ,
endLine: 2 ,
score: 1 ,
snippet: "secondary agent memory" ,
source: "memory" ,
},
],
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
await searchMemoryWiki({
config,
appConfig: createAppConfig(),
agentSessionKey: "agent:secondary:thread" ,
query: "secondary" ,
});
expect(resolveSessionAgentIdMock).toHaveBeenCalledWith({
sessionKey: "agent:secondary:thread" ,
config: createAppConfig(),
});
expect(getActiveMemorySearchManagerMock).toHaveBeenCalledWith({
cfg: createAppConfig(),
agentId: "secondary" ,
});
});
it("allows per-call corpus overrides without changing config defaults" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "shared" , corpus: "wiki" },
},
});
await fs.writeFile(
path.join(rootDir, "sources" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: { pageType: "source" , id: "source.alpha" , title: "Alpha Source" },
body: "# Alpha Source\n\nalpha body text\n" ,
}),
"utf8" ,
);
const manager = createMemoryManager({
searchResults: [
{
path: "MEMORY.md" ,
startLine: 10 ,
endLine: 12 ,
score: 99 ,
snippet: "memory-only alpha" ,
source: "memory" ,
},
],
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
const memoryOnly = await searchMemoryWiki({
config,
appConfig: createAppConfig(),
query: "alpha" ,
searchCorpus: "memory" ,
});
expect(memoryOnly).toHaveLength(1 );
expect(memoryOnly[0 ]?.corpus).toBe("memory" );
expect(manager.search).toHaveBeenCalledWith("alpha" , { maxResults: 10 });
});
it("keeps memory search disabled when the backend is local" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "local" , corpus: "all" },
},
});
await fs.writeFile(
path.join(rootDir, "sources" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: { pageType: "source" , id: "source.alpha" , title: "Alpha Source" },
body: "# Alpha Source\n\nalpha only wiki\n" ,
}),
"utf8" ,
);
const manager = createMemoryManager({
searchResults: [
{
path: "MEMORY.md" ,
startLine: 1 ,
endLine: 2 ,
score: 50 ,
snippet: "alpha memory" ,
source: "memory" ,
},
],
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
const results = await searchMemoryWiki({
config,
appConfig: createAppConfig(),
query: "alpha" ,
});
expect(results).toHaveLength(1 );
expect(results[0 ]?.corpus).toBe("wiki" );
expect(manager.search).not.toHaveBeenCalled();
});
});
describe("getMemoryWikiPage" , () => {
it("reads wiki pages by relative path and slices line ranges" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "sources" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: { pageType: "source" , id: "source.alpha" , title: "Alpha Source" },
body: "# Alpha Source\n\nline one\nline two\nline three\n" ,
}),
"utf8" ,
);
const result = await getMemoryWikiPage({
config,
lookup: "sources/alpha.md" ,
fromLine: 4 ,
lineCount: 2 ,
});
expect(result?.corpus).toBe("wiki" );
expect(result?.path).toBe("sources/alpha.md" );
expect(result?.content).toContain("line one" );
expect(result?.content).toContain("line two" );
expect(result?.content).not.toContain("line three" );
expect(result?.totalLines).toBe(7 );
expect(result?.truncated).toBe(true );
});
it("resolves compiled claim ids back to the owning page" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "entities" , "alpha.md" ),
renderWikiMarkdown({
frontmatter: {
pageType: "entity" ,
id: "entity.alpha" ,
title: "Alpha" ,
claims: [
{
id: "claim.alpha.db" ,
text: "Alpha uses PostgreSQL for production writes." ,
status: "supported" ,
evidence: [{ sourceId: "source.alpha" , lines: "1-2" }],
},
],
},
body: "# Alpha\n\nline one\nline two\n" ,
}),
"utf8" ,
);
await compileMemoryWikiVault(config);
const result = await getMemoryWikiPage({
config,
lookup: "claim.alpha.db" ,
});
expect(result).toMatchObject({
corpus: "wiki" ,
path: "entities/alpha.md" ,
title: "Alpha" ,
id: "entity.alpha" ,
});
expect(result?.content).toContain("line one" );
});
it("returns provenance for imported wiki source pages" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
});
await fs.writeFile(
path.join(rootDir, "sources" , "unsafe-alpha.md" ),
renderWikiMarkdown({
frontmatter: {
pageType: "source" ,
id: "source.unsafe.alpha" ,
title: "Unsafe Alpha" ,
sourceType: "memory-unsafe-local" ,
provenanceMode: "unsafe-local" ,
sourcePath: "/tmp/private/alpha.md" ,
unsafeLocalConfiguredPath: "/tmp/private" ,
unsafeLocalRelativePath: "alpha.md" ,
updatedAt: "2026-04-05T13:00:00.000Z" ,
},
body: "# Unsafe Alpha\n\nsecret alpha\n" ,
}),
"utf8" ,
);
const result = await getMemoryWikiPage({
config,
lookup: "sources/unsafe-alpha.md" ,
});
expect(result).toMatchObject({
corpus: "wiki" ,
path: "sources/unsafe-alpha.md" ,
sourceType: "memory-unsafe-local" ,
provenanceMode: "unsafe-local" ,
sourcePath: "/tmp/private/alpha.md" ,
provenanceLabel: "unsafe-local: alpha.md" ,
updatedAt: "2026-04-05T13:00:00.000Z" ,
});
});
it("falls back to active memory reads when memory corpus is selected" , async () => {
const { config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "shared" , corpus: "memory" },
},
});
const manager = createMemoryManager({
readResult: {
path: "MEMORY.md" ,
text: "durable alpha memory\nline two" ,
},
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
const result = await getMemoryWikiPage({
config,
appConfig: createAppConfig(),
lookup: "MEMORY.md" ,
fromLine: 2 ,
lineCount: 2 ,
});
expect(result).toEqual({
corpus: "memory" ,
path: "MEMORY.md" ,
title: "MEMORY" ,
kind: "memory" ,
content: "durable alpha memory\nline two" ,
fromLine: 2 ,
lineCount: 2 ,
});
expect(manager.readFile).toHaveBeenCalledWith({
relPath: "MEMORY.md" ,
from: 2 ,
lines: 2 ,
});
});
it("uses the active session agent for shared memory reads" , async () => {
const { config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "shared" , corpus: "memory" },
},
});
const manager = createMemoryManager({
readResult: {
path: "MEMORY.md" ,
text: "secondary memory line" ,
},
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
const result = await getMemoryWikiPage({
config,
appConfig: createAppConfig(),
agentSessionKey: "agent:secondary:thread" ,
lookup: "MEMORY.md" ,
});
expect(result?.corpus).toBe("memory" );
expect(resolveSessionAgentIdMock).toHaveBeenCalledWith({
sessionKey: "agent:secondary:thread" ,
config: createAppConfig(),
});
expect(getActiveMemorySearchManagerMock).toHaveBeenCalledWith({
cfg: createAppConfig(),
agentId: "secondary" ,
});
});
it("allows per-call get overrides to bypass wiki and force memory fallback" , async () => {
const { rootDir, config } = await createQueryVault({
initialize: true ,
config: {
search: { backend: "shared" , corpus: "wiki" },
},
});
await fs.writeFile(
path.join(rootDir, "sources" , "MEMORY.md" ),
renderWikiMarkdown({
frontmatter: { pageType: "source" , id: "source.memory.shadow" , title: "Shadow Memory" },
body: "# Shadow Memory\n\nwiki copy\n" ,
}),
"utf8" ,
);
const manager = createMemoryManager({
readResult: {
path: "MEMORY.md" ,
text: "forced memory read" ,
},
});
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
const result = await getMemoryWikiPage({
config,
appConfig: createAppConfig(),
lookup: "MEMORY.md" ,
searchCorpus: "memory" ,
});
expect(result?.corpus).toBe("memory" );
expect(result?.content).toBe("forced memory read" );
expect(manager.readFile).toHaveBeenCalled();
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-04)
¤
*© Formatika GbR, Deutschland