Menu
popagent
publicLatest change 1d29dff4c5c63378a8860a94989afec219abff9c - Show live Memory system graph by Ólafur Búi Ólafsson
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { LongTermMemoryStore } from "./long-term-memory";
const store = new LongTermMemoryStore();
const resourceId = `memory-test-${crypto.randomUUID()}`;
beforeAll(() => store.init());
afterAll(() => store.storage.db.none(
"DELETE FROM popagent_memories WHERE resource_id = $1 OR resource_id = $2",
[resourceId, `${resourceId}-other`],
));
describe("LongTermMemoryStore", () => {
test("retains an episode and recalls it across sessions", async () => {
await store.retainEpisode({
resourceId,
sessionId: "first-session",
userText: "I always use dark mode and prefer concise answers",
assistantText: "I will remember those preferences.",
});
const recalled = await store.recall({
resourceId,
query: "What display mode and answer style do I prefer?",
});
expect(recalled.some((memory) => memory.content.includes("dark mode"))).toBe(true);
expect(recalled.some((memory) => memory.sessionId === "first-session")).toBe(true);
});
test("upserts explicit facts and ranks them above episodes", async () => {
await store.rememberFact({
resourceId,
sessionId: "second-session",
content: "The user's preferred runtime is Bun.",
importance: 0.9,
});
await store.rememberFact({
resourceId,
sessionId: "third-session",
content: "The user's preferred runtime is Bun.",
importance: 0.9,
});
const recalled = await store.recall({ resourceId, query: "preferred runtime" });
expect(recalled[0]).toEqual(expect.objectContaining({
kind: "fact",
content: "The user's preferred runtime is Bun.",
}));
expect(recalled.filter((memory) => memory.content.includes("preferred runtime is Bun"))).toHaveLength(1);
});
test("does not leak memories between resources", async () => {
const recalled = await store.recall({
resourceId: `${resourceId}-other`,
query: "dark mode Bun",
});
expect(recalled).toEqual([]);
});
test("summarizes durable memory location and activity", async () => {
const statusResource = `${resourceId}-status`;
try {
await store.rememberFact({
resourceId: statusResource,
sessionId: "fact-session",
key: "system.location",
content: "Durable facts live in PostgreSQL.",
});
await store.retainEpisode({
resourceId: statusResource,
sessionId: "episode-session",
userText: "Where are memories stored?",
assistantText: "In PostgreSQL.",
});
expect(await store.status(statusResource)).toEqual({
facts: 1,
episodes: 1,
recalled: 0,
latestUpdatedAt: expect.any(String),
});
} finally {
await store.storage.db.none("DELETE FROM popagent_memories WHERE resource_id = $1", [statusResource]);
}
});
test("enforces the durable-fact size limit for every write path", async () => {
const content = "x".repeat(2_001);
await expect(store.rememberFact({
resourceId,
sessionId: "oversized-fact",
content,
})).rejects.toThrow("Fact content exceeds 2000 characters");
const existing = (await store.list({ resourceId, kind: "fact", limit: 1 }))[0];
expect(existing).toBeDefined();
await expect(store.updateFact({
id: existing!.id,
resourceId,
content,
importance: 1,
})).rejects.toThrow("Fact content exceeds 2000 characters");
});
test("truncates retained episodes and total rendered recall context", async () => {
const marker = `budget${crypto.randomUUID().replaceAll("-", "")}`;
await store.retainEpisode({
resourceId,
sessionId: "large-episode",
userText: `${marker} ${"u".repeat(8_000)}`,
assistantText: `${marker} ${"a".repeat(16_000)}`,
});
const episode = (await store.list({ resourceId, query: marker, kind: "episode", limit: 1 }))[0];
expect(episode).toBeDefined();
expect(episode!.content.length).toBeLessThanOrEqual(12_050);
expect(episode!.content.match(/… \[truncated\]/g)).toHaveLength(2);
for (let index = 0; index < 15; index++) {
await store.rememberFact({
resourceId,
sessionId: "recall-budget",
key: `${marker}.${index}`,
content: `${marker} ${index} `.padEnd(2_000, String(index % 10)),
importance: 1,
});
}
const rendered = await store.formatRecall({ resourceId, query: marker, limit: 20 });
expect(rendered).toBeDefined();
expect(rendered!.length).toBeLessThanOrEqual(24_000);
expect(rendered).toContain("… [truncated]");
});
});