Skip to main content
Glama
storage.test.ts11.5 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import { getNotesDirectory, saveNote, readNote, listNotes, getAllNoteFiles, saveNoteMetadata, loadNoteMetadata, } from '../storage.js'; import type { SessionNote } from '../../types/session.js'; describe('storage', () => { const testDir = path.join(os.tmpdir(), 'second-brain-test-' + Date.now()); const originalEnv = process.env.SECOND_BRAIN_NOTES_DIR; beforeEach(async () => { await fs.mkdir(testDir, { recursive: true }); delete process.env.SECOND_BRAIN_NOTES_DIR; }); afterEach(async () => { await fs.rm(testDir, { recursive: true, force: true }); if (originalEnv) { process.env.SECOND_BRAIN_NOTES_DIR = originalEnv; } else { delete process.env.SECOND_BRAIN_NOTES_DIR; } }); describe('getNotesDirectory', () => { it('should return config directory when provided', () => { const result = getNotesDirectory({ notesDirectory: '/custom/path' }); expect(result).toBe('/custom/path'); }); it('should expand tilde in config directory', () => { const result = getNotesDirectory({ notesDirectory: '~/notes' }); expect(result).toBe(path.join(os.homedir(), 'notes')); }); it('should use env var when config not provided', () => { process.env.SECOND_BRAIN_NOTES_DIR = '/env/path'; const result = getNotesDirectory(); expect(result).toBe('/env/path'); }); it('should expand tilde in env var', () => { process.env.SECOND_BRAIN_NOTES_DIR = '~/env-notes'; const result = getNotesDirectory(); expect(result).toBe(path.join(os.homedir(), 'env-notes')); }); it('should return default ~/notes when no config or env', () => { const result = getNotesDirectory(); expect(result).toBe(path.join(os.homedir(), 'notes')); }); it('should prioritize config over env var', () => { process.env.SECOND_BRAIN_NOTES_DIR = '/env/path'; const result = getNotesDirectory({ notesDirectory: '/config/path' }); expect(result).toBe('/config/path'); }); it('should handle lone tilde', () => { const result = getNotesDirectory({ notesDirectory: '~' }); expect(result).toBe(os.homedir()); }); }); describe('saveNote', () => { it('should save note to correct directory', async () => { const note: SessionNote = { summary: 'Test session', timestamp: '2025-01-15T10:30:00.000Z', projectName: 'test-project', }; const filePath = await saveNote(note, { notesDirectory: testDir }); expect(filePath).toContain('test-project'); expect(filePath).toMatch(/\.md$/); const exists = await fs.access(filePath).then(() => true).catch(() => false); expect(exists).toBe(true); }); it('should create project subdirectory', async () => { const note: SessionNote = { summary: 'Test session', timestamp: '2025-01-15T10:30:00.000Z', projectName: 'My Cool Project', }; const filePath = await saveNote(note, { notesDirectory: testDir }); const dirName = path.dirname(filePath); expect(dirName).toContain('my-cool-project'); }); it('should save note without project name', async () => { const note: SessionNote = { summary: 'Test session', timestamp: '2025-01-15T10:30:00.000Z', }; const filePath = await saveNote(note, { notesDirectory: testDir }); expect(filePath).toContain(testDir); expect(filePath).toMatch(/\.md$/); }); it('should format markdown correctly', async () => { const note: SessionNote = { summary: 'Test summary', timestamp: '2025-01-15T10:30:00.000Z', projectName: 'test-project', topic: 'Test Topic', tags: ['tag1', 'tag2'], }; const filePath = await saveNote(note, { notesDirectory: testDir }); const content = await fs.readFile(filePath, 'utf-8'); expect(content).toContain('# test-project'); expect(content).toContain('## Test Topic'); expect(content).toContain('Test summary'); expect(content).toContain('#tag1'); expect(content).toContain('#tag2'); }); it('should save metadata file', async () => { const note: SessionNote = { summary: 'Test session', timestamp: '2025-01-15T10:30:00.000Z', projectName: 'test-project', tags: ['test'], }; const filePath = await saveNote(note, { notesDirectory: testDir }); const metaPath = filePath.replace(/\.md$/, '.meta.json'); const metaExists = await fs.access(metaPath).then(() => true).catch(() => false); expect(metaExists).toBe(true); const metaContent = await fs.readFile(metaPath, 'utf-8'); const metadata = JSON.parse(metaContent); expect(metadata.summary).toBe('Test session'); expect(metadata.tags).toEqual(['test']); }); }); describe('readNote', () => { it('should read note content', async () => { const testContent = '# Test Note\n\nThis is a test.'; const testFile = path.join(testDir, 'test.md'); await fs.writeFile(testFile, testContent, 'utf-8'); const content = await readNote(testFile); expect(content).toBe(testContent); }); it('should throw error for non-existent file', async () => { await expect(readNote(path.join(testDir, 'nonexistent.md'))) .rejects.toThrow(); }); }); describe('listNotes', () => { beforeEach(async () => { const projectDir = path.join(testDir, 'test-project'); await fs.mkdir(projectDir, { recursive: true }); await fs.writeFile(path.join(projectDir, '2025-01-15_10-00-00_note1.md'), 'Note 1', 'utf-8'); await fs.writeFile(path.join(projectDir, '2025-01-16_10-00-00_note2.md'), 'Note 2', 'utf-8'); await fs.writeFile(path.join(projectDir, 'readme.txt'), 'Not a note', 'utf-8'); }); it('should list all markdown files for project', async () => { const notes = await listNotes('test-project', { notesDirectory: testDir }); expect(notes).toHaveLength(2); expect(notes[0]).toMatch(/note2\.md$/); expect(notes[1]).toMatch(/note1\.md$/); }); it('should return most recent first', async () => { const notes = await listNotes('test-project', { notesDirectory: testDir }); expect(notes[0]).toContain('2025-01-16'); expect(notes[1]).toContain('2025-01-15'); }); it('should return empty array for non-existent project', async () => { const notes = await listNotes('nonexistent', { notesDirectory: testDir }); expect(notes).toEqual([]); }); it('should list notes in root when no project specified', async () => { await fs.writeFile(path.join(testDir, 'root-note.md'), 'Root note', 'utf-8'); const notes = await listNotes(undefined, { notesDirectory: testDir }); expect(notes.some(n => n.includes('root-note.md'))).toBe(true); }); }); describe('getAllNoteFiles', () => { beforeEach(async () => { const project1Dir = path.join(testDir, 'project1'); const project2Dir = path.join(testDir, 'project2'); await fs.mkdir(project1Dir, { recursive: true }); await fs.mkdir(project2Dir, { recursive: true }); await fs.writeFile(path.join(project1Dir, 'note1.md'), 'Note 1', 'utf-8'); await fs.writeFile(path.join(project2Dir, 'note2.md'), 'Note 2', 'utf-8'); await fs.writeFile(path.join(testDir, 'root-note.md'), 'Root', 'utf-8'); }); it('should get all notes recursively with config object', async () => { const notes = await getAllNoteFiles({ notesDirectory: testDir }); expect(notes).toHaveLength(3); }); it('should get all notes recursively with string path', async () => { const notes = await getAllNoteFiles(testDir); expect(notes).toHaveLength(3); }); it('should sort notes by most recent first', async () => { const notes = await getAllNoteFiles(testDir); for (let i = 0; i < notes.length - 1; i++) { expect(notes[i] >= notes[i + 1]).toBe(true); } }); it('should return empty array for non-existent directory', async () => { const notes = await getAllNoteFiles('/nonexistent/path'); expect(notes).toEqual([]); }); it('should only include markdown files', async () => { await fs.writeFile(path.join(testDir, 'test.txt'), 'Not markdown', 'utf-8'); const notes = await getAllNoteFiles(testDir); expect(notes.every(n => n.endsWith('.md'))).toBe(true); }); }); describe('saveNoteMetadata', () => { it('should save metadata as JSON', async () => { const filePath = path.join(testDir, 'test.md'); const note: SessionNote = { summary: 'Test summary', timestamp: '2025-01-15T10:30:00.000Z', projectName: 'test-project', topic: 'Test Topic', tags: ['tag1', 'tag2'], analysis: { pattern: 'new-feature', patternConfidence: 0.9, complexity: 'moderate', fileCount: 45, keyFiles: [], }, }; await saveNoteMetadata(filePath, note); const metaPath = path.join(testDir, 'test.meta.json'); const exists = await fs.access(metaPath).then(() => true).catch(() => false); expect(exists).toBe(true); const content = await fs.readFile(metaPath, 'utf-8'); const metadata = JSON.parse(content); expect(metadata.summary).toBe('Test summary'); expect(metadata.projectName).toBe('test-project'); expect(metadata.topic).toBe('Test Topic'); expect(metadata.tags).toEqual(['tag1', 'tag2']); expect(metadata.analysis.pattern).toBe('new-feature'); }); it('should create valid JSON format', async () => { const filePath = path.join(testDir, 'test.md'); const note: SessionNote = { summary: 'Test', timestamp: '2025-01-15T10:30:00.000Z', }; await saveNoteMetadata(filePath, note); const metaPath = path.join(testDir, 'test.meta.json'); const content = await fs.readFile(metaPath, 'utf-8'); expect(() => JSON.parse(content)).not.toThrow(); }); }); describe('loadNoteMetadata', () => { it('should load metadata from JSON file', async () => { const filePath = path.join(testDir, 'test.md'); const metaPath = path.join(testDir, 'test.meta.json'); const metadata = { summary: 'Test summary', timestamp: '2025-01-15T10:30:00.000Z', projectName: 'test-project', tags: ['tag1'], }; await fs.writeFile(metaPath, JSON.stringify(metadata), 'utf-8'); const loaded = await loadNoteMetadata(filePath); expect(loaded).toEqual(metadata); }); it('should return null for non-existent metadata', async () => { const filePath = path.join(testDir, 'nonexistent.md'); const loaded = await loadNoteMetadata(filePath); expect(loaded).toBeNull(); }); it('should return null for invalid JSON', async () => { const filePath = path.join(testDir, 'test.md'); const metaPath = path.join(testDir, 'test.meta.json'); await fs.writeFile(metaPath, 'invalid json{', 'utf-8'); const loaded = await loadNoteMetadata(filePath); expect(loaded).toBeNull(); }); }); });

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/VoCoufi/second-brain-mcp'

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