attach-memory-block.test.js•21.2 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
    handleAttachMemoryBlock,
    attachMemoryBlockToolDefinition,
} from '../../../tools/memory/attach-memory-block.js';
import { createMockLettaServer } from '../../utils/mock-server.js';
import { expectValidToolResponse } from '../../utils/test-helpers.js';
describe('Attach Memory Block', () => {
    let mockServer;
    beforeEach(() => {
        mockServer = createMockLettaServer();
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    describe('Tool Definition', () => {
        it('should have correct tool definition', () => {
            expect(attachMemoryBlockToolDefinition.name).toBe('attach_memory_block');
            expect(attachMemoryBlockToolDefinition.description).toContain(
                'Attach a memory block to an agent',
            );
            expect(attachMemoryBlockToolDefinition.inputSchema.required).toEqual([
                'block_id',
                'agent_id',
            ]);
            expect(attachMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty(
                'block_id',
            );
            expect(attachMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty(
                'agent_id',
            );
            expect(attachMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty('label');
        });
    });
    describe('Functionality Tests', () => {
        it('should attach memory block successfully with minimal args', async () => {
            const blockId = 'block-123';
            const agentId = 'agent-456';
            const mockBlock = {
                id: blockId,
                name: 'Test Memory Block',
                label: 'persona',
                value: 'I am a helpful assistant',
            };
            const mockAgent = {
                id: agentId,
                name: 'Test Agent',
            };
            // Mock block verification
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            // Mock attachment (patch returns empty)
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            // Mock agent info retrieval
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            // Verify block verification call
            expect(mockServer.api.get).toHaveBeenCalledWith(
                `/blocks/${blockId}`,
                expect.objectContaining({
                    headers: expect.objectContaining({
                        user_id: agentId,
                    }),
                }),
            );
            // Verify attachment call
            expect(mockServer.api.patch).toHaveBeenCalledWith(
                `/agents/${agentId}/core-memory/blocks/attach/${blockId}`,
                {},
                expect.objectContaining({
                    headers: expect.objectContaining({
                        user_id: agentId,
                    }),
                }),
            );
            // Verify agent info call
            expect(mockServer.api.get).toHaveBeenCalledWith(
                `/agents/${agentId}`,
                expect.objectContaining({
                    headers: expect.objectContaining({
                        user_id: agentId,
                    }),
                }),
            );
            // Verify response
            const data = expectValidToolResponse(result);
            expect(data.agent_id).toBe(agentId);
            expect(data.agent_name).toBe('Test Agent');
            expect(data.block_id).toBe(blockId);
            expect(data.block_name).toBe('Test Memory Block');
            expect(data.label).toBe('persona'); // Uses block's label
        });
        it('should attach with custom label override', async () => {
            const blockId = 'block-custom';
            const agentId = 'agent-custom';
            const mockBlock = {
                id: blockId,
                name: 'Original Label Block',
                label: 'persona', // Original label
                value: 'Content',
            };
            const mockAgent = {
                id: agentId,
                name: 'Custom Label Agent',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
                label: 'human', // Override label
            });
            const data = expectValidToolResponse(result);
            expect(data.label).toBe('human'); // Should use provided label
        });
        it('should handle block without name', async () => {
            const blockId = 'nameless-block';
            const agentId = 'agent-nameless';
            const mockBlock = {
                id: blockId,
                // No name field
                label: 'system',
                value: 'System content',
            };
            const mockAgent = {
                id: agentId,
                name: 'Agent for Nameless',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            const data = expectValidToolResponse(result);
            expect(data.block_name).toBe('Unnamed Block');
        });
        it('should handle agent without name', async () => {
            const blockId = 'block-noagent';
            const agentId = 'nameless-agent';
            const mockBlock = {
                id: blockId,
                name: 'Block for Nameless Agent',
                label: 'persona',
            };
            const mockAgent = {
                id: agentId,
                // No name field
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            const data = expectValidToolResponse(result);
            expect(data.agent_name).toBe('Unknown');
        });
        it('should use default label when block has no label', async () => {
            const blockId = 'no-label-block';
            const agentId = 'agent-default-label';
            const mockBlock = {
                id: blockId,
                name: 'No Label Block',
                // No label field
                value: 'Content without label',
            };
            const mockAgent = {
                id: agentId,
                name: 'Default Label Agent',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            const data = expectValidToolResponse(result);
            expect(data.label).toBe('custom'); // Default when no label
        });
        it('should attach different types of memory blocks', async () => {
            const labels = ['persona', 'human', 'system', 'custom_type'];
            for (const label of labels) {
                const blockId = `block-${label}`;
                const agentId = `agent-${label}`;
                const mockBlock = {
                    id: blockId,
                    name: `${label} Block`,
                    label: label,
                    value: `Content for ${label}`,
                };
                const mockAgent = {
                    id: agentId,
                    name: `${label} Agent`,
                };
                mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
                mockServer.api.patch.mockResolvedValueOnce({ data: {} });
                mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
                const result = await handleAttachMemoryBlock(mockServer, {
                    block_id: blockId,
                    agent_id: agentId,
                });
                const data = expectValidToolResponse(result);
                expect(data.label).toBe(label);
            }
        });
        it('should include user_id header in all API calls', async () => {
            const blockId = 'auth-block';
            const agentId = 'auth-agent';
            mockServer.api.get.mockResolvedValueOnce({ data: { id: blockId } });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: { id: agentId } });
            await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            // Verify all calls included user_id header
            expect(mockServer.api.get).toHaveBeenCalledTimes(2);
            expect(mockServer.api.patch).toHaveBeenCalledTimes(1);
            // All calls should have user_id header
            const allCalls = [...mockServer.api.get.mock.calls, ...mockServer.api.patch.mock.calls];
            allCalls.forEach((call) => {
                const headers = call[1]?.headers || call[2]?.headers;
                expect(headers).toHaveProperty('user_id', agentId);
            });
        });
        it('should handle attaching template blocks', async () => {
            const blockId = 'template-block';
            const agentId = 'template-agent';
            const mockBlock = {
                id: blockId,
                name: 'Template Block',
                label: 'system',
                value: 'Template with {{variable}}',
                is_template: true,
                template_variables: ['variable'],
            };
            const mockAgent = {
                id: agentId,
                name: 'Template Using Agent',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            const data = expectValidToolResponse(result);
            expect(data.block_id).toBe(blockId);
            expect(data.agent_id).toBe(agentId);
        });
    });
    describe('Error Handling', () => {
        it('should throw error for missing block_id', async () => {
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    agent_id: 'agent-123',
                }),
            ).rejects.toThrow('Missing required argument: block_id');
        });
        it('should throw error for missing agent_id', async () => {
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: 'block-123',
                }),
            ).rejects.toThrow('Missing required argument: agent_id');
        });
        it('should throw error for missing both required args', async () => {
            await expect(handleAttachMemoryBlock(mockServer, {})).rejects.toThrow(
                'Missing required argument: block_id',
            );
        });
        it('should handle block not found error', async () => {
            const error = new Error('Block not found');
            error.response = {
                status: 404,
                data: { error: 'Memory block not found' },
            };
            mockServer.api.get.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: 'non-existent-block',
                    agent_id: 'agent-123',
                }),
            ).rejects.toThrow('Block not found');
        });
        it('should handle agent not found error during attachment', async () => {
            const blockId = 'block-123';
            const agentId = 'non-existent-agent';
            // Block exists
            mockServer.api.get.mockResolvedValueOnce({
                data: { id: blockId, name: 'Test Block' },
            });
            // Attachment fails - agent not found
            const error = new Error('Agent not found');
            error.response = {
                status: 404,
                data: { error: 'Agent not found' },
            };
            mockServer.api.patch.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: blockId,
                    agent_id: agentId,
                }),
            ).rejects.toThrow('Agent not found');
        });
        it('should handle attachment conflict error', async () => {
            const blockId = 'conflict-block';
            const agentId = 'conflict-agent';
            // Block exists
            mockServer.api.get.mockResolvedValueOnce({
                data: { id: blockId, name: 'Conflict Block' },
            });
            // Attachment fails - already attached
            const error = new Error('Conflict');
            error.response = {
                status: 409,
                data: { error: 'Memory block already attached to agent' },
            };
            mockServer.api.patch.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: blockId,
                    agent_id: agentId,
                }),
            ).rejects.toThrow('Conflict');
        });
        it('should handle forbidden access error', async () => {
            const blockId = 'forbidden-block';
            const agentId = 'forbidden-agent';
            const error = new Error('Forbidden');
            error.response = {
                status: 403,
                data: { error: 'Not authorized to access this block' },
            };
            mockServer.api.get.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: blockId,
                    agent_id: agentId,
                }),
            ).rejects.toThrow('Forbidden');
        });
        it('should handle error when retrieving agent info after attachment', async () => {
            const blockId = 'block-123';
            const agentId = 'agent-error';
            // Block verification succeeds
            mockServer.api.get.mockResolvedValueOnce({
                data: { id: blockId, name: 'Test Block' },
            });
            // Attachment succeeds
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            // Agent info retrieval fails
            const error = new Error('Failed to get agent');
            error.response = { status: 500 };
            mockServer.api.get.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: blockId,
                    agent_id: agentId,
                }),
            ).rejects.toThrow('Failed to get agent');
        });
        it('should handle network errors', async () => {
            const error = new Error('Network error: Connection refused');
            mockServer.api.get.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: 'block-123',
                    agent_id: 'agent-123',
                }),
            ).rejects.toThrow('Network error');
        });
        it('should handle server errors', async () => {
            const error = new Error('Internal server error');
            error.response = {
                status: 500,
                data: { error: 'Database error' },
            };
            mockServer.api.get.mockRejectedValueOnce(error);
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: 'block-123',
                    agent_id: 'agent-123',
                }),
            ).rejects.toThrow('Internal server error');
        });
    });
    describe('Edge Cases', () => {
        it('should handle empty string IDs gracefully', async () => {
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: '',
                    agent_id: 'agent-123',
                }),
            ).rejects.toThrow('Missing required argument: block_id');
            await expect(
                handleAttachMemoryBlock(mockServer, {
                    block_id: 'block-123',
                    agent_id: '',
                }),
            ).rejects.toThrow('Missing required argument: agent_id');
        });
        it('should handle UUID format IDs', async () => {
            const blockId = '550e8400-e29b-41d4-a716-446655440000';
            const agentId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
            const mockBlock = {
                id: blockId,
                name: 'UUID Block',
                label: 'system',
            };
            const mockAgent = {
                id: agentId,
                name: 'UUID Agent',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            expect(mockServer.api.patch).toHaveBeenCalledWith(
                `/agents/${agentId}/core-memory/blocks/attach/${blockId}`,
                {},
                expect.any(Object),
            );
            const data = expectValidToolResponse(result);
            expect(data.block_id).toBe(blockId);
            expect(data.agent_id).toBe(agentId);
        });
        it('should handle special characters in names', async () => {
            const blockId = 'special-block';
            const agentId = 'special-agent';
            const mockBlock = {
                id: blockId,
                name: 'Block with "quotes" & symbols <tag>',
                label: 'persona',
            };
            const mockAgent = {
                id: agentId,
                name: 'Agent: Special édition 🚀',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            const data = expectValidToolResponse(result);
            expect(data.block_name).toBe('Block with "quotes" & symbols <tag>');
            expect(data.agent_name).toBe('Agent: Special édition 🚀');
        });
        it('should handle attaching shared blocks', async () => {
            const blockId = 'shared-block';
            const agentId = 'new-agent';
            const mockBlock = {
                id: blockId,
                name: 'Shared System Config',
                label: 'system',
                value: 'Shared configuration',
                shared: true,
                agents: [
                    { id: 'agent-1', name: 'Agent One' },
                    { id: 'agent-2', name: 'Agent Two' },
                ],
            };
            const mockAgent = {
                id: agentId,
                name: 'New Agent',
            };
            mockServer.api.get.mockResolvedValueOnce({ data: mockBlock });
            mockServer.api.patch.mockResolvedValueOnce({ data: {} });
            mockServer.api.get.mockResolvedValueOnce({ data: mockAgent });
            const result = await handleAttachMemoryBlock(mockServer, {
                block_id: blockId,
                agent_id: agentId,
            });
            const data = expectValidToolResponse(result);
            expect(data.block_id).toBe(blockId);
            expect(data.agent_id).toBe(agentId);
        });
    });
});