mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-11 03:37:56 +00:00
Every integration (AI SDK, OpenAI, Mastra, VoltAgent, Claude memory) now calls v5 through `supermemory@5`. The config takes one `namespace` instead of `containerTags` or `projectId`, and `withSupermemory` takes `namespace` and `id` instead of `containerTag` and `customId`. No aliases. With no config the tools still use `sm_project_default`. Conversations are stored as one document per conversation keyed by `id` instead of `/v4/conversations`. `memoryForget` drops `reason`; forgetting by text previews with `forgetMatching` and then forgets exact matches by id. Claude memory marks its files with `metadata.source` instead of a second tag. Search keeps the 2.x defaults so results do not shift. `packages/ai-sdk` is removed: npm already deprecates it in favor of `@supermemory/tools/ai-sdk`, so its CI steps and playground wiring go too. `apps/sdk-playground` moves to the new tools API and v5 routes, including its Python server. Tested against production: add, search, profile, list and delete. 112 unit tests pass; type errors drop from 148 to 141, none new. The `supermemory` dependency pins the rc until 5.0.0 is published.
979 lines
26 KiB
TypeScript
979 lines
26 KiB
TypeScript
/**
|
|
* Unit tests for the Mastra integration
|
|
* Tests processors, wrapper, and factory functions
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
|
|
import {
|
|
addBody,
|
|
errorResponse,
|
|
jsonResponse,
|
|
profileBody,
|
|
requestJson,
|
|
requestPath,
|
|
} from "../v5-fetch"
|
|
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",
|
|
namespace: "test-mastra-user",
|
|
id: "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,
|
|
clearSystemMessages: vi.fn(),
|
|
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 = profileBody
|
|
const createMockConversationResponse = () => addBody("mem-123")
|
|
|
|
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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
|
})
|
|
|
|
it("should accept API key via options", () => {
|
|
delete process.env.SUPERMEMORY_API_KEY
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
apiKey: "custom-key",
|
|
})
|
|
expect(processor.id).toBe("supermemory-input")
|
|
})
|
|
})
|
|
|
|
describe("processInput", () => {
|
|
it("should inject memories into messageList", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(
|
|
createMockProfileResponse(
|
|
["User likes TypeScript"],
|
|
["Recent interest in AI"],
|
|
),
|
|
),
|
|
)
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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[0]).toContain(
|
|
'<supermemory context="user-memories" readonly>',
|
|
)
|
|
expect(systemCall?.args[1]).toBe("supermemory")
|
|
})
|
|
|
|
it("should use cached memories on second call with same message", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(createMockProfileResponse(["Cached memory"])),
|
|
)
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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(
|
|
jsonResponse(
|
|
createMockProfileResponse([`Memory from call ${callCount}`]),
|
|
),
|
|
)
|
|
})
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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.mockImplementation(async () => errorResponse(500))
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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.mockImplementation(async () =>
|
|
jsonResponse(createMockProfileResponse(["Memory"])),
|
|
)
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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.mockImplementation(async () =>
|
|
jsonResponse(createMockProfileResponse(["Memory"])),
|
|
)
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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.mockImplementation(async () =>
|
|
jsonResponse(createMockProfileResponse(["Memory"])),
|
|
)
|
|
|
|
const processor = new SupermemoryInputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
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.mockImplementation(async () =>
|
|
jsonResponse(createMockConversationResponse()),
|
|
)
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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)
|
|
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
|
expect(requestPath(url)).toBe(`/ns/${TEST_CONFIG.namespace}/document`)
|
|
expect(init.method).toBe("POST")
|
|
expect(new Headers(init.headers).get("authorization")).toBe(
|
|
`Bearer ${TEST_CONFIG.apiKey}`,
|
|
)
|
|
|
|
const callBody = requestJson(fetchMock.mock.calls[0]?.[1])
|
|
expect(callBody.id).toBe("conv-456")
|
|
expect(String(callBody.content).split("\n\n")).toHaveLength(2)
|
|
expect(callBody.dreaming).toBe("instant")
|
|
})
|
|
|
|
it("should not save conversation when addMemory is never", async () => {
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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 id from options for conversation save", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(createMockConversationResponse()),
|
|
)
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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 = requestJson(fetchMock.mock.calls[0]?.[1])
|
|
expect(callBody.id).toBe("my-custom-id")
|
|
})
|
|
|
|
it("should use threadId from requestContext (takes precedence over id)", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(createMockConversationResponse()),
|
|
)
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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 = requestJson(fetchMock.mock.calls[0]?.[1])
|
|
// RequestContext threadId takes precedence for per-request dynamic IDs
|
|
expect(callBody.id).toBe("ctx-thread-789")
|
|
})
|
|
|
|
it("should fall back to id when requestContext has no threadId", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(createMockConversationResponse()),
|
|
)
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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 = requestJson(fetchMock.mock.calls[0]?.[1])
|
|
// Falls back to id when no RequestContext threadId
|
|
expect(callBody.id).toBe("fallback-custom-id")
|
|
})
|
|
|
|
it("should skip system messages when saving", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(createMockConversationResponse()),
|
|
)
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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 = requestJson(fetchMock.mock.calls[0]?.[1])
|
|
expect(String(callBody.content).split("\n\n")).toHaveLength(2)
|
|
expect(callBody.content).not.toContain("System:")
|
|
})
|
|
|
|
it("should handle messages with array content parts", async () => {
|
|
fetchMock.mockImplementation(async () =>
|
|
jsonResponse(createMockConversationResponse()),
|
|
)
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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 = requestJson(fetchMock.mock.calls[0]?.[1])
|
|
expect(String(callBody.content).split("\n\n")).toHaveLength(2)
|
|
})
|
|
|
|
it("should handle save errors gracefully", async () => {
|
|
fetchMock.mockImplementation(async () => errorResponse(500))
|
|
|
|
const processor = new SupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
|
expect(processor.id).toBe("supermemory-input")
|
|
})
|
|
|
|
it("should pass options to processor", () => {
|
|
const processor = createSupermemoryProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
apiKey: "custom-key",
|
|
mode: "full",
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
|
})
|
|
})
|
|
|
|
describe("createSupermemoryOutputProcessor", () => {
|
|
it("should create output processor", () => {
|
|
const processor = createSupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
|
|
expect(processor.id).toBe("supermemory-output")
|
|
})
|
|
|
|
it("should pass options to processor", () => {
|
|
const processor = createSupermemoryOutputProcessor({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
expect(input).toBeInstanceOf(SupermemoryInputProcessor)
|
|
expect(output).toBeInstanceOf(SupermemoryOutputProcessor)
|
|
})
|
|
|
|
it("should share options between processors", () => {
|
|
const { input, output } = createSupermemoryProcessors({
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
}).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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
|
|
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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
|
|
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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
|
|
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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
|
|
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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: TEST_CONFIG.id,
|
|
})
|
|
|
|
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, {
|
|
namespace: TEST_CONFIG.namespace,
|
|
id: "conv-123",
|
|
mode: "full",
|
|
addMemory: "always",
|
|
verbose: true,
|
|
})
|
|
|
|
expect(enhanced.inputProcessors).toHaveLength(1)
|
|
expect(enhanced.outputProcessors).toHaveLength(1)
|
|
})
|
|
})
|
|
})
|