claude-flow
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
405 lines (327 loc) • 13.7 kB
JavaScript
/**
* Tests for memory command
*/
import { jest } from '@jest/globals';
import { memoryCommand } from '../memory.js';
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
jest.mock('fs-extra');
jest.mock('chalk', () => ({
default: {
blue: jest.fn(str => str),
green: jest.fn(str => str),
yellow: jest.fn(str => str),
red: jest.fn(str => str),
cyan: jest.fn(str => str),
magenta: jest.fn(str => str),
dim: jest.fn(str => str),
bold: jest.fn(str => str),
}
}));
describe('Memory Command', () => {
let consoleLogSpy;
let consoleErrorSpy;
const memoryPath = path.join(process.cwd(), '.claude', 'memory', 'memory.json');
beforeEach(() => {
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
jest.clearAllMocks();
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('store subcommand', () => {
test('should store memory entry', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['store', 'test-key', 'test-value'], {});
expect(fs.writeJson).toHaveBeenCalled();
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries).toHaveLength(1);
expect(writeCall[1].entries[0].key).toBe('test-key');
expect(writeCall[1].entries[0].value).toBe('test-value');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Memory stored successfully')
);
});
test('should store memory with tags', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['store', 'key', 'value'], { tags: 'important,api' });
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries[0].tags).toEqual(['important', 'api']);
});
test('should store memory with TTL', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['store', 'key', 'value'], { ttl: 3600 });
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries[0].expiresAt).toBeDefined();
});
test('should handle JSON values', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
fs.writeJson.mockResolvedValue(undefined);
const jsonValue = '{"name":"test","count":42}';
await memoryCommand(['store', 'json-key', jsonValue], {});
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries[0].value).toEqual({ name: 'test', count: 42 });
});
test('should create memory file if not exists', async () => {
fs.pathExists.mockResolvedValue(false);
fs.ensureDir.mockResolvedValue(undefined);
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['store', 'key', 'value'], {});
expect(fs.ensureDir).toHaveBeenCalledWith(path.dirname(memoryPath));
expect(fs.writeJson).toHaveBeenCalled();
});
});
describe('retrieve subcommand', () => {
test('should retrieve memory by key', async () => {
const mockMemory = {
entries: [
{ key: 'test-key', value: 'test-value', timestamp: new Date().toISOString() }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
await memoryCommand(['retrieve', 'test-key'], {});
const output = consoleLogSpy.mock.calls.flat().join('\n');
expect(output).toContain('test-value');
});
test('should show not found message', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
await memoryCommand(['retrieve', 'nonexistent'], {});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('No memory found for key: nonexistent')
);
});
test('should handle expired entries', async () => {
const pastDate = new Date(Date.now() - 1000).toISOString();
const mockMemory = {
entries: [
{ key: 'expired', value: 'value', expiresAt: pastDate }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
await memoryCommand(['retrieve', 'expired'], {});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('No memory found for key: expired')
);
});
});
describe('list subcommand', () => {
test('should list all memories', async () => {
const mockMemory = {
entries: [
{ key: 'key1', value: 'value1', timestamp: new Date().toISOString() },
{ key: 'key2', value: { complex: 'object' }, timestamp: new Date().toISOString() },
{ key: 'key3', value: 'value3', tags: ['important'], timestamp: new Date().toISOString() }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
await memoryCommand(['list'], {});
const output = consoleLogSpy.mock.calls.flat().join('\n');
expect(output).toContain('Memory Entries (3)');
expect(output).toContain('key1');
expect(output).toContain('key2');
expect(output).toContain('key3');
expect(output).toContain('[important]');
});
test('should filter by pattern', async () => {
const mockMemory = {
entries: [
{ key: 'api/user', value: 'data1', timestamp: new Date().toISOString() },
{ key: 'api/product', value: 'data2', timestamp: new Date().toISOString() },
{ key: 'config/settings', value: 'data3', timestamp: new Date().toISOString() }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
await memoryCommand(['list'], { pattern: 'api/*' });
const output = consoleLogSpy.mock.calls.flat().join('\n');
expect(output).toContain('api/user');
expect(output).toContain('api/product');
expect(output).not.toContain('config/settings');
});
test('should filter by tags', async () => {
const mockMemory = {
entries: [
{ key: 'key1', value: 'v1', tags: ['important'], timestamp: new Date().toISOString() },
{ key: 'key2', value: 'v2', tags: ['temporary'], timestamp: new Date().toISOString() },
{ key: 'key3', value: 'v3', tags: ['important', 'api'], timestamp: new Date().toISOString() }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
await memoryCommand(['list'], { tags: 'important' });
const output = consoleLogSpy.mock.calls.flat().join('\n');
expect(output).toContain('key1');
expect(output).toContain('key3');
expect(output).not.toContain('key2');
});
test('should show empty message', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
await memoryCommand(['list'], {});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('No memories stored')
);
});
});
describe('delete subcommand', () => {
test('should delete memory by key', async () => {
const mockMemory = {
entries: [
{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['delete', 'key1'], {});
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries).toHaveLength(1);
expect(writeCall[1].entries[0].key).toBe('key2');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Memory deleted: key1')
);
});
test('should handle non-existent key', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue({ entries: [] });
await memoryCommand(['delete', 'nonexistent'], {});
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Memory not found: nonexistent')
);
});
});
describe('clear subcommand', () => {
test('should clear all memories with force flag', async () => {
fs.pathExists.mockResolvedValue(true);
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['clear'], { force: true });
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries).toEqual([]);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('All memories cleared')
);
});
test('should warn without force flag', async () => {
await memoryCommand(['clear'], {});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Use --force to confirm')
);
});
});
describe('export subcommand', () => {
test('should export memories to file', async () => {
const mockMemory = {
entries: [
{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['export', 'backup.json'], {});
expect(fs.writeJson).toHaveBeenCalledWith(
'backup.json',
mockMemory,
{ spaces: 2 }
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Memories exported to backup.json')
);
});
});
describe('import subcommand', () => {
test('should import memories from file', async () => {
const importData = {
entries: [
{ key: 'imported1', value: 'value1' },
{ key: 'imported2', value: 'value2' }
]
};
fs.pathExists.mockResolvedValueOnce(true) // import file exists
.mockResolvedValueOnce(true); // memory file exists
fs.readJson.mockResolvedValueOnce(importData) // import data
.mockResolvedValueOnce({ entries: [] }); // existing memory
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['import', 'import.json'], {});
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries).toHaveLength(2);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Imported 2 memory entries')
);
});
test('should merge with existing memories', async () => {
const importData = { entries: [{ key: 'new', value: 'imported' }] };
const existingData = { entries: [{ key: 'existing', value: 'original' }] };
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValueOnce(importData)
.mockResolvedValueOnce(existingData);
fs.writeJson.mockResolvedValue(undefined);
await memoryCommand(['import', 'import.json'], { merge: true });
const writeCall = fs.writeJson.mock.calls[0];
expect(writeCall[1].entries).toHaveLength(2);
});
});
describe('stats subcommand', () => {
test('should show memory statistics', async () => {
const mockMemory = {
entries: [
{ key: 'key1', value: 'short', tags: ['tag1'] },
{ key: 'key2', value: { complex: 'object' }, tags: ['tag1', 'tag2'] },
{ key: 'key3', value: 'medium length value', tags: ['tag2'] },
{ key: 'expired', value: 'value', expiresAt: new Date(Date.now() - 1000).toISOString() }
]
};
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockResolvedValue(mockMemory);
await memoryCommand(['stats'], {});
const output = consoleLogSpy.mock.calls.flat().join('\n');
expect(output).toContain('Memory Statistics');
expect(output).toContain('Total entries: 4');
expect(output).toContain('Active entries: 3');
expect(output).toContain('Expired entries: 1');
expect(output).toContain('Unique tags: 2');
});
});
describe('help subcommand', () => {
test('should show help when no arguments', async () => {
await memoryCommand([], {});
const output = consoleLogSpy.mock.calls.flat().join('\n');
expect(output).toContain('Memory Management');
expect(output).toContain('USAGE:');
expect(output).toContain('memory <subcommand>');
});
});
describe('error handling', () => {
test('should handle file system errors', async () => {
fs.pathExists.mockRejectedValue(new Error('Permission denied'));
await memoryCommand(['list'], {});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Error:')
);
});
test('should handle invalid JSON in import', async () => {
fs.pathExists.mockResolvedValue(true);
fs.readJson.mockRejectedValue(new Error('Invalid JSON'));
await memoryCommand(['import', 'bad.json'], {});
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Error:')
);
});
});
});