mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-19 21:44:02 +00:00
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.
80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
import { randomUUID } from "node:crypto"
|
|
import { describe, expect, it } from "vitest"
|
|
import { API_KEY, callTool, connect, recallUntil, textOf } from "./helpers"
|
|
|
|
type ToolLike = {
|
|
name: string
|
|
inputSchema?: { properties?: Record<string, unknown> }
|
|
}
|
|
|
|
const propsOf = (tools: ToolLike[], name: string): Record<string, unknown> =>
|
|
tools.find((t) => t.name === name)?.inputSchema?.properties ?? {}
|
|
|
|
// Fixed tag (not a per-run UUID) so the test doesn't mint a new project each run.
|
|
const SCOPE_TAG = "sm_e2e_root"
|
|
|
|
// x-sm-project locks the connection to one project: strips containerTag from schemas and scopes every op — distinct from the per-call arg.
|
|
describe.skipIf(!API_KEY)("MCP — x-sm-project root scoping", () => {
|
|
it("strips containerTag from tool schemas when x-sm-project is set", async () => {
|
|
const scoped = await connect({ containerTag: SCOPE_TAG })
|
|
const plain = await connect()
|
|
try {
|
|
const scopedTools = (await scoped.client.listTools()).tools
|
|
const plainTools = (await plain.client.listTools()).tools
|
|
|
|
expect(propsOf(plainTools, "memory")).toHaveProperty("containerTag")
|
|
expect(propsOf(plainTools, "recall")).toHaveProperty("containerTag")
|
|
|
|
expect(propsOf(scopedTools, "memory")).not.toHaveProperty("containerTag")
|
|
expect(propsOf(scopedTools, "recall")).not.toHaveProperty("containerTag")
|
|
} finally {
|
|
await scoped.close()
|
|
await plain.close()
|
|
}
|
|
})
|
|
|
|
it("scopes saves to the connection project and isolates them from default", async () => {
|
|
const marker = `root-${randomUUID()}`
|
|
const content = `e2e root scope. token=${marker}. The root flower is bluebell.`
|
|
|
|
const rooted = await connect({ containerTag: SCOPE_TAG })
|
|
try {
|
|
const save = await callTool(rooted.client, "memory", {
|
|
content,
|
|
action: "save",
|
|
})
|
|
expect(save.isError).toBeFalsy()
|
|
expect(textOf(save)).toContain(SCOPE_TAG)
|
|
|
|
const found = await recallUntil(
|
|
rooted.client,
|
|
"root flower bluebell",
|
|
marker,
|
|
)
|
|
expect(found, "marker not found within its root scope").not.toBeNull()
|
|
} finally {
|
|
await callTool(rooted.client, "memory", {
|
|
content,
|
|
action: "forget",
|
|
}).catch(() => {})
|
|
await rooted.close()
|
|
}
|
|
|
|
// A default connection searches sm_project_default only — must not see it.
|
|
const plain = await connect()
|
|
try {
|
|
const leaked = await recallUntil(
|
|
plain.client,
|
|
"root flower bluebell",
|
|
marker,
|
|
{
|
|
tries: 3,
|
|
delayMs: 3000,
|
|
},
|
|
)
|
|
expect(leaked, "rooted memory leaked into the default project").toBeNull()
|
|
} finally {
|
|
await plain.close()
|
|
}
|
|
}, 120_000)
|
|
})
|