update-memory-block.test.js•19.9 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
handleUpdateMemoryBlock,
updateMemoryBlockToolDefinition,
} from '../../../tools/memory/update-memory-block.js';
import { createMockLettaServer } from '../../utils/mock-server.js';
import { expectValidToolResponse } from '../../utils/test-helpers.js';
describe('Update Memory Block', () => {
let mockServer;
beforeEach(() => {
mockServer = createMockLettaServer();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('Tool Definition', () => {
it('should have correct tool definition', () => {
expect(updateMemoryBlockToolDefinition.name).toBe('update_memory_block');
expect(updateMemoryBlockToolDefinition.description).toContain(
'Update the contents and metadata',
);
expect(updateMemoryBlockToolDefinition.inputSchema.required).toEqual(['block_id']);
expect(updateMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty(
'block_id',
);
expect(updateMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty('value');
expect(updateMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty(
'metadata',
);
expect(updateMemoryBlockToolDefinition.inputSchema.properties).toHaveProperty(
'agent_id',
);
});
});
describe('Functionality Tests', () => {
it('should update memory block value successfully', async () => {
const updatedBlock = {
id: 'block-123',
name: 'Updated Block',
label: 'persona',
value: 'Updated content for the memory block',
limit: 5000,
metadata: { version: '1.0' },
updated_at: '2024-01-02T00:00:00Z',
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'block-123',
value: 'Updated content for the memory block',
});
// Verify API call
expect(mockServer.api.patch).toHaveBeenCalledWith(
'/blocks/block-123',
{
value: 'Updated content for the memory block',
},
expect.objectContaining({
headers: expect.any(Object),
}),
);
// Verify response
const data = expectValidToolResponse(result);
expect(data.id).toBe('block-123');
expect(data.value).toBe('Updated content for the memory block');
expect(data.updated_at).toBe('2024-01-02T00:00:00Z');
});
it('should update memory block metadata successfully', async () => {
const newMetadata = {
version: '2.0',
tags: ['updated', 'important'],
last_modified_by: 'user-123',
};
const updatedBlock = {
id: 'block-456',
name: 'Metadata Update Block',
label: 'system',
value: 'Original content',
metadata: newMetadata,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'block-456',
metadata: newMetadata,
});
// Verify only metadata was sent
expect(mockServer.api.patch).toHaveBeenCalledWith(
'/blocks/block-456',
{
metadata: newMetadata,
},
expect.any(Object),
);
const data = expectValidToolResponse(result);
expect(data.metadata).toEqual(newMetadata);
expect(data.value).toBe('Original content'); // Value unchanged
});
it('should update both value and metadata', async () => {
const newValue = 'Completely new content';
const newMetadata = {
version: '3.0',
complete_update: true,
};
const updatedBlock = {
id: 'block-789',
name: 'Complete Update Block',
label: 'human',
value: newValue,
metadata: newMetadata,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'block-789',
value: newValue,
metadata: newMetadata,
});
// Verify both fields were sent
expect(mockServer.api.patch).toHaveBeenCalledWith(
'/blocks/block-789',
{
value: newValue,
metadata: newMetadata,
},
expect.any(Object),
);
const data = expectValidToolResponse(result);
expect(data.value).toBe(newValue);
expect(data.metadata).toEqual(newMetadata);
});
it('should update with agent_id authorization', async () => {
const agentId = 'agent-auth-123';
const updatedBlock = {
id: 'agent-block',
value: 'Agent authorized update',
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'agent-block',
value: 'Agent authorized update',
agent_id: agentId,
});
// Verify user_id header was included
expect(mockServer.api.patch).toHaveBeenCalledWith(
'/blocks/agent-block',
expect.any(Object),
expect.objectContaining({
headers: expect.objectContaining({
user_id: agentId,
}),
}),
);
const data = expectValidToolResponse(result);
expect(data.value).toBe('Agent authorized update');
});
it('should handle updating to empty string value', async () => {
// Empty string is considered as falsy, so the tool will reject it
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'empty-block',
value: '',
}),
).rejects.toThrow('Either value or metadata must be provided');
});
it('should handle updating to empty metadata object', async () => {
const updatedBlock = {
id: 'empty-metadata-block',
value: 'Content',
metadata: {},
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'empty-metadata-block',
metadata: {},
});
expect(mockServer.api.patch).toHaveBeenCalledWith(
'/blocks/empty-metadata-block',
{
metadata: {},
},
expect.any(Object),
);
const data = expectValidToolResponse(result);
expect(data.metadata).toEqual({});
});
it('should handle very large value updates', async () => {
const largeValue = 'Y'.repeat(10000); // 10k characters
const updatedBlock = {
id: 'large-update-block',
value: largeValue,
limit: 10000,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'large-update-block',
value: largeValue,
});
const data = expectValidToolResponse(result);
expect(data.value).toBe(largeValue);
expect(data.value.length).toBe(10000);
});
it('should preserve undefined fields during update', async () => {
const originalBlock = {
id: 'preserve-block',
name: 'Original Name',
label: 'persona',
value: 'New value only',
metadata: { original: true },
limit: 5000,
};
mockServer.api.patch.mockResolvedValueOnce({ data: originalBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'preserve-block',
value: 'New value only',
// Not updating metadata
});
// Verify only value was sent in update
expect(mockServer.api.patch).toHaveBeenCalledWith(
'/blocks/preserve-block',
{
value: 'New value only',
// metadata should not be included
},
expect.any(Object),
);
const data = expectValidToolResponse(result);
expect(data.name).toBe('Original Name');
expect(data.label).toBe('persona');
expect(data.metadata).toEqual({ original: true });
});
it('should handle complex nested metadata updates', async () => {
const complexMetadata = {
level1: {
level2: {
level3: {
deep_value: 'nested content',
array: [1, 2, { nested: true }],
},
},
},
timestamps: {
created: '2024-01-01T00:00:00Z',
modified: '2024-01-02T00:00:00Z',
},
flags: {
active: true,
reviewed: false,
priority: 'high',
},
};
const updatedBlock = {
id: 'complex-metadata-block',
value: 'Content',
metadata: complexMetadata,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'complex-metadata-block',
metadata: complexMetadata,
});
const data = expectValidToolResponse(result);
expect(data.metadata).toEqual(complexMetadata);
expect(data.metadata.level1.level2.level3.deep_value).toBe('nested content');
});
it('should handle special characters in updates', async () => {
const specialValue =
'Line 1\nLine 2\tTabbed\r\nWindows line\n\nDouble space\n"Quoted" text';
const specialMetadata = {
unicode: '你好世界 🌍',
emoji: '🚀🎉🔧',
symbols: '!@#$%^&*()_+-=[]{}|;:,.<>?',
};
const updatedBlock = {
id: 'special-chars-block',
value: specialValue,
metadata: specialMetadata,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'special-chars-block',
value: specialValue,
metadata: specialMetadata,
});
const data = expectValidToolResponse(result);
expect(data.value).toBe(specialValue);
expect(data.metadata.unicode).toBe('你好世界 🌍');
});
});
describe('Error Handling', () => {
it('should throw error for missing block_id', async () => {
await expect(
handleUpdateMemoryBlock(mockServer, {
value: 'Some value',
}),
).rejects.toThrow('Missing required argument: block_id');
});
it('should throw error for null block_id', async () => {
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: null,
value: 'Some value',
}),
).rejects.toThrow('Missing required argument: block_id');
});
it('should throw error when neither value nor metadata provided', async () => {
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'block-123',
}),
).rejects.toThrow('Either value or metadata must be provided');
});
it('should throw error for undefined args', async () => {
await expect(handleUpdateMemoryBlock(mockServer, undefined)).rejects.toThrow(
'Missing required argument: block_id',
);
});
it('should handle 404 error when block not found', async () => {
const error = new Error('Not found');
error.response = {
status: 404,
data: { error: 'Memory block not found' },
};
mockServer.api.patch.mockRejectedValueOnce(error);
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'non-existent-block',
value: 'New value',
}),
).rejects.toThrow('Not found');
});
it('should handle 403 forbidden error', async () => {
const error = new Error('Forbidden');
error.response = {
status: 403,
data: { error: 'Cannot update this memory block' },
};
mockServer.api.patch.mockRejectedValueOnce(error);
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'protected-block',
value: 'Attempted update',
}),
).rejects.toThrow('Forbidden');
});
it('should handle 422 validation error', async () => {
const error = new Error('Validation failed');
error.response = {
status: 422,
data: {
error: 'Validation error',
details: {
value: 'Value exceeds maximum length of 5000 characters',
},
},
};
mockServer.api.patch.mockRejectedValueOnce(error);
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'block-123',
value: 'X'.repeat(6000),
}),
).rejects.toThrow('Validation failed');
});
it('should handle network errors', async () => {
const error = new Error('Network error: Connection timeout');
mockServer.api.patch.mockRejectedValueOnce(error);
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'block-123',
value: 'Update value',
}),
).rejects.toThrow('Network error');
});
it('should handle server errors', async () => {
const error = new Error('Internal server error');
error.response = {
status: 500,
data: { error: 'Database update failed' },
};
mockServer.api.patch.mockRejectedValueOnce(error);
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: 'block-123',
metadata: { update: 'failed' },
}),
).rejects.toThrow('Internal server error');
});
});
describe('Edge Cases', () => {
it('should handle empty string block_id gracefully', async () => {
await expect(
handleUpdateMemoryBlock(mockServer, {
block_id: '',
value: 'Some value',
}),
).rejects.toThrow('Missing required argument: block_id');
});
it('should allow updating with null metadata values', async () => {
const metadataWithNulls = {
field1: null,
field2: 'value',
field3: null,
};
const updatedBlock = {
id: 'null-metadata-block',
value: 'Content',
metadata: metadataWithNulls,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'null-metadata-block',
metadata: metadataWithNulls,
});
const data = expectValidToolResponse(result);
expect(data.metadata.field1).toBeNull();
expect(data.metadata.field2).toBe('value');
expect(data.metadata.field3).toBeNull();
});
it('should handle UUID format block IDs in updates', async () => {
const uuidBlockId = '550e8400-e29b-41d4-a716-446655440000';
const updatedBlock = {
id: uuidBlockId,
value: 'Updated UUID block',
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: uuidBlockId,
value: 'Updated UUID block',
});
expect(mockServer.api.patch).toHaveBeenCalledWith(
`/blocks/${uuidBlockId}`,
expect.any(Object),
expect.any(Object),
);
const data = expectValidToolResponse(result);
expect(data.id).toBe(uuidBlockId);
});
it('should handle updating blocks at character limit', async () => {
const maxValue = 'Z'.repeat(5000); // Exactly at limit
const updatedBlock = {
id: 'max-limit-block',
value: maxValue,
limit: 5000,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'max-limit-block',
value: maxValue,
});
const data = expectValidToolResponse(result);
expect(data.value.length).toBe(5000);
});
it('should handle concurrent metadata updates correctly', async () => {
// Simulate a scenario where metadata might have version conflicts
const versionedMetadata = {
version: 5,
last_update_timestamp: Date.now(),
update_count: 10,
};
const updatedBlock = {
id: 'versioned-block',
value: 'Content',
metadata: versionedMetadata,
};
mockServer.api.patch.mockResolvedValueOnce({ data: updatedBlock });
const result = await handleUpdateMemoryBlock(mockServer, {
block_id: 'versioned-block',
metadata: versionedMetadata,
});
const data = expectValidToolResponse(result);
expect(data.metadata.version).toBe(5);
expect(data.metadata.update_count).toBe(10);
});
});
});