Files
rohitg00--agentmemory/test/mesh.test.ts
T
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Waiting to run
CI / test (20, ubuntu-latest) (push) Waiting to run
CI / test (22, macos-latest) (push) Waiting to run
CI / test (22, ubuntu-latest) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

756 lines
23 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
vi.mock("../src/logger.js", () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
import { registerMeshFunction } from "../src/functions/mesh.js";
import type {
MeshPeer,
Memory,
Action,
SemanticMemory,
ProceduralMemory,
MemoryRelation,
GraphNode,
GraphEdge,
} from "../src/types.js";
function mockKV() {
const store = new Map<string, Map<string, unknown>>();
return {
get: async <T>(scope: string, key: string): Promise<T | null> => {
return (store.get(scope)?.get(key) as T) ?? null;
},
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
if (!store.has(scope)) store.set(scope, new Map());
store.get(scope)!.set(key, data);
return data;
},
delete: async (scope: string, key: string): Promise<void> => {
store.get(scope)?.delete(key);
},
list: async <T>(scope: string): Promise<T[]> => {
const entries = store.get(scope);
return entries ? (Array.from(entries.values()) as T[]) : [];
},
};
}
function mockSdk() {
const functions = new Map<string, Function>();
return {
registerFunction: (idOrOpts: string | { id: string }, handler: Function) => {
const id = typeof idOrOpts === "string" ? idOrOpts : idOrOpts.id;
functions.set(id, handler);
},
registerTrigger: () => {},
trigger: async (idOrInput: string | { function_id: string; payload: unknown }, data?: unknown) => {
const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id;
const payload = typeof idOrInput === "string" ? data : idOrInput.payload;
const fn = functions.get(id);
if (!fn) throw new Error(`No function: ${id}`);
return fn(payload);
},
};
}
describe("Mesh Functions", () => {
let sdk: ReturnType<typeof mockSdk>;
let kv: ReturnType<typeof mockKV>;
beforeEach(() => {
sdk = mockSdk();
kv = mockKV();
vi.clearAllMocks();
registerMeshFunction(sdk as never, kv as never);
});
describe("mesh-register", () => {
it("registers a valid peer", async () => {
const result = (await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
name: "peer-1",
sharedScopes: ["memories"],
})) as { success: boolean; peer: MeshPeer };
expect(result.success).toBe(true);
expect(result.peer.url).toBe("https://peer1.example.com");
expect(result.peer.name).toBe("peer-1");
expect(result.peer.status).toBe("disconnected");
expect(result.peer.sharedScopes).toEqual(["memories"]);
expect(result.peer.id).toMatch(/^peer_/);
const peers = await kv.list<MeshPeer>("mem:mesh");
expect(peers.length).toBe(1);
});
it("uses expanded default sharedScopes when not provided", async () => {
const result = (await sdk.trigger("mem::mesh-register", {
url: "https://peer2.example.com",
name: "peer-2",
})) as { success: boolean; peer: MeshPeer };
expect(result.success).toBe(true);
expect(result.peer.sharedScopes).toEqual([
"memories",
"actions",
"semantic",
"procedural",
"relations",
"graph:nodes",
"graph:edges",
]);
});
it("stores syncFilter when provided", async () => {
const result = (await sdk.trigger("mem::mesh-register", {
url: "https://peer3.example.com",
name: "peer-3",
syncFilter: { project: "/my/project" },
})) as { success: boolean; peer: MeshPeer };
expect(result.success).toBe(true);
expect(result.peer.syncFilter).toEqual({ project: "/my/project" });
});
it("returns error when url is missing", async () => {
const result = (await sdk.trigger("mem::mesh-register", {
name: "peer-1",
})) as { success: boolean; error: string };
expect(result.success).toBe(false);
expect(result.error).toContain("url and name are required");
});
it("returns error when name is missing", async () => {
const result = (await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
})) as { success: boolean; error: string };
expect(result.success).toBe(false);
expect(result.error).toContain("url and name are required");
});
it("returns error for duplicate url", async () => {
await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
name: "peer-1",
});
const result = (await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
name: "peer-1-duplicate",
})) as { success: boolean; error: string; peerId: string };
expect(result.success).toBe(false);
expect(result.error).toContain("peer already registered");
expect(result.peerId).toBeDefined();
});
});
describe("mesh-list", () => {
it("returns empty list when no peers registered", async () => {
const result = (await sdk.trigger("mem::mesh-list", {})) as {
success: boolean;
peers: MeshPeer[];
};
expect(result.success).toBe(true);
expect(result.peers).toEqual([]);
});
it("returns all registered peers", async () => {
await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
name: "peer-1",
});
await sdk.trigger("mem::mesh-register", {
url: "https://peer2.example.com",
name: "peer-2",
});
const result = (await sdk.trigger("mem::mesh-list", {})) as {
success: boolean;
peers: MeshPeer[];
};
expect(result.success).toBe(true);
expect(result.peers.length).toBe(2);
expect(result.peers.map((p) => p.name).sort()).toEqual(["peer-1", "peer-2"]);
});
});
describe("mesh-sync", () => {
it("requires a configured shared secret", async () => {
const regResult = (await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
name: "peer-1",
})) as { success: boolean; peer: MeshPeer };
const result = (await sdk.trigger("mem::mesh-sync", {
peerId: regResult.peer.id,
direction: "push",
})) as { success: boolean; error: string };
expect(result.success).toBe(false);
expect(result.error).toContain("AGENTMEMORY_SECRET");
});
it("sends authorization headers to peers when syncing", async () => {
const authedSdk = mockSdk();
const authedKv = mockKV();
registerMeshFunction(authedSdk as never, authedKv as never, "mesh-secret");
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ accepted: 0 }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const regResult = (await authedSdk.trigger("mem::mesh-register", {
url: "https://peer2.example.com",
name: "peer-2",
})) as { success: boolean; peer: MeshPeer };
const result = (await authedSdk.trigger("mem::mesh-sync", {
peerId: regResult.peer.id,
direction: "push",
})) as { success: boolean; results: Array<{ errors: string[] }> };
expect(result.success).toBe(true);
expect(result.results[0].errors).toEqual([]);
expect(fetchMock).toHaveBeenCalledWith(
"https://peer2.example.com/agentmemory/mesh/receive",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: "Bearer mesh-secret",
}),
}),
);
vi.unstubAllGlobals();
});
});
describe("mesh-receive", () => {
it("accepts new memories", async () => {
const mem: Memory = {
id: "mem_1",
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
type: "pattern",
title: "Test memory",
content: "Test content",
concepts: ["test"],
files: [],
sessionIds: ["ses_1"],
strength: 5,
version: 1,
isLatest: true,
};
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [mem],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<Memory>("mem:memories", "mem_1");
expect(stored).toBeDefined();
expect(stored!.title).toBe("Test memory");
});
it("accepts newer memory over existing (last-write-wins)", async () => {
const older: Memory = {
id: "mem_1",
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
type: "pattern",
title: "Old title",
content: "Old content",
concepts: [],
files: [],
sessionIds: [],
strength: 5,
version: 1,
isLatest: true,
};
await kv.set("mem:memories", "mem_1", older);
const newer: Memory = {
...older,
updatedAt: "2026-03-02T00:00:00Z",
title: "New title",
content: "New content",
version: 2,
};
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [newer],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<Memory>("mem:memories", "mem_1");
expect(stored!.title).toBe("New title");
});
it("rejects older memory than existing", async () => {
const existing: Memory = {
id: "mem_1",
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-02T00:00:00Z",
type: "pattern",
title: "Existing title",
content: "Existing content",
concepts: [],
files: [],
sessionIds: [],
strength: 5,
version: 2,
isLatest: true,
};
await kv.set("mem:memories", "mem_1", existing);
const older: Memory = {
...existing,
updatedAt: "2026-03-01T00:00:00Z",
title: "Old title",
version: 1,
};
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [older],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
const stored = await kv.get<Memory>("mem:memories", "mem_1");
expect(stored!.title).toBe("Existing title");
});
it("skips memory entries with missing id", async () => {
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [
{ updatedAt: "2026-03-01T00:00:00Z", title: "No ID" } as unknown as Memory,
],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
});
it("skips memory entries with invalid date", async () => {
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [
{
id: "mem_bad_date",
updatedAt: "not-a-date",
title: "Bad date",
} as unknown as Memory,
],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
});
it("accepts new actions", async () => {
const action: Action = {
id: "act_1",
title: "Fix bug",
description: "Fix the login bug",
status: "pending",
priority: 1,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
createdBy: "agent-1",
tags: ["bug"],
sourceObservationIds: [],
sourceMemoryIds: [],
};
const result = (await sdk.trigger("mem::mesh-receive", {
actions: [action],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<Action>("mem:actions", "act_1");
expect(stored).toBeDefined();
expect(stored!.title).toBe("Fix bug");
});
it("accepts newer action over existing (last-write-wins)", async () => {
const older: Action = {
id: "act_1",
title: "Old action",
description: "Old desc",
status: "pending",
priority: 1,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
createdBy: "agent-1",
tags: [],
sourceObservationIds: [],
sourceMemoryIds: [],
};
await kv.set("mem:actions", "act_1", older);
const newer: Action = {
...older,
updatedAt: "2026-03-02T00:00:00Z",
title: "Updated action",
status: "done",
};
const result = (await sdk.trigger("mem::mesh-receive", {
actions: [newer],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<Action>("mem:actions", "act_1");
expect(stored!.title).toBe("Updated action");
expect(stored!.status).toBe("done");
});
it("rejects older action than existing", async () => {
const existing: Action = {
id: "act_1",
title: "Current action",
description: "Current desc",
status: "active",
priority: 1,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-02T00:00:00Z",
createdBy: "agent-1",
tags: [],
sourceObservationIds: [],
sourceMemoryIds: [],
};
await kv.set("mem:actions", "act_1", existing);
const older: Action = {
...existing,
updatedAt: "2026-03-01T00:00:00Z",
title: "Stale action",
};
const result = (await sdk.trigger("mem::mesh-receive", {
actions: [older],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
const stored = await kv.get<Action>("mem:actions", "act_1");
expect(stored!.title).toBe("Current action");
});
it("skips action entries with missing id", async () => {
const result = (await sdk.trigger("mem::mesh-receive", {
actions: [
{ updatedAt: "2026-03-01T00:00:00Z", title: "No ID" } as unknown as Action,
],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
});
it("skips action entries with invalid date", async () => {
const result = (await sdk.trigger("mem::mesh-receive", {
actions: [
{
id: "act_bad_date",
updatedAt: "invalid-date-string",
title: "Bad date",
} as unknown as Action,
],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
});
it("accepts both memories and actions in one call", async () => {
const mem: Memory = {
id: "mem_combo",
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
type: "fact",
title: "Combo memory",
content: "Content",
concepts: [],
files: [],
sessionIds: [],
strength: 3,
version: 1,
isLatest: true,
};
const action: Action = {
id: "act_combo",
title: "Combo action",
description: "Desc",
status: "pending",
priority: 2,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
createdBy: "agent-1",
tags: [],
sourceObservationIds: [],
sourceMemoryIds: [],
};
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [mem],
actions: [action],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(2);
});
it("returns zero accepted for empty arrays", async () => {
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [],
actions: [],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(0);
});
});
describe("mesh-remove", () => {
it("removes a registered peer", async () => {
const regResult = (await sdk.trigger("mem::mesh-register", {
url: "https://peer1.example.com",
name: "peer-1",
})) as { success: boolean; peer: MeshPeer };
const result = (await sdk.trigger("mem::mesh-remove", {
peerId: regResult.peer.id,
})) as { success: boolean };
expect(result.success).toBe(true);
const peers = await kv.list<MeshPeer>("mem:mesh");
expect(peers.length).toBe(0);
});
it("returns error when peerId is missing", async () => {
const result = (await sdk.trigger("mem::mesh-remove", {})) as {
success: boolean;
error: string;
};
expect(result.success).toBe(false);
expect(result.error).toContain("peerId is required");
});
it("succeeds silently for non-existent peerId", async () => {
const result = (await sdk.trigger("mem::mesh-remove", {
peerId: "peer_nonexistent",
})) as { success: boolean };
expect(result.success).toBe(true);
});
});
describe("mesh-receive expanded scopes", () => {
it("accepts semantic memories", async () => {
const sem: SemanticMemory = {
id: "sem_1",
fact: "React uses JSX",
confidence: 0.9,
sourceSessionIds: ["ses_1"],
sourceMemoryIds: ["mem_1"],
accessCount: 1,
lastAccessedAt: "2026-03-01T00:00:00Z",
strength: 7,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
};
const result = (await sdk.trigger("mem::mesh-receive", {
semantic: [sem],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<SemanticMemory>("mem:semantic", "sem_1");
expect(stored).toBeDefined();
expect(stored!.fact).toBe("React uses JSX");
});
it("accepts procedural memories", async () => {
const proc: ProceduralMemory = {
id: "proc_1",
name: "Deploy to prod",
steps: ["build", "test", "deploy"],
triggerCondition: "on merge to main",
frequency: 5,
sourceSessionIds: ["ses_1"],
strength: 8,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
};
const result = (await sdk.trigger("mem::mesh-receive", {
procedural: [proc],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<ProceduralMemory>("mem:procedural", "proc_1");
expect(stored!.name).toBe("Deploy to prod");
});
it("accepts graph nodes", async () => {
const node: GraphNode = {
id: "gn_1",
type: "concept",
name: "typescript",
properties: {},
sourceObservationIds: ["obs_1"],
createdAt: "2026-03-01T00:00:00Z",
};
const result = (await sdk.trigger("mem::mesh-receive", {
graphNodes: [node],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<GraphNode>("mem:graph:nodes", "gn_1");
expect(stored!.name).toBe("typescript");
});
it("accepts graph edges", async () => {
const edge: GraphEdge = {
id: "ge_1",
type: "uses",
sourceNodeId: "gn_1",
targetNodeId: "gn_2",
weight: 1,
sourceObservationIds: ["obs_1"],
createdAt: "2026-03-01T00:00:00Z",
};
const result = (await sdk.trigger("mem::mesh-receive", {
graphEdges: [edge],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<GraphEdge>("mem:graph:edges", "ge_1");
expect(stored!.type).toBe("uses");
});
it("accepts relations", async () => {
const rel: MemoryRelation = {
type: "supersedes",
sourceId: "mem_2",
targetId: "mem_1",
createdAt: "2026-03-01T00:00:00Z",
confidence: 0.95,
};
const relWithId = { ...rel, id: "rel_1" } as MemoryRelation & { id: string };
const result = (await sdk.trigger("mem::mesh-receive", {
relations: [relWithId],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
});
it("accepts all scope types in one call", async () => {
const mem: Memory = {
id: "mem_all",
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
type: "fact",
title: "All scopes test",
content: "Content",
concepts: [],
files: [],
sessionIds: [],
strength: 5,
version: 1,
isLatest: true,
};
const sem: SemanticMemory = {
id: "sem_all",
fact: "Test",
confidence: 0.5,
sourceSessionIds: [],
sourceMemoryIds: [],
accessCount: 0,
lastAccessedAt: "2026-03-01T00:00:00Z",
strength: 5,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
};
const node: GraphNode = {
id: "gn_all",
type: "file",
name: "test.ts",
properties: {},
sourceObservationIds: [],
createdAt: "2026-03-01T00:00:00Z",
};
const result = (await sdk.trigger("mem::mesh-receive", {
memories: [mem],
semantic: [sem],
graphNodes: [node],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(3);
});
it("applies LWW for semantic memories", async () => {
const older: SemanticMemory = {
id: "sem_lww",
fact: "Old fact",
confidence: 0.5,
sourceSessionIds: [],
sourceMemoryIds: [],
accessCount: 1,
lastAccessedAt: "2026-03-01T00:00:00Z",
strength: 5,
createdAt: "2026-03-01T00:00:00Z",
updatedAt: "2026-03-01T00:00:00Z",
};
await kv.set("mem:semantic", "sem_lww", older);
const newer: SemanticMemory = {
...older,
fact: "New fact",
updatedAt: "2026-03-02T00:00:00Z",
};
const result = (await sdk.trigger("mem::mesh-receive", {
semantic: [newer],
})) as { success: boolean; accepted: number };
expect(result.success).toBe(true);
expect(result.accepted).toBe(1);
const stored = await kv.get<SemanticMemory>("mem:semantic", "sem_lww");
expect(stored!.fact).toBe("New fact");
});
});
});