Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
MemoryManager.triggers.test.tsโ€ข12.3 kB
/** * Unit tests for Memory trigger extraction (Issue #1124) * Verifies that MemoryManager properly extracts triggers from memory files * for Enhanced Index integration */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; import { MemoryManager } from '../../src/elements/memories/MemoryManager.js'; import { Memory } from '../../src/elements/memories/Memory.js'; import { PortfolioManager } from '../../src/portfolio/PortfolioManager.js'; import * as path from 'path'; import * as fs from 'fs/promises'; import * as os from 'os'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); describe('MemoryManager - Trigger Extraction', () => { let memoryManager: MemoryManager; let testDir: string; let memoriesDir: string; let originalPortfolioDir: string | undefined; beforeAll(async () => { // Create test directory testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-triggers-test-')); memoriesDir = path.join(testDir, 'memories'); await fs.mkdir(memoriesDir, { recursive: true }); // Save original portfolio dir originalPortfolioDir = process.env.DOLLHOUSE_PORTFOLIO_DIR; // Set test directory as portfolio dir process.env.DOLLHOUSE_PORTFOLIO_DIR = testDir; // Reset PortfolioManager singleton (PortfolioManager as any).instance = null; // Initialize manager memoryManager = new MemoryManager(); }); afterAll(async () => { // Restore original portfolio dir if (originalPortfolioDir) { process.env.DOLLHOUSE_PORTFOLIO_DIR = originalPortfolioDir; } else { delete process.env.DOLLHOUSE_PORTFOLIO_DIR; } // Reset PortfolioManager singleton (PortfolioManager as any).instance = null; // Clean up test directory await fs.rm(testDir, { recursive: true, force: true }); }); beforeEach(async () => { // Clear memories directory between tests const entries = await fs.readdir(memoriesDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(memoriesDir, entry.name); if (entry.isDirectory()) { await fs.rm(fullPath, { recursive: true, force: true }); } else { await fs.unlink(fullPath); } } }); describe('parseMemoryFile', () => { it('should extract triggers from memory metadata', async () => { // Copy fixture to memories directory const fixtureContent = await fs.readFile( path.join(__dirname, '../fixtures/memory-with-triggers.yaml'), 'utf-8' ); const testFile = path.join(memoriesDir, 'test-memory.yaml'); await fs.writeFile(testFile, fixtureContent); // Load the memory with triggers (use basename) const memory = await memoryManager.load('test-memory.yaml'); // Verify metadata was loaded correctly expect(memory.metadata.name).toBe('test-memory-triggers'); expect(memory.metadata.description).toBe('Test memory with triggers for Enhanced Index'); // Verify triggers were extracted expect(memory.metadata.triggers).toBeDefined(); expect(Array.isArray(memory.metadata.triggers)).toBe(true); expect(memory.metadata.triggers).toContain('remember'); expect(memory.metadata.triggers).toContain('recall'); expect(memory.metadata.triggers).toContain('retrieve'); expect(memory.metadata.triggers).toContain('context'); expect(memory.metadata.triggers).toContain('history'); expect(memory.metadata.triggers).toContain('recollect'); }); it('should handle memories without triggers field', async () => { // Create a memory without triggers const memoryWithoutTriggers = `metadata: name: "memory-no-triggers" description: "Memory without triggers field" version: "1.0.0" entries: - id: "entry-1" timestamp: "2025-09-26T16:00:00Z" content: "Test entry"`; const noTriggersFile = path.join(memoriesDir, 'no-triggers.yaml'); await fs.writeFile(noTriggersFile, memoryWithoutTriggers); const memory = await memoryManager.load('no-triggers.yaml'); // Should have empty triggers array expect(memory.metadata.triggers).toBeDefined(); expect(Array.isArray(memory.metadata.triggers)).toBe(true); expect(memory.metadata.triggers).toHaveLength(0); }); it('should sanitize trigger values', async () => { // Create a memory with potentially unsafe trigger values const memoryWithUnsafeTriggers = `metadata: name: "memory-unsafe-triggers" description: "Memory with unsafe triggers" triggers: ["<script>alert('xss')</script>", "valid-trigger", "another$#@!trigger"] version: "1.0.0" entries: []`; const unsafeFile = path.join(memoriesDir, 'unsafe-triggers.yaml'); await fs.writeFile(unsafeFile, memoryWithUnsafeTriggers); const memory = await memoryManager.load('unsafe-triggers.yaml'); // Triggers should be sanitized expect(memory.metadata.triggers).toBeDefined(); memory.metadata.triggers?.forEach(trigger => { expect(trigger).not.toContain('<script>'); expect(trigger).not.toContain('</script>'); }); }); }); describe('save', () => { it('should preserve triggers when saving memory', async () => { // Create a memory with triggers const memory = new Memory({ name: 'save-test-memory', description: 'Test saving with triggers', triggers: ['remember', 'recall', 'retrieve'] }); // Add a test entry await memory.addEntry('Test content for save', ['test']); // Save the memory const saveFile = 'saved-memory.yaml'; await memoryManager.save(memory, saveFile); // Load it back and verify triggers were preserved const loadedMemory = await memoryManager.load(saveFile); expect(loadedMemory.metadata.triggers).toBeDefined(); expect(loadedMemory.metadata.triggers).toContain('remember'); expect(loadedMemory.metadata.triggers).toContain('recall'); expect(loadedMemory.metadata.triggers).toContain('retrieve'); }); }); describe('Edge Cases', () => { it('should filter out triggers with invalid characters', async () => { const memoryWithInvalidTriggers = `metadata: name: "memory-invalid-chars" description: "Memory with invalid character triggers" triggers: ["valid-trigger", "invalid!@#", "another$test", "good_one", "bad space", "okay-dash"] version: "1.0.0" entries: []`; await fs.writeFile(path.join(memoriesDir, 'invalid-chars.yaml'), memoryWithInvalidTriggers); const memory = await memoryManager.load('invalid-chars.yaml'); // Should only include valid triggers (alphanumeric, hyphens, underscores) expect(memory.metadata.triggers).toBeDefined(); expect(memory.metadata.triggers).toContain('valid-trigger'); expect(memory.metadata.triggers).toContain('good_one'); expect(memory.metadata.triggers).toContain('okay-dash'); // Invalid triggers should be filtered out expect(memory.metadata.triggers).not.toContain('invalid!@#'); expect(memory.metadata.triggers).not.toContain('another$test'); expect(memory.metadata.triggers).not.toContain('bad space'); }); it('should handle extremely long trigger lists', async () => { // Create a memory with many triggers const manyTriggers = Array.from({ length: 100 }, (_, i) => `trigger-${i}`); const memoryWithManyTriggers = `metadata: name: "memory-many-triggers" description: "Memory with many triggers" triggers: ${JSON.stringify(manyTriggers)} version: "1.0.0" entries: []`; await fs.writeFile(path.join(memoriesDir, 'many-triggers.yaml'), memoryWithManyTriggers); const memory = await memoryManager.load('many-triggers.yaml'); // Should handle all triggers without error, but limit to 20 expect(memory.metadata.triggers).toBeDefined(); expect(Array.isArray(memory.metadata.triggers)).toBe(true); expect(memory.metadata.triggers?.length).toBe(20); // Limited to 20 triggers max }); it('should truncate triggers that exceed maximum length', async () => { const longTrigger = 'a'.repeat(100); // Much longer than MAX_TAG_LENGTH const memoryWithLongTrigger = `metadata: name: "memory-long-trigger" description: "Memory with overly long trigger" triggers: ["short", "${longTrigger}"] version: "1.0.0" entries: []`; await fs.writeFile(path.join(memoriesDir, 'long-trigger.yaml'), memoryWithLongTrigger); const memory = await memoryManager.load('long-trigger.yaml'); // Long trigger should be truncated to MAX_TAG_LENGTH expect(memory.metadata.triggers).toBeDefined(); expect(memory.metadata.triggers).toContain('short'); // The long trigger should be truncated const truncatedTrigger = memory.metadata.triggers?.find(t => t !== 'short'); expect(truncatedTrigger).toBeDefined(); expect(truncatedTrigger?.length).toBeLessThanOrEqual(50); // MAX_TAG_LENGTH default }); it('should handle mixed valid and invalid trigger types', async () => { const memoryWithMixedTypes = `metadata: name: "memory-mixed-types" description: "Memory with mixed trigger types" triggers: ["valid-string", 123, null, true, {"object": "value"}, ["array"], "another-valid"] version: "1.0.0" entries: []`; await fs.writeFile(path.join(memoriesDir, 'mixed-types.yaml'), memoryWithMixedTypes); // Should not throw error, but only include valid string triggers const memory = await memoryManager.load('mixed-types.yaml'); expect(memory.metadata.triggers).toBeDefined(); expect(memory.metadata.triggers).toContain('valid-string'); expect(memory.metadata.triggers).toContain('another-valid'); // Numbers get converted to strings "123", booleans to "true", null becomes "null", objects/arrays become "[object Object]" and "array" expect(memory.metadata.triggers).toContain('123'); expect(memory.metadata.triggers).toContain('true'); expect(memory.metadata.triggers).toContain('null'); expect(memory.metadata.triggers).toContain('array'); // ["array"] becomes "array" // Note: {"object": "value"} becomes "[object Object]" which contains spaces and gets rejected expect(memory.metadata.triggers?.length).toBe(6); // valid-string, 123, null, true, array, another-valid }); }); describe('Enhanced Index Integration', () => { it('should make triggers available for portfolio indexing', async () => { // Copy fixture to memories directory const fixtureContent = await fs.readFile( path.join(__dirname, '../fixtures/memory-with-triggers.yaml'), 'utf-8' ); await fs.writeFile(path.join(memoriesDir, 'test-memory.yaml'), fixtureContent); // Load memory with triggers const memory = await memoryManager.load('test-memory.yaml'); // The metadata.triggers field should be available for PortfolioIndexManager // This is what the Enhanced Index will use to build action_triggers expect(memory.metadata).toHaveProperty('triggers'); expect(memory.metadata.triggers).toEqual([ 'remember', 'recall', 'retrieve', 'context', 'history', 'recollect' ]); // Common memory-related verbs should be included const commonMemoryVerbs = ['remember', 'recall', 'retrieve']; commonMemoryVerbs.forEach(verb => { expect(memory.metadata.triggers).toContain(verb); }); }); it('should support recommended memory trigger verbs', async () => { // Test that common memory verbs work as triggers const recommendedVerbs = [ 'remember', 'recall', 'retrieve', 'recollect', 'find', 'search', 'lookup', 'history', 'context', 'past' ]; const memory = new Memory({ name: 'verb-test-memory', description: 'Test memory with recommended verbs', triggers: recommendedVerbs }); expect(memory.metadata.triggers).toBeDefined(); recommendedVerbs.forEach(verb => { expect(memory.metadata.triggers).toContain(verb); }); }); }); });

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/DollhouseMCP/DollhouseMCP'

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