supermemory/packages/tools/test/mastra/integration.test.ts
2026-04-24 23:47:46 +00:00

625 lines
18 KiB
TypeScript

/**
* Integration tests for the Mastra integration
* Tests processors and wrapper with real Supermemory API calls
*/
import { describe, it, expect, vi } from "vitest"
import {
RequestContext,
MASTRA_THREAD_ID_KEY,
} from "@mastra/core/request-context"
import {
SupermemoryInputProcessor,
SupermemoryOutputProcessor,
createSupermemoryProcessors,
withSupermemory,
} from "../../src/mastra"
import type {
ProcessInputArgs,
ProcessOutputResultArgs,
MessageList,
MastraDBMessage,
MastraMessageContentV2,
Processor,
} from "../../src/mastra"
interface MockAgentConfig {
id: string
name?: string
model?: string
inputProcessors?: Processor[]
outputProcessors?: Processor[]
[key: string]: unknown
}
import "dotenv/config"
const INTEGRATION_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "",
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
containerTag: "integration-test-mastra",
customId: "integration-test-conversation",
}
const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY
/**
* Helper to create MastraMessageContentV2 from text
*/
function createMessageContent(text: string): MastraMessageContentV2 {
return {
format: 2,
parts: [{ type: "text", text }],
}
}
/**
* Helper to create a MastraDBMessage
*/
function createMessage(
role: "user" | "assistant" | "system",
text: string,
): MastraDBMessage {
return {
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
role,
content: createMessageContent(text),
createdAt: new Date(),
}
}
/**
* Creates a mock MessageList that captures calls for assertion.
*/
const createIntegrationMessageList = (): MessageList & {
calls: { method: string; args: unknown[] }[]
getSystemContent: () => string | undefined
} => {
const calls: { method: string; args: unknown[] }[] = []
return {
calls,
addSystem: vi.fn((content: string, id?: string) => {
calls.push({ method: "addSystem", args: [content, id] })
}),
addUser: vi.fn((content: string) => {
calls.push({ method: "addUser", args: [content] })
}),
addAssistant: vi.fn((content: string) => {
calls.push({ method: "addAssistant", args: [content] })
}),
getSystemContent: () => {
const systemCall = calls.find((c) => c.method === "addSystem")
return systemCall?.args[0] as string | undefined
},
} as unknown as MessageList & {
calls: { method: string; args: unknown[] }[]
getSystemContent: () => string | undefined
}
}
describe.skipIf(!shouldRunIntegration)(
"Integration: Mastra processors with real API",
() => {
describe("SupermemoryInputProcessor", () => {
it("should fetch real memories and inject into messageList", async () => {
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
})
const messageList = createIntegrationMessageList()
const messages: MastraDBMessage[] = [
createMessage("user", "Hello, what do you know about me?"),
]
const args: ProcessInputArgs = {
messages,
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
const systemContent = messageList.getSystemContent()
expect(typeof systemContent).toBe("string")
})
it("should use query mode with user message as search query", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "query",
})
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [
createMessage(
"user",
"What are my favorite programming languages?",
),
],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
)
expect(profileCalls.length).toBeGreaterThan(0)
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
const body = JSON.parse(
(profileCall[1] as RequestInit).body as string,
)
expect(body.q).toBe("What are my favorite programming languages?")
}
fetchSpy.mockRestore()
})
it("should use full mode with both profile and query", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "full",
})
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Full mode test query")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
)
expect(profileCalls.length).toBeGreaterThan(0)
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
const body = JSON.parse(
(profileCall[1] as RequestInit).body as string,
)
expect(body.q).toBe("Full mode test query")
}
fetchSpy.mockRestore()
})
it("should cache memories for repeated calls with same message", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
})
const messages: MastraDBMessage[] = [
createMessage("user", "Cache test message"),
]
const args1: ProcessInputArgs = {
messages,
systemMessages: [],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args1)
const callsAfterFirst = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
).length
const args2: ProcessInputArgs = {
messages,
systemMessages: [],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args2)
const callsAfterSecond = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
).length
expect(callsAfterSecond).toBe(callsAfterFirst)
fetchSpy.mockRestore()
})
it("should use custom promptTemplate for memory formatting", async () => {
const customTemplate = (data: {
userMemories: string
generalSearchMemories: string
}) => `<mastra-memories>${data.userMemories}</mastra-memories>`
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
promptTemplate: customTemplate,
})
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Custom template test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const systemContent = messageList.getSystemContent()
expect(systemContent).toMatch(/<mastra-memories>.*<\/mastra-memories>/s)
})
})
describe("SupermemoryOutputProcessor", () => {
it("should save conversation when addMemory is always", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const customId = `test-mastra-${Date.now()}`
const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always",
})
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Hello from Mastra integration test"),
createMessage("assistant", "Hi! I'm responding to the test."),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
const conversationCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" &&
call[0].includes("/v4/conversations"),
)
expect(conversationCalls.length).toBeGreaterThan(0)
fetchSpy.mockRestore()
})
it("should not save when addMemory is never", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: "test-thread",
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "never",
})
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "This should not be saved"),
createMessage("assistant", "Agreed"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processOutputResult(args)
const conversationCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" &&
call[0].includes("/v4/conversations"),
)
expect(conversationCalls.length).toBe(0)
fetchSpy.mockRestore()
})
it("should use threadId from RequestContext when available", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always",
})
const contextThreadId = `context-thread-${Date.now()}`
const requestContext = new RequestContext()
requestContext.set(MASTRA_THREAD_ID_KEY, contextThreadId)
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Test with RequestContext threadId"),
createMessage("assistant", "Got it!"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
requestContext,
}
await processor.processOutputResult(args)
const conversationCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" &&
call[0].includes("/v4/conversations"),
)
expect(conversationCalls.length).toBeGreaterThan(0)
fetchSpy.mockRestore()
})
})
describe("createSupermemoryProcessors", () => {
it("should create working input and output processors", async () => {
const { input, output } = createSupermemoryProcessors({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: `processors-test-${Date.now()}`,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
addMemory: "always",
})
const messageList = createIntegrationMessageList()
const inputArgs: ProcessInputArgs = {
messages: [createMessage("user", "Test processors factory")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await input.processInput(inputArgs)
expect(messageList.addSystem).toHaveBeenCalled()
const outputArgs: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Test processors factory"),
createMessage("assistant", "Response"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await output.processOutputResult(outputArgs)
})
})
describe("withSupermemory wrapper", () => {
it("should enhance config with working processors", async () => {
const config: MockAgentConfig = {
id: "test-mastra-agent",
name: "Test Mastra Agent",
model: "gpt-4o",
}
const enhanced = withSupermemory(config, {
containerTag: INTEGRATION_CONFIG.containerTag,
customId: `wrapper-test-${Date.now()}`,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
addMemory: "always",
})
expect(enhanced.id).toBe("test-mastra-agent")
expect(enhanced.name).toBe("Test Mastra Agent")
expect(enhanced.model).toBe("gpt-4o")
expect(enhanced.inputProcessors).toHaveLength(1)
expect(enhanced.outputProcessors).toHaveLength(1)
const inputProcessor = enhanced.inputProcessors?.[0]
expect(inputProcessor?.id).toBe("supermemory-input")
if (inputProcessor?.processInput) {
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Wrapper test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await inputProcessor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
}
})
it("should merge with existing processors correctly", async () => {
const existingInputProcessor = {
id: "existing-input",
name: "Existing Input Processor",
processInput: vi.fn().mockResolvedValue(undefined),
}
const existingOutputProcessor = {
id: "existing-output",
name: "Existing Output Processor",
processOutputResult: vi.fn().mockResolvedValue(undefined),
}
const config: MockAgentConfig = {
id: "agent-with-processors",
name: "Agent With Processors",
inputProcessors: [existingInputProcessor],
outputProcessors: [existingOutputProcessor],
}
const enhanced = withSupermemory(config, {
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
})
expect(enhanced.inputProcessors).toHaveLength(2)
expect(enhanced.outputProcessors).toHaveLength(2)
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
})
})
describe("Options", () => {
it("verbose mode should not break functionality", async () => {
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
verbose: true,
})
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Verbose mode test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
expect(messageList.addSystem).toHaveBeenCalled()
})
it("custom baseUrl should be used for API calls", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch")
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: INTEGRATION_CONFIG.apiKey,
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
})
const args: ProcessInputArgs = {
messages: [createMessage("user", "Base URL test")],
systemMessages: [],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await processor.processInput(args)
const profileCalls = fetchSpy.mock.calls.filter(
(call) =>
typeof call[0] === "string" && call[0].includes("/v4/profile"),
)
expect(profileCalls.length).toBeGreaterThan(0)
const url = profileCalls[0]?.[0] as string
expect(url.startsWith(INTEGRATION_CONFIG.baseUrl)).toBe(true)
fetchSpy.mockRestore()
})
})
describe("Error handling", () => {
it("should handle invalid API key gracefully", async () => {
const processor = new SupermemoryInputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: INTEGRATION_CONFIG.customId,
apiKey: "invalid-api-key-12345",
baseUrl: INTEGRATION_CONFIG.baseUrl,
mode: "profile",
})
const messageList = createIntegrationMessageList()
const args: ProcessInputArgs = {
messages: [createMessage("user", "Invalid key test")],
systemMessages: [],
messageList,
abort: vi.fn() as never,
retryCount: 0,
}
const result = await processor.processInput(args)
expect(result).toBe(messageList)
expect(messageList.addSystem).not.toHaveBeenCalled()
})
it("output processor should handle save errors gracefully", async () => {
const processor = new SupermemoryOutputProcessor({
containerTag: INTEGRATION_CONFIG.containerTag,
customId: "error-test",
apiKey: "invalid-api-key-12345",
baseUrl: INTEGRATION_CONFIG.baseUrl,
addMemory: "always",
})
const args: ProcessOutputResultArgs = {
messages: [
createMessage("user", "Error test"),
createMessage("assistant", "Response"),
],
messageList: createIntegrationMessageList(),
abort: vi.fn() as never,
retryCount: 0,
}
await expect(processor.processOutputResult(args)).resolves.toBeDefined()
})
})
},
)