supermemory/apps/mcp/e2e/root-scope.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

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)
})