mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-19 21:44:02 +00:00
1027 lines
27 KiB
TypeScript
1027 lines
27 KiB
TypeScript
/**
|
|
* Unit tests for the Mastra integration
|
|
* Tests processors, wrapper, and factory functions
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
|
|
import {
|
|
RequestContext,
|
|
MASTRA_THREAD_ID_KEY,
|
|
} from "@mastra/core/request-context"
|
|
import {
|
|
SupermemoryInputProcessor,
|
|
SupermemoryOutputProcessor,
|
|
createSupermemoryProcessor,
|
|
createSupermemoryOutputProcessor,
|
|
createSupermemoryProcessors,
|
|
withSupermemory,
|
|
} from "../../src/mastra"
|
|
import type {
|
|
ProcessInputArgs,
|
|
ProcessOutputResultArgs,
|
|
MessageList,
|
|
MastraDBMessage,
|
|
MastraMessageContentV2,
|
|
Processor,
|
|
} from "../../src/mastra"
|
|
|
|
const TEST_CONFIG = {
|
|
apiKey: "test-api-key",
|
|
baseUrl: "https://api.supermemory.ai",
|
|
containerTag: "test-mastra-user",
|
|
customId: "test-conversation",
|
|
}
|
|
|
|
interface MockAgentConfig {
|
|
id: string
|
|
name?: string
|
|
model?: string
|
|
customProp?: string
|
|
inputProcessors?: Processor[]
|
|
outputProcessors?: Processor[]
|
|
[key: string]: unknown
|
|
}
|
|
|
|
/**
|
|
* 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(),
|
|
}
|
|
}
|
|
|
|
const createMockMessageList = (): MessageList & {
|
|
calls: { method: string; args: unknown[] }[]
|
|
} => {
|
|
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] })
|
|
}),
|
|
} as unknown as MessageList & { calls: { method: string; args: unknown[] }[] }
|
|
}
|
|
|
|
const createMockProfileResponse = (
|
|
staticMemories: string[] = [],
|
|
dynamicMemories: string[] = [],
|
|
searchResults: string[] = [],
|
|
) => ({
|
|
profile: {
|
|
static: staticMemories.map((memory) => ({ memory })),
|
|
dynamic: dynamicMemories.map((memory) => ({ memory })),
|
|
},
|
|
searchResults: {
|
|
results: searchResults.map((memory) => ({ memory })),
|
|
},
|
|
})
|
|
|
|
const createMockConversationResponse = () => ({
|
|
id: "mem-123",
|
|
conversationId: "conv-456",
|
|
status: "created",
|
|
})
|
|
|
|
describe("SupermemoryInputProcessor", () => {
|
|
let originalEnv: string | undefined
|
|
let originalFetch: typeof globalThis.fetch
|
|
let fetchMock: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
originalEnv = process.env.SUPERMEMORY_API_KEY
|
|
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
|
originalFetch = globalThis.fetch
|
|
fetchMock = vi.fn()
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalEnv) {
|
|
process.env.SUPERMEMORY_API_KEY = originalEnv
|
|
} else {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
}
|
|
globalThis.fetch = originalFetch
|
|
})
|
|
|
|
describe("constructor", () => {
|
|
it("should create processor with required options", () => {
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
expect(processor.id).toBe("supermemory-input")
|
|
expect(processor.name).toBe("Supermemory Memory Injection")
|
|
})
|
|
|
|
it("should throw error if API key is not set", () => {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
|
|
expect(() => {
|
|
new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
|
})
|
|
|
|
it("should accept API key via options", () => {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: "custom-key",
|
|
})
|
|
expect(processor.id).toBe("supermemory-input")
|
|
})
|
|
})
|
|
|
|
describe("processInput", () => {
|
|
it("should inject memories into messageList", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve(
|
|
createMockProfileResponse(
|
|
["User likes TypeScript"],
|
|
["Recent interest in AI"],
|
|
),
|
|
),
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "profile",
|
|
})
|
|
|
|
const messageList = createMockMessageList()
|
|
const messages: MastraDBMessage[] = [createMessage("user", "Hello")]
|
|
|
|
const args: ProcessInputArgs = {
|
|
messages,
|
|
systemMessages: [],
|
|
messageList,
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args)
|
|
|
|
expect(messageList.addSystem).toHaveBeenCalled()
|
|
const systemCall = messageList.calls.find((c) => c.method === "addSystem")
|
|
expect(systemCall).toBeDefined()
|
|
expect(systemCall?.args[0]).toContain("TypeScript")
|
|
expect(systemCall?.args[1]).toBe("supermemory")
|
|
})
|
|
|
|
it("should use cached memories on second call with same message", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve(createMockProfileResponse(["Cached memory"])),
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "profile",
|
|
})
|
|
|
|
const messages: MastraDBMessage[] = [createMessage("user", "Hello")]
|
|
|
|
const args1: ProcessInputArgs = {
|
|
messages,
|
|
systemMessages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args1)
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
|
|
const args2: ProcessInputArgs = {
|
|
messages,
|
|
systemMessages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args2)
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it("should refetch memories for different user message", async () => {
|
|
let callCount = 0
|
|
fetchMock.mockImplementation(() => {
|
|
callCount++
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve(
|
|
createMockProfileResponse([`Memory from call ${callCount}`]),
|
|
),
|
|
})
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "query",
|
|
})
|
|
|
|
const args1: ProcessInputArgs = {
|
|
messages: [createMessage("user", "First message")],
|
|
systemMessages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args1)
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
|
|
const args2: ProcessInputArgs = {
|
|
messages: [createMessage("user", "Different message")],
|
|
systemMessages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args2)
|
|
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it("should return messageList in query mode when no user message", async () => {
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "query",
|
|
})
|
|
|
|
const messageList = createMockMessageList()
|
|
const args: ProcessInputArgs = {
|
|
messages: [],
|
|
systemMessages: [],
|
|
messageList,
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
const result = await processor.processInput(args)
|
|
|
|
expect(result).toBe(messageList)
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
expect(messageList.addSystem).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("should handle API errors gracefully", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
statusText: "Internal Server Error",
|
|
text: () => Promise.resolve("Server error"),
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "profile",
|
|
})
|
|
|
|
const messageList = createMockMessageList()
|
|
const args: ProcessInputArgs = {
|
|
messages: [createMessage("user", "Hello")],
|
|
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("should use threadId from options", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "thread-123",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "profile",
|
|
})
|
|
|
|
const args: ProcessInputArgs = {
|
|
messages: [createMessage("user", "Hello")],
|
|
systemMessages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalled()
|
|
})
|
|
|
|
it("should use threadId from requestContext when not in options", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "profile",
|
|
})
|
|
|
|
const requestContext = new RequestContext()
|
|
requestContext.set(MASTRA_THREAD_ID_KEY, "ctx-thread-456")
|
|
|
|
const args: ProcessInputArgs = {
|
|
messages: [createMessage("user", "Hello")],
|
|
systemMessages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
requestContext,
|
|
}
|
|
|
|
await processor.processInput(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalled()
|
|
})
|
|
|
|
it("should handle messages with array content parts", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
mode: "query",
|
|
})
|
|
|
|
const messages: MastraDBMessage[] = [
|
|
{
|
|
id: "msg-1",
|
|
role: "user",
|
|
content: {
|
|
format: 2,
|
|
parts: [
|
|
{ type: "text", text: "Hello " },
|
|
{ type: "text", text: "World" },
|
|
],
|
|
},
|
|
createdAt: new Date(),
|
|
},
|
|
]
|
|
|
|
const messageList = createMockMessageList()
|
|
const args: ProcessInputArgs = {
|
|
messages,
|
|
systemMessages: [],
|
|
messageList,
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processInput(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("SupermemoryOutputProcessor", () => {
|
|
let originalEnv: string | undefined
|
|
let originalFetch: typeof globalThis.fetch
|
|
let fetchMock: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
originalEnv = process.env.SUPERMEMORY_API_KEY
|
|
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
|
originalFetch = globalThis.fetch
|
|
fetchMock = vi.fn()
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalEnv) {
|
|
process.env.SUPERMEMORY_API_KEY = originalEnv
|
|
} else {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
}
|
|
globalThis.fetch = originalFetch
|
|
})
|
|
|
|
describe("constructor", () => {
|
|
it("should create processor with required options", () => {
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
expect(processor.id).toBe("supermemory-output")
|
|
expect(processor.name).toBe("Supermemory Conversation Save")
|
|
})
|
|
})
|
|
|
|
describe("processOutputResult", () => {
|
|
it("should save conversation when addMemory is always", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockConversationResponse()),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-456",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const messages: MastraDBMessage[] = [
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi there!"),
|
|
]
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages,
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("/v4/conversations"),
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: expect.objectContaining({
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${TEST_CONFIG.apiKey}`,
|
|
}),
|
|
}),
|
|
)
|
|
|
|
const callBody = JSON.parse(
|
|
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
)
|
|
expect(callBody.conversationId).toBe("conv-456")
|
|
expect(callBody.messages).toHaveLength(2)
|
|
expect(callBody.containerTags).toContain(TEST_CONFIG.containerTag)
|
|
})
|
|
|
|
it("should not save conversation when addMemory is never", async () => {
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-456",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "never",
|
|
})
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages: [
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi!"),
|
|
],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("should use customId from options for conversation save", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockConversationResponse()),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "my-custom-id",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages: [
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi!"),
|
|
],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
const callBody = JSON.parse(
|
|
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
)
|
|
expect(callBody.conversationId).toBe("my-custom-id")
|
|
})
|
|
|
|
it("should use threadId from requestContext (takes precedence over customId)", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockConversationResponse()),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "fallback-custom-id",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const requestContext = new RequestContext()
|
|
requestContext.set(MASTRA_THREAD_ID_KEY, "ctx-thread-789")
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages: [
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi!"),
|
|
],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
requestContext,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
const callBody = JSON.parse(
|
|
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
)
|
|
// RequestContext threadId takes precedence for per-request dynamic IDs
|
|
expect(callBody.conversationId).toBe("ctx-thread-789")
|
|
})
|
|
|
|
it("should fall back to customId when requestContext has no threadId", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockConversationResponse()),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "fallback-custom-id",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages: [
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi!"),
|
|
],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
const callBody = JSON.parse(
|
|
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
)
|
|
// Falls back to customId when no RequestContext threadId
|
|
expect(callBody.conversationId).toBe("fallback-custom-id")
|
|
})
|
|
|
|
it("should skip system messages when saving", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockConversationResponse()),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-456",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const messages: MastraDBMessage[] = [
|
|
createMessage("system", "You are a helpful assistant"),
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi there!"),
|
|
]
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages,
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
const callBody = JSON.parse(
|
|
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
)
|
|
expect(callBody.messages).toHaveLength(2)
|
|
expect(
|
|
callBody.messages.every((m: { role: string }) => m.role !== "system"),
|
|
).toBe(true)
|
|
})
|
|
|
|
it("should handle messages with array content parts", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(createMockConversationResponse()),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-456",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const messages: MastraDBMessage[] = [
|
|
{
|
|
id: "msg-1",
|
|
role: "user",
|
|
content: {
|
|
format: 2,
|
|
parts: [
|
|
{ type: "text", text: "Hello" },
|
|
{ type: "text", text: " World" },
|
|
],
|
|
},
|
|
createdAt: new Date(),
|
|
},
|
|
{
|
|
id: "msg-2",
|
|
role: "assistant",
|
|
content: {
|
|
format: 2,
|
|
parts: [{ type: "text", text: "Hi!" }],
|
|
},
|
|
createdAt: new Date(),
|
|
},
|
|
]
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages,
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
const callBody = JSON.parse(
|
|
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
)
|
|
expect(callBody.messages).toHaveLength(2)
|
|
})
|
|
|
|
it("should handle save errors gracefully", async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
statusText: "Internal Server Error",
|
|
text: () => Promise.resolve("Server error"),
|
|
})
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-456",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages: [
|
|
createMessage("user", "Hello"),
|
|
createMessage("assistant", "Hi!"),
|
|
],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
// Should not throw
|
|
await expect(processor.processOutputResult(args)).resolves.toBeDefined()
|
|
})
|
|
|
|
it("should not save when no messages to save", async () => {
|
|
const processor = new SupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-456",
|
|
apiKey: TEST_CONFIG.apiKey,
|
|
addMemory: "always",
|
|
})
|
|
|
|
const args: ProcessOutputResultArgs = {
|
|
messages: [],
|
|
messageList: createMockMessageList(),
|
|
abort: vi.fn() as never,
|
|
retryCount: 0,
|
|
}
|
|
|
|
await processor.processOutputResult(args)
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("Factory functions", () => {
|
|
let originalEnv: string | undefined
|
|
|
|
beforeEach(() => {
|
|
originalEnv = process.env.SUPERMEMORY_API_KEY
|
|
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalEnv) {
|
|
process.env.SUPERMEMORY_API_KEY = originalEnv
|
|
} else {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
}
|
|
})
|
|
|
|
describe("createSupermemoryProcessor", () => {
|
|
it("should create input processor", () => {
|
|
const processor = createSupermemoryProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
|
expect(processor.id).toBe("supermemory-input")
|
|
})
|
|
|
|
it("should pass options to processor", () => {
|
|
const processor = createSupermemoryProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: "custom-key",
|
|
mode: "full",
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
|
})
|
|
})
|
|
|
|
describe("createSupermemoryOutputProcessor", () => {
|
|
it("should create output processor", () => {
|
|
const processor = createSupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
|
|
expect(processor.id).toBe("supermemory-output")
|
|
})
|
|
|
|
it("should pass options to processor", () => {
|
|
const processor = createSupermemoryOutputProcessor({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-123",
|
|
apiKey: "custom-key",
|
|
addMemory: "always",
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
|
|
})
|
|
})
|
|
|
|
describe("createSupermemoryProcessors", () => {
|
|
it("should create both input and output processors", () => {
|
|
const { input, output } = createSupermemoryProcessors({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
expect(input).toBeInstanceOf(SupermemoryInputProcessor)
|
|
expect(output).toBeInstanceOf(SupermemoryOutputProcessor)
|
|
})
|
|
|
|
it("should share options between processors", () => {
|
|
const { input, output } = createSupermemoryProcessors({
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-123",
|
|
apiKey: "custom-key",
|
|
mode: "full",
|
|
addMemory: "always",
|
|
})
|
|
expect(input.id).toBe("supermemory-input")
|
|
expect(output.id).toBe("supermemory-output")
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("withSupermemory", () => {
|
|
let originalEnv: string | undefined
|
|
|
|
beforeEach(() => {
|
|
originalEnv = process.env.SUPERMEMORY_API_KEY
|
|
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalEnv) {
|
|
process.env.SUPERMEMORY_API_KEY = originalEnv
|
|
} else {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
}
|
|
})
|
|
|
|
describe("API key validation", () => {
|
|
it("should throw error if API key is not set", () => {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
|
|
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
|
|
|
expect(() => {
|
|
withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
|
})
|
|
|
|
it("should accept API key via options", () => {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
|
|
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
apiKey: "custom-key",
|
|
})
|
|
|
|
expect(enhanced).toBeDefined()
|
|
expect(enhanced.inputProcessors).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe("processor injection", () => {
|
|
it("should inject input and output processors", () => {
|
|
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
|
|
expect(enhanced.inputProcessors).toHaveLength(1)
|
|
expect(enhanced.outputProcessors).toHaveLength(1)
|
|
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
|
|
expect(enhanced.outputProcessors?.[0]?.id).toBe("supermemory-output")
|
|
})
|
|
|
|
it("should preserve original config properties", () => {
|
|
const config: MockAgentConfig = {
|
|
id: "test-agent",
|
|
name: "Test Agent",
|
|
model: "gpt-4",
|
|
customProp: "value",
|
|
}
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
|
|
expect(enhanced.id).toBe("test-agent")
|
|
expect(enhanced.name).toBe("Test Agent")
|
|
expect(enhanced.model).toBe("gpt-4")
|
|
expect(enhanced.customProp).toBe("value")
|
|
})
|
|
|
|
it("should prepend input processor to existing processors", () => {
|
|
const existingInputProcessor: Processor = {
|
|
id: "existing-input",
|
|
name: "Existing Input",
|
|
}
|
|
const config: MockAgentConfig = {
|
|
id: "test-agent",
|
|
name: "Test Agent",
|
|
inputProcessors: [existingInputProcessor],
|
|
}
|
|
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
|
|
expect(enhanced.inputProcessors).toHaveLength(2)
|
|
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
|
|
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
|
|
})
|
|
|
|
it("should append output processor to existing processors", () => {
|
|
const existingOutputProcessor: Processor = {
|
|
id: "existing-output",
|
|
name: "Existing Output",
|
|
}
|
|
const config: MockAgentConfig = {
|
|
id: "test-agent",
|
|
name: "Test Agent",
|
|
outputProcessors: [existingOutputProcessor],
|
|
}
|
|
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
|
|
expect(enhanced.outputProcessors).toHaveLength(2)
|
|
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
|
|
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
|
|
})
|
|
|
|
it("should handle configs with both existing input and output processors", () => {
|
|
const existingInput: Processor = { id: "existing-input" }
|
|
const existingOutput: Processor = { id: "existing-output" }
|
|
const config: MockAgentConfig = {
|
|
id: "test-agent",
|
|
name: "Test Agent",
|
|
inputProcessors: [existingInput],
|
|
outputProcessors: [existingOutput],
|
|
}
|
|
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: TEST_CONFIG.customId,
|
|
})
|
|
|
|
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 passthrough", () => {
|
|
it("should pass options to processors", () => {
|
|
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
|
const enhanced = withSupermemory(config, {
|
|
containerTag: TEST_CONFIG.containerTag,
|
|
customId: "conv-123",
|
|
mode: "full",
|
|
addMemory: "always",
|
|
verbose: true,
|
|
})
|
|
|
|
expect(enhanced.inputProcessors).toHaveLength(1)
|
|
expect(enhanced.outputProcessors).toHaveLength(1)
|
|
})
|
|
})
|
|
})
|