mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
271 lines
9.4 KiB
TypeScript
271 lines
9.4 KiB
TypeScript
// Memory Wiki tests cover agent-scoped vault isolation through the public tools.
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers";
|
|
import {
|
|
clearMemoryPluginState,
|
|
registerMemoryCorpusSupplement,
|
|
} from "openclaw/plugin-sdk/memory-host-core";
|
|
import type { AnyAgentTool, OpenClawPluginToolFactory } from "openclaw/plugin-sdk/plugin-entry";
|
|
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
|
|
import { describe, expect, it } from "vitest";
|
|
import memoryCorePlugin from "../../memory-core/index.js";
|
|
import type { OpenClawConfig } from "../api.js";
|
|
import {
|
|
resolveMemoryWikiAgentConfig,
|
|
resolveMemoryWikiConfig,
|
|
type ResolvedMemoryWikiConfig,
|
|
} from "./config.js";
|
|
import { createWikiCorpusSupplement } from "./corpus-supplement.js";
|
|
import { createMemoryWikiTestHarness } from "./test-helpers.js";
|
|
import { createWikiApplyTool, createWikiGetTool, createWikiSearchTool } from "./tool.js";
|
|
|
|
const { createTempDir } = createMemoryWikiTestHarness();
|
|
|
|
function assertToolDetailsRecord(value: unknown): Record<string, unknown> {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new Error("Expected tool details object");
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function textContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
return result.content.find((part) => part.type === "text")?.text ?? "";
|
|
}
|
|
|
|
function registerMemoryCoreToolFactories(
|
|
appConfig: OpenClawConfig,
|
|
): Map<string, OpenClawPluginToolFactory> {
|
|
const factories = new Map<string, OpenClawPluginToolFactory>();
|
|
memoryCorePlugin.register(
|
|
createTestPluginApi({
|
|
id: "memory-core",
|
|
config: appConfig,
|
|
runtime: createPluginRuntimeMock(),
|
|
registerTool(tool, options) {
|
|
if (typeof tool !== "function") {
|
|
return;
|
|
}
|
|
for (const name of options?.names ?? []) {
|
|
factories.set(name, tool);
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
return factories;
|
|
}
|
|
|
|
function createMemoryCoreTool(params: {
|
|
factories: Map<string, OpenClawPluginToolFactory>;
|
|
name: "memory_search" | "memory_get";
|
|
appConfig: OpenClawConfig;
|
|
agentId: string;
|
|
}): AnyAgentTool {
|
|
const factory = params.factories.get(params.name);
|
|
if (!factory) {
|
|
throw new Error(`Expected memory-core to register ${params.name}`);
|
|
}
|
|
const tool = factory({
|
|
config: params.appConfig,
|
|
runtimeConfig: params.appConfig,
|
|
getRuntimeConfig: () => params.appConfig,
|
|
agentId: params.agentId,
|
|
sessionKey: `agent:${params.agentId}:main`,
|
|
});
|
|
if (!tool || Array.isArray(tool)) {
|
|
throw new Error(`Expected one ${params.name} tool`);
|
|
}
|
|
return tool;
|
|
}
|
|
|
|
describe("agent-scoped memory-wiki tools", () => {
|
|
it("keeps apply, search, and get behavior isolated by configured agent", async () => {
|
|
const vaultParent = await createTempDir("memory-wiki-agent-vaults-");
|
|
const appConfig = {
|
|
// This suite registers memory-core directly; runtime discovery would load unrelated plugins.
|
|
plugins: { enabled: false },
|
|
agents: {
|
|
list: [{ id: "support", default: true }, { id: "marketing" }],
|
|
},
|
|
} as OpenClawConfig;
|
|
const baseConfig = resolveMemoryWikiConfig({
|
|
vault: { scope: "agent", path: vaultParent },
|
|
search: { backend: "local", corpus: "wiki" },
|
|
});
|
|
|
|
const agents: Array<{
|
|
id: string;
|
|
title: string;
|
|
sentinel: string;
|
|
config: ResolvedMemoryWikiConfig;
|
|
pagePath?: string;
|
|
}> = [
|
|
{
|
|
id: "support",
|
|
title: "Support Private Synthesis",
|
|
sentinel: "SUPPORT_ONLY_7f3c21",
|
|
config: resolveMemoryWikiAgentConfig({
|
|
config: baseConfig,
|
|
appConfig,
|
|
agentId: "support",
|
|
}),
|
|
},
|
|
{
|
|
id: "marketing",
|
|
title: "Marketing Private Synthesis",
|
|
sentinel: "MARKETING_ONLY_8e4d62",
|
|
config: resolveMemoryWikiAgentConfig({
|
|
config: baseConfig,
|
|
appConfig,
|
|
agentId: "marketing",
|
|
}),
|
|
},
|
|
];
|
|
|
|
for (const agent of agents) {
|
|
const result = await createWikiApplyTool(agent.config, appConfig).execute(
|
|
`apply-${agent.id}`,
|
|
{
|
|
op: "create_synthesis",
|
|
title: agent.title,
|
|
body: `Private synthesis marker: ${agent.sentinel}`,
|
|
sourceIds: [`source.${agent.id}`],
|
|
},
|
|
);
|
|
const pagePath = assertToolDetailsRecord(result.details).pagePath;
|
|
if (typeof pagePath !== "string") {
|
|
throw new Error("Expected wiki_apply to return pagePath");
|
|
}
|
|
agent.pagePath = pagePath;
|
|
}
|
|
|
|
expect(agents[0]?.config.vault.path).toBe(path.join(vaultParent, "support"));
|
|
expect(agents[1]?.config.vault.path).toBe(path.join(vaultParent, "marketing"));
|
|
expect(agents[0]?.config.vault.path).not.toBe(agents[1]?.config.vault.path);
|
|
|
|
for (const agent of agents) {
|
|
const foreignAgent = agents.find((candidate) => candidate.id !== agent.id);
|
|
if (!agent.pagePath || !foreignAgent?.pagePath) {
|
|
throw new Error("Expected both agent synthesis paths");
|
|
}
|
|
|
|
expect((await fs.stat(agent.config.vault.path)).isDirectory()).toBe(true);
|
|
await expect(
|
|
fs.readFile(path.join(agent.config.vault.path, agent.pagePath), "utf8"),
|
|
).resolves.toContain(agent.sentinel);
|
|
await expect(
|
|
fs.access(path.join(agent.config.vault.path, foreignAgent.pagePath)),
|
|
).rejects.toThrow();
|
|
|
|
const searchTool = createWikiSearchTool(agent.config, appConfig, {
|
|
agentId: agent.id,
|
|
});
|
|
const ownSearch = await searchTool.execute(`search-own-${agent.id}`, {
|
|
query: agent.sentinel,
|
|
});
|
|
expect(textContent(ownSearch)).toContain(agent.sentinel);
|
|
expect(assertToolDetailsRecord(ownSearch.details).results).toEqual([
|
|
expect.objectContaining({ path: agent.pagePath }),
|
|
]);
|
|
|
|
const foreignSearch = await searchTool.execute(`search-foreign-${agent.id}`, {
|
|
query: foreignAgent.sentinel,
|
|
});
|
|
expect(textContent(foreignSearch)).toBe("No wiki or memory results.");
|
|
expect(assertToolDetailsRecord(foreignSearch.details).results).toEqual([]);
|
|
|
|
const getTool = createWikiGetTool(agent.config, appConfig, { agentId: agent.id });
|
|
const ownGet = await getTool.execute(`get-own-${agent.id}`, { lookup: agent.pagePath });
|
|
expect(textContent(ownGet)).toContain(agent.sentinel);
|
|
expect(assertToolDetailsRecord(ownGet.details).found).toBe(true);
|
|
|
|
const foreignGet = await getTool.execute(`get-foreign-${agent.id}`, {
|
|
lookup: foreignAgent.pagePath,
|
|
});
|
|
expect(textContent(foreignGet)).toBe(`Wiki page not found: ${foreignAgent.pagePath}`);
|
|
expect(assertToolDetailsRecord(foreignGet.details).found).toBe(false);
|
|
}
|
|
|
|
clearMemoryPluginState();
|
|
try {
|
|
registerMemoryCorpusSupplement(
|
|
"memory-wiki",
|
|
createWikiCorpusSupplement({
|
|
resolveConfig: (agentId, currentAppConfig) =>
|
|
resolveMemoryWikiAgentConfig({
|
|
config: baseConfig,
|
|
appConfig: currentAppConfig,
|
|
agentId,
|
|
}),
|
|
getAppConfig: () => appConfig,
|
|
}),
|
|
);
|
|
const memoryCoreFactories = registerMemoryCoreToolFactories(appConfig);
|
|
|
|
for (const agent of agents) {
|
|
const foreignAgent = agents.find((candidate) => candidate.id !== agent.id);
|
|
if (!agent.pagePath || !foreignAgent?.pagePath) {
|
|
throw new Error("Expected both agent synthesis paths");
|
|
}
|
|
|
|
const memorySearch = createMemoryCoreTool({
|
|
factories: memoryCoreFactories,
|
|
name: "memory_search",
|
|
appConfig,
|
|
agentId: agent.id,
|
|
});
|
|
const ownMemorySearch = await memorySearch.execute(`memory-search-own-${agent.id}`, {
|
|
query: agent.sentinel,
|
|
corpus: "wiki",
|
|
});
|
|
expect(assertToolDetailsRecord(ownMemorySearch.details).results).toEqual([
|
|
expect.objectContaining({
|
|
corpus: "wiki",
|
|
path: agent.pagePath,
|
|
snippet: expect.stringContaining(agent.sentinel),
|
|
}),
|
|
]);
|
|
|
|
const foreignMemorySearch = await memorySearch.execute(
|
|
`memory-search-foreign-${agent.id}`,
|
|
{
|
|
query: foreignAgent.sentinel,
|
|
corpus: "wiki",
|
|
},
|
|
);
|
|
expect(assertToolDetailsRecord(foreignMemorySearch.details).results).toEqual([]);
|
|
|
|
const memoryGet = createMemoryCoreTool({
|
|
factories: memoryCoreFactories,
|
|
name: "memory_get",
|
|
appConfig,
|
|
agentId: agent.id,
|
|
});
|
|
const ownMemoryGet = await memoryGet.execute(`memory-get-own-${agent.id}`, {
|
|
path: agent.pagePath,
|
|
corpus: "wiki",
|
|
});
|
|
expect(assertToolDetailsRecord(ownMemoryGet.details)).toMatchObject({
|
|
status: "ok",
|
|
corpus: "wiki",
|
|
path: agent.pagePath,
|
|
text: expect.stringContaining(agent.sentinel),
|
|
});
|
|
|
|
const foreignMemoryGet = await memoryGet.execute(`memory-get-foreign-${agent.id}`, {
|
|
path: foreignAgent.pagePath,
|
|
corpus: "wiki",
|
|
});
|
|
expect(assertToolDetailsRecord(foreignMemoryGet.details)).toMatchObject({
|
|
path: foreignAgent.pagePath,
|
|
text: "",
|
|
status: "not_found",
|
|
corpora: [{ corpus: "wiki", outcome: "ok" }],
|
|
});
|
|
}
|
|
} finally {
|
|
clearMemoryPluginState();
|
|
}
|
|
}, 360_000);
|
|
});
|