Skip to main content
Glama
modify-passage.test.js27.3 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { handleModifyPassage, modifyPassageDefinition, } from '../../../tools/passages/modify-passage.js'; import { createMockLettaServer } from '../../utils/mock-server.js'; import { expectValidToolResponse } from '../../utils/test-helpers.js'; describe('Modify Passage', () => { let mockServer; beforeEach(() => { mockServer = createMockLettaServer(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('Tool Definition', () => { it('should have correct tool definition', () => { expect(modifyPassageDefinition.name).toBe('modify_passage'); expect(modifyPassageDefinition.description).toContain( "Modify a memory in the agent's archival memory", ); expect(modifyPassageDefinition.inputSchema.required).toEqual([ 'agent_id', 'memory_id', 'update_data', ]); expect(modifyPassageDefinition.inputSchema.properties).toHaveProperty('agent_id'); expect(modifyPassageDefinition.inputSchema.properties).toHaveProperty('memory_id'); expect(modifyPassageDefinition.inputSchema.properties).toHaveProperty('update_data'); expect(modifyPassageDefinition.inputSchema.properties).toHaveProperty( 'include_embeddings', ); expect(modifyPassageDefinition.inputSchema.properties.update_data.required).toEqual([ 'text', ]); expect(modifyPassageDefinition.inputSchema.properties.include_embeddings.default).toBe( false, ); }); }); describe('Functionality Tests', () => { it('should modify passage text successfully without embeddings', async () => { const agentId = 'agent-123'; const memoryId = 'passage-456'; const newText = 'Updated memory content for archival storage'; const existingPassage = { id: memoryId, text: 'Original memory content', embedding: [0.1, 0.2, 0.3], embedding_config: { model: 'text-embedding-ada-002' }, metadata: { created_at: '2024-01-01T00:00:00Z' }, }; const modifiedPassage = { ...existingPassage, text: newText, updated_at: '2024-01-02T00:00:00Z', }; // Mock fetching all passages mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage, { id: 'other-passage', text: 'Other' }], }); // Mock PATCH update mockServer.api.patch.mockResolvedValueOnce({ data: [modifiedPassage], }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: newText }, }); // Verify fetching passages with embeddings expect(mockServer.api.get).toHaveBeenCalledWith( `/agents/${agentId}/archival-memory`, expect.objectContaining({ headers: expect.any(Object), params: { include_embeddings: true }, }), ); // Verify PATCH call with full payload expect(mockServer.api.patch).toHaveBeenCalledWith( `/agents/${agentId}/archival-memory/${memoryId}`, expect.objectContaining({ id: memoryId, text: newText, embedding: [0.1, 0.2, 0.3], embedding_config: { model: 'text-embedding-ada-002' }, }), expect.objectContaining({ headers: expect.any(Object), }), ); // Verify response without embeddings const data = expectValidToolResponse(result); expect(data.passages).toHaveLength(1); expect(data.passages[0].text).toBe(newText); expect(data.passages[0].embedding).toBeUndefined(); expect(data.passages[0].updated_at).toBe('2024-01-02T00:00:00Z'); }); it('should include embeddings when requested', async () => { const agentId = 'agent-embed'; const memoryId = 'passage-embed'; const newText = 'Modified text with embeddings'; const embeddings = [0.4, 0.5, 0.6]; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1, 0.2, 0.3], embedding_config: { model: 'ada' }, }; const modifiedPassage = { ...existingPassage, text: newText, embedding: embeddings, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [modifiedPassage] }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: newText }, include_embeddings: true, }); const data = expectValidToolResponse(result); expect(data.passages[0].embedding).toEqual(embeddings); }); it('should handle special characters in agent ID', async () => { const agentId = 'agent@special#id'; const encodedAgentId = encodeURIComponent(agentId); const memoryId = 'passage-123'; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [existingPassage] }); await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated' }, }); expect(mockServer.api.get).toHaveBeenCalledWith( `/agents/${encodedAgentId}/archival-memory`, expect.any(Object), ); expect(mockServer.api.patch).toHaveBeenCalledWith( `/agents/${encodedAgentId}/archival-memory/${memoryId}`, expect.any(Object), expect.any(Object), ); }); it('should handle special characters in memory ID', async () => { const agentId = 'agent-123'; const memoryId = 'passage@special#id'; const encodedMemoryId = encodeURIComponent(memoryId); const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [existingPassage] }); await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated' }, }); expect(mockServer.api.patch).toHaveBeenCalledWith( `/agents/${agentId}/archival-memory/${encodedMemoryId}`, expect.any(Object), expect.any(Object), ); }); it('should preserve all existing fields when updating text', async () => { const agentId = 'agent-preserve'; const memoryId = 'passage-preserve'; const existingPassage = { id: memoryId, text: 'Original text', embedding: [0.1, 0.2], embedding_config: { model: 'text-embedding-ada-002', dimensions: 1536, }, metadata: { source: 'user', tags: ['important', 'reference'], custom_field: 'custom_value', }, created_at: '2024-01-01T00:00:00Z', created_by_id: 'user-123', organization_id: 'org-456', }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [{ ...existingPassage, text: 'Updated text' }], }); await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated text' }, }); // Verify all fields are preserved in the PATCH payload expect(mockServer.api.patch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ id: memoryId, text: 'Updated text', embedding: [0.1, 0.2], embedding_config: existingPassage.embedding_config, metadata: existingPassage.metadata, created_at: '2024-01-01T00:00:00Z', created_by_id: 'user-123', organization_id: 'org-456', }), expect.any(Object), ); }); it('should handle very long text updates', async () => { const agentId = 'agent-long'; const memoryId = 'passage-long'; const longText = 'B'.repeat(10000); // 10k characters const existingPassage = { id: memoryId, text: 'Short original', embedding: new Array(1536).fill(0.1), embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [{ ...existingPassage, text: longText }], }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: longText }, }); const data = expectValidToolResponse(result); expect(data.passages[0].text).toBe(longText); }); it('should handle multiple passages returned from update', async () => { const agentId = 'agent-multi'; const memoryId = 'passage-multi'; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; const modifiedPassages = [ { id: memoryId, text: 'Updated part 1', embedding: [0.2] }, { id: 'new-passage', text: 'Updated part 2', embedding: [0.3] }, ]; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: modifiedPassages }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated text that gets split' }, }); const data = expectValidToolResponse(result); expect(data.passages).toHaveLength(2); expect(data.passages[0].embedding).toBeUndefined(); expect(data.passages[1].embedding).toBeUndefined(); }); it('should handle empty text updates', async () => { const agentId = 'agent-empty'; const memoryId = 'passage-empty'; const existingPassage = { id: memoryId, text: 'Will be cleared', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [{ ...existingPassage, text: '' }], }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: '' }, }); const data = expectValidToolResponse(result); expect(data.passages[0].text).toBe(''); }); it('should handle special characters in text', async () => { const agentId = 'agent-special'; const memoryId = 'passage-special'; const specialText = 'Line 1\nLine 2\t"Quoted"\r\n\'Apostrophes\' & symbols < > {} 🚀'; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [{ ...existingPassage, text: specialText }], }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: specialText }, }); const data = expectValidToolResponse(result); expect(data.passages[0].text).toBe(specialText); }); }); describe('Error Handling', () => { it('should handle missing agent_id', async () => { await expect( handleModifyPassage(mockServer, { memory_id: 'passage-123', update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( 'Missing required argument: agent_id', ); }); it('should handle missing memory_id', async () => { await expect( handleModifyPassage(mockServer, { agent_id: 'agent-123', update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( 'Missing required argument: memory_id', ); }); it('should handle missing update_data', async () => { await expect( handleModifyPassage(mockServer, { agent_id: 'agent-123', memory_id: 'passage-123', }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining('Missing or invalid required argument: update_data'), ); }); it('should handle update_data without text field', async () => { await expect( handleModifyPassage(mockServer, { agent_id: 'agent-123', memory_id: 'passage-123', update_data: {}, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining("update_data must contain a 'text' field"), ); }); it('should handle non-string text in update_data', async () => { await expect( handleModifyPassage(mockServer, { agent_id: 'agent-123', memory_id: 'passage-123', update_data: { text: 123 }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining("update_data must contain a 'text' field (string)"), ); }); it('should handle passage not found', async () => { const agentId = 'agent-123'; const memoryId = 'non-existent-passage'; // Return passages but without the requested one mockServer.api.get.mockResolvedValueOnce({ data: [ { id: 'other-passage-1', text: 'Other' }, { id: 'other-passage-2', text: 'Another' }, ], }); await expect( handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining(`Could not find passage ${memoryId}`), ); }); it('should handle passage missing required fields', async () => { const agentId = 'agent-123'; const memoryId = 'incomplete-passage'; const incompletePassage = { id: memoryId, text: 'Text only', // Missing embedding and embedding_config }; mockServer.api.get.mockResolvedValueOnce({ data: [incompletePassage] }); await expect( handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining('is missing required fields'), ); }); it('should handle 404 agent not found during list', async () => { const error = new Error('Not found'); error.response = { status: 404, data: { error: 'Agent not found' }, }; mockServer.api.get.mockRejectedValueOnce(error); await expect( handleModifyPassage(mockServer, { agent_id: 'non-existent-agent', memory_id: 'passage-123', update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( 'Agent not found when listing passages: non-existent-agent', ); }); it('should handle 404 during PATCH update', async () => { const agentId = 'agent-123'; const memoryId = 'passage-123'; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); const error = new Error('Not found'); error.response = { status: 404, data: { error: 'Passage not found' }, }; mockServer.api.patch.mockRejectedValueOnce(error); await expect( handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining('Agent or Passage not found during update'), ); }); it('should handle 422 validation error', async () => { const agentId = 'agent-123'; const memoryId = 'passage-123'; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); const error = new Error('Validation error'); error.response = { status: 422, data: { detail: 'Text exceeds maximum length', errors: { text: ['Too long'] }, }, }; mockServer.api.patch.mockRejectedValueOnce(error); await expect( handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'X'.repeat(100000) }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining('Validation error modifying passage'), ); }); it('should handle generic API errors', async () => { const agentId = 'agent-123'; const memoryId = 'passage-123'; const existingPassage = { id: memoryId, text: 'Original', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); const error = new Error('Internal server error'); error.response = { status: 500, data: { error: 'Database error' }, }; mockServer.api.patch.mockRejectedValueOnce(error); await expect( handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith(error); }); it('should handle network errors', async () => { const error = new Error('Network error: Connection refused'); mockServer.api.get.mockRejectedValueOnce(error); await expect( handleModifyPassage(mockServer, { agent_id: 'agent-123', memory_id: 'passage-123', update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining('Failed to fetch passages'), ); }); }); describe('Edge Cases', () => { it('should handle null args', async () => { await expect(handleModifyPassage(mockServer, null)).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( 'Missing required argument: agent_id', ); }); it('should handle non-array response from list', async () => { const agentId = 'agent-123'; const memoryId = 'passage-123'; // API returns non-array mockServer.api.get.mockResolvedValueOnce({ data: { invalid: 'response' }, }); await expect( handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'New text' }, }), ).rejects.toThrow(); expect(mockServer.createErrorResponse).toHaveBeenCalledWith( expect.stringContaining(`Could not find passage ${memoryId}`), ); }); it('should handle UUID format IDs', async () => { const agentId = '550e8400-e29b-41d4-a716-446655440000'; const memoryId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const existingPassage = { id: memoryId, text: 'UUID passage', embedding: [0.1], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [existingPassage] }); await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated UUID passage' }, }); expect(mockServer.api.patch).toHaveBeenCalledWith( `/agents/${agentId}/archival-memory/${memoryId}`, expect.any(Object), expect.any(Object), ); }); it('should handle passages with null embeddings', async () => { const agentId = 'agent-null'; const memoryId = 'passage-null'; const existingPassage = { id: memoryId, text: 'Original', embedding: null, embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: [existingPassage] }); mockServer.api.patch.mockResolvedValueOnce({ data: [{ ...existingPassage, text: 'Updated' }], }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated' }, include_embeddings: true, }); const data = expectValidToolResponse(result); expect(data.passages[0].embedding).toBeNull(); }); it('should handle very large passage lists when searching', async () => { const agentId = 'agent-large'; const memoryId = 'target-passage'; // Create 1000 passages const manyPassages = Array.from({ length: 1000 }, (_, i) => ({ id: `passage-${i}`, text: `Passage ${i}`, embedding: [i * 0.001], embedding_config: { model: 'ada' }, })); // Add target passage in the middle manyPassages[500] = { id: memoryId, text: 'Target passage', embedding: [0.5], embedding_config: { model: 'ada' }, }; mockServer.api.get.mockResolvedValueOnce({ data: manyPassages }); mockServer.api.patch.mockResolvedValueOnce({ data: [{ ...manyPassages[500], text: 'Updated target' }], }); const result = await handleModifyPassage(mockServer, { agent_id: agentId, memory_id: memoryId, update_data: { text: 'Updated target' }, }); const data = expectValidToolResponse(result); expect(data.passages[0].text).toBe('Updated target'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/oculairmedia/Letta-MCP-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server