supermemory/apps/mcp/e2e/memory.test.ts
Mahesh Sanikommu 0da13138fa
fix(mcp): expose dreaming status and clarify document acceptance (#1844)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-10-28 12:20:29 -07:00

126 lines
4.1 KiB
TypeScript

import { randomUUID } from "node:crypto"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import {
OAUTH_CREDENTIALS_AVAILABLE,
callTool,
connect,
recallUntil,
type Session,
textOf,
} from "./helpers"
describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () => {
let s: Session
const created: Array<{ content: string; namespace?: string }> = []
beforeAll(async () => {
s = await connect()
})
afterAll(async () => {
for (const { content, namespace } of created) {
await callTool(s.client, "add_memory", {
content,
action: "forget",
...(namespace ? { namespace } : {}),
}).catch(() => {})
}
await s?.close()
})
it("save → recall round-trips the saved memory", async () => {
const marker = `rt-${randomUUID()}`
const content = `e2e round-trip. token=${marker}. The test fruit is dragonfruit.`
created.push({ content })
const save = await callTool(s.client, "add_memory", {
content,
action: "save",
})
expect(save.isError).toBeFalsy()
expect(textOf(save)).toMatch(/Document accepted/i)
const found = await recallUntil(s.client, "test fruit dragonfruit", marker)
expect(found, `recall never returned marker ${marker}`).not.toBeNull()
}, 120_000)
it("searchMemory does not include profile sections", async () => {
const res = await callTool(s.client, "search_memory", {
query: "dragonfruit",
})
expect(res.isError).toBeFalsy()
const txt = textOf(res)
expect(txt).not.toMatch(/## (Profile|Stable Context|Recent [Cc]ontext)/)
expect(txt).toMatch(/## Matching memories|No matching memories found/i)
}, 30_000)
it("getProfile returns profile sections", async () => {
const res = await callTool(s.client, "get_profile", {})
expect(res.isError).toBeFalsy()
const txt = textOf(res)
expect(txt).toMatch(
/## (Stable Context|Recent Context)|No profile facts are available/i,
)
}, 30_000)
// Hybrid search returns nearest matches even for unrelated queries — assert it responds gracefully, not empty.
it("recall responds gracefully for an unmatched query", async () => {
const res = await callTool(s.client, "search_memory", {
query: `zzz-no-such-memory-${randomUUID()}`,
})
expect(res.isError).toBeFalsy()
expect(textOf(res)).toMatch(
/## Matching memories|No matching memories found/i,
)
})
it("forget accepts a saved-memory request before extraction completes", async () => {
const marker = `fg-${randomUUID()}`
const content = `e2e forget target. token=${marker}. Secret animal is axolotl.`
created.push({ content })
await callTool(s.client, "add_memory", { content, action: "save" })
const found = await recallUntil(s.client, "secret animal axolotl", marker)
expect(found, "memory should exist before forget").not.toBeNull()
const forgotten = await callTool(s.client, "add_memory", {
content,
action: "forget",
})
expect(forgotten.isError).toBeFalsy()
expect(textOf(forgotten)).toMatch(/forgot|No matching memory found/i)
}, 120_000)
it("namespace scopes memories (isolation)", async () => {
// Fixed namespaces (not per-run UUIDs) so the test doesn't mint a new one each run.
const namespaceA = "sm_e2e_scope_a"
const namespaceB = "sm_e2e_scope_b"
const marker = `sc-${randomUUID()}`
const content = `e2e scoping. token=${marker}. Project color is teal.`
created.push({ content, namespace: namespaceA })
await callTool(s.client, "add_memory", {
content,
action: "save",
namespace: namespaceA,
})
const inA = await recallUntil(s.client, "project color teal", marker, {
namespace: namespaceA,
})
expect(inA, "marker should be found in its own namespace").not.toBeNull()
// Same query scoped to a different namespace must NOT see it.
const leaked = await recallUntil(s.client, "project color teal", marker, {
namespace: namespaceB,
tries: 3,
delayMs: 3000,
})
expect(leaked, "marker leaked across namespaces").toBeNull()
}, 120_000)
it("returns an error result for a missing required argument", async () => {
const res = await callTool(s.client, "search_memory", {})
expect(res.isError).toBe(true)
expect(textOf(res).length).toBeGreaterThan(0)
})
})