supermemory/apps/mcp/e2e/memory.test.ts
MaheshtheDev 9808441f6b test(mcp): add deployed-server e2e suite (#1057)
End-to-end tests that drive the deployed Supermemory MCP over streamable HTTP (no mocks):

- discovery: handshake + tool/resource/prompt listing
- identity: whoAmI, listProjects
- memory: save -> recall round-trip, profile variants, forget, container scoping
- graph/resources/prompts: memory-graph, fetch-graph-data, profile/projects, context
- oauth: protected-resource discovery chain, dynamic client registration,
token-endpoint negatives, and a real refresh -> access token round-trip
- auth: GET / info, 401 on missing/invalid token
- root-scope: x-sm-project scoping behavior

Tests skip without SUPERMEMORY_API_KEY (OAuth tier-D skips without its token env vars) so CI is safe without secrets. Adds vitest + a test:e2e script and documents running + the OAuth flow in apps/mcp/README.md.
2026-06-07 04:30:17 +00:00

127 lines
4.1 KiB
TypeScript

import { randomUUID } from "node:crypto"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import {
API_KEY,
callTool,
connect,
forgetUntilForgotten,
recallUntil,
recallUntilAbsent,
type Session,
textOf,
} from "./helpers"
describe.skipIf(!API_KEY)("MCP — memory behaviors", () => {
let s: Session
const created: Array<{ content: string; containerTag?: string }> = []
beforeAll(async () => {
s = await connect()
})
afterAll(async () => {
for (const { content, containerTag } of created) {
await callTool(s.client, "memory", {
content,
action: "forget",
...(containerTag ? { containerTag } : {}),
}).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, "memory", { content, action: "save" })
expect(save.isError).toBeFalsy()
expect(textOf(save)).toMatch(/Saved memory/i)
const found = await recallUntil(s.client, "test fruit dragonfruit", marker)
expect(found, `recall never returned marker ${marker}`).not.toBeNull()
}, 120_000)
it("recall includeProfile=true returns profile + memories sections", async () => {
const res = await callTool(s.client, "recall", {
query: "dragonfruit",
includeProfile: true,
})
expect(res.isError).toBeFalsy()
const txt = textOf(res)
expect(txt).toMatch(/## (User Profile|Relevant Memories)/)
}, 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, "recall", {
query: `zzz-no-such-memory-${randomUUID()}`,
includeProfile: false,
})
expect(res.isError).toBeFalsy()
expect(textOf(res)).toMatch(/## Relevant Memories|No memories found/i)
})
// Hard-asserts forget is accepted; removal is eventually-consistent, so disappearance is best-effort.
it("forget accepts and removes a saved memory", async () => {
const marker = `fg-${randomUUID()}`
const content = `e2e forget target. token=${marker}. Secret animal is axolotl.`
created.push({ content })
await callTool(s.client, "memory", { content, action: "save" })
const found = await recallUntil(s.client, "secret animal axolotl", marker)
expect(found, "memory should exist before forget").not.toBeNull()
// Polls forget until it confirms real removal ("forgot"), past the extraction window.
const forgotten = await forgetUntilForgotten(s.client, content)
expect(
forgotten,
`forget never confirmed removal for ${marker} (memory entry never extracted in time)`,
).not.toBeNull()
const gone = await recallUntilAbsent(
s.client,
"secret animal axolotl",
marker,
)
if (!gone) {
console.warn(
`[e2e] forget confirmed but ${marker} still indexed after ~60s (eventual deletion)`,
)
}
}, 240_000)
it("containerTag scopes memories (isolation)", async () => {
// Fixed tags (not per-run UUIDs) so the test doesn't mint a new project each run.
const tagA = "sm_e2e_scope_a"
const tagB = "sm_e2e_scope_b"
const marker = `sc-${randomUUID()}`
const content = `e2e scoping. token=${marker}. Project color is teal.`
created.push({ content, containerTag: tagA })
await callTool(s.client, "memory", {
content,
action: "save",
containerTag: tagA,
})
const inA = await recallUntil(s.client, "project color teal", marker, {
containerTag: tagA,
})
expect(inA, "marker should be found in its own container").not.toBeNull()
// Same query scoped to a different container must NOT see it.
const leaked = await recallUntil(s.client, "project color teal", marker, {
containerTag: tagB,
tries: 3,
delayMs: 3000,
})
expect(leaked, "marker leaked across containers").toBeNull()
}, 120_000)
it("returns an error result for a missing required argument", async () => {
const res = await callTool(s.client, "recall", {})
expect(res.isError).toBe(true)
expect(textOf(res).length).toBeGreaterThan(0)
})
})