Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
SkillManager.triggers.test.tsโ€ข10.1 kB
/** * Unit tests for Skill trigger extraction (Issue #1121) * Following pattern from MemoryManager.triggers.test.ts */ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { SkillManager } from '../../src/elements/skills/SkillManager.js'; import { Skill } from '../../src/elements/skills/Skill.js'; import { FileLockManager } from '../../src/security/fileLockManager.js'; import { PortfolioManager } from '../../src/portfolio/PortfolioManager.js'; import { ElementType } from '../../src/portfolio/types.js'; import { SecurityMonitor } from '../../src/security/securityMonitor.js'; import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; // Mock dependencies jest.mock('../../src/security/fileLockManager.js'); jest.mock('../../src/security/securityMonitor.js'); jest.mock('../../src/utils/logger.js'); describe('SkillManager - Trigger Extraction', () => { let skillManager: SkillManager; let tempDir: string; let skillsDir: string; let testSkillPath: string; let portfolioManager: PortfolioManager; beforeEach(async () => { // Create temp directory for tests tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-trigger-test-')); // Set up portfolio directory process.env.DOLLHOUSE_PORTFOLIO_DIR = tempDir; // Reset singleton (PortfolioManager as any).instance = undefined; (PortfolioManager as any).initializationPromise = null; portfolioManager = PortfolioManager.getInstance(); await portfolioManager.initialize(); // Get skills directory skillsDir = portfolioManager.getElementDir(ElementType.SKILL); // Ensure skills directory exists await fs.mkdir(skillsDir, { recursive: true }); // Create skill manager skillManager = new SkillManager(); // Set up mocks jest.clearAllMocks(); // Mock FileLockManager - make atomicWriteFile actually write the file (FileLockManager as any).atomicWriteFile = jest.fn(async (filePath: string, content: string) => { await fs.writeFile(filePath, content, 'utf-8'); }); (FileLockManager as any).atomicReadFile = jest.fn(async (filePath: string) => { return fs.readFile(filePath, 'utf-8'); }); // Mock SecurityMonitor (SecurityMonitor as any).logSecurityEvent = jest.fn(); testSkillPath = 'test-skill.md'; }); afterEach(async () => { jest.clearAllMocks(); // Clean up temp directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } delete process.env.DOLLHOUSE_PORTFOLIO_DIR; }); describe('Loading Skills with Triggers', () => { it('should extract triggers from skill frontmatter', async () => { const skillContent = `--- name: Code Review Skill description: Performs code review and analysis version: 1.0.0 triggers: - analyze - review - validate - audit --- # Instructions This skill performs comprehensive code reviews.`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); expect(skill.metadata.triggers).toBeDefined(); expect(skill.metadata.triggers).toEqual(['analyze', 'review', 'validate', 'audit']); }); it('should handle skills without triggers', async () => { const skillContent = `--- name: Basic Skill description: A simple skill without triggers version: 1.0.0 --- # Instructions Basic skill instructions.`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); expect(skill.metadata.triggers).toBeUndefined(); }); it('should filter out invalid trigger characters', async () => { const skillContent = `--- name: Filtered Skill description: Skill with invalid trigger characters version: 1.0.0 triggers: - valid-trigger - another_valid - "invalid trigger!" - "@special#chars" - valid123 - "has spaces" --- # Instructions`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); expect(skill.metadata.triggers).toBeDefined(); expect(skill.metadata.triggers).toEqual(['valid-trigger', 'another_valid', 'valid123']); }); it('should limit triggers to 20 maximum', async () => { const manyTriggers = Array.from({ length: 30 }, (_, i) => `trigger${i}`); const skillContent = `--- name: Many Triggers Skill description: Skill with many triggers version: 1.0.0 triggers: ${JSON.stringify(manyTriggers)} --- # Instructions`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); expect(skill.metadata.triggers).toBeDefined(); expect(skill.metadata.triggers?.length).toBe(20); expect(skill.metadata.triggers).toEqual(manyTriggers.slice(0, 20)); }); it('should sanitize and truncate long trigger names', async () => { const longTrigger = 'a'.repeat(100); const skillContent = `--- name: Long Trigger Skill description: Skill with long trigger names version: 1.0.0 triggers: - ${longTrigger} - normal --- # Instructions`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); expect(skill.metadata.triggers).toBeDefined(); expect(skill.metadata.triggers?.length).toBe(2); expect(skill.metadata.triggers![0].length).toBeLessThanOrEqual(50); expect(skill.metadata.triggers![1]).toBe('normal'); }); it('should handle non-array trigger values gracefully', async () => { const skillContent = `--- name: Invalid Triggers Skill description: Skill with invalid trigger format version: 1.0.0 triggers: "not an array" --- # Instructions`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); // Should not have triggers since it wasn't an array expect(skill.metadata.triggers).toBeUndefined(); }); it('should handle mixed valid and invalid trigger types', async () => { const skillContent = `--- name: Mixed Types Skill description: Skill with mixed trigger types version: 1.0.0 triggers: - valid-string - 123 - null - true - another-valid --- # Instructions`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); const skill = await skillManager.load(testSkillPath); expect(skill.metadata.triggers).toBeDefined(); // Numbers will be converted to strings, null and boolean filtered out expect(skill.metadata.triggers).toContain('valid-string'); expect(skill.metadata.triggers).toContain('123'); expect(skill.metadata.triggers).toContain('another-valid'); }); }); describe('Saving Skills with Triggers', () => { it('should preserve triggers when saving a skill', async () => { const skill = new Skill({ name: 'Save Test Skill', description: 'Testing trigger preservation', version: '1.0.0', triggers: ['debug', 'test', 'validate'] }, 'Test instructions'); await skillManager.save(skill, testSkillPath); // Read back and verify const savedContent = await fs.readFile(path.join(skillsDir, testSkillPath), 'utf-8'); expect(savedContent).toContain('triggers:'); expect(savedContent).toContain('- debug'); expect(savedContent).toContain('- test'); expect(savedContent).toContain('- validate'); // Load and verify const loadedSkill = await skillManager.load(testSkillPath); expect(loadedSkill.metadata.triggers).toEqual(['debug', 'test', 'validate']); }); it('should preserve empty triggers array when saving', async () => { const skill = new Skill({ name: 'Empty Triggers Skill', description: 'Skill with empty triggers', version: '1.0.0', triggers: [] }, 'Instructions'); await skillManager.save(skill, testSkillPath); // Read back and verify const savedContent = await fs.readFile(path.join(skillsDir, testSkillPath), 'utf-8'); expect(savedContent).toContain('triggers: []'); // Load and verify const loadedSkill = await skillManager.load(testSkillPath); expect(loadedSkill.metadata.triggers).toEqual([]); }); it('should handle skills without triggers when saving', async () => { const skill = new Skill({ name: 'No Triggers Skill', description: 'Skill without triggers field', version: '1.0.0' }, 'Instructions'); await skillManager.save(skill, testSkillPath); // Read back and verify const savedContent = await fs.readFile(path.join(skillsDir, testSkillPath), 'utf-8'); expect(savedContent).not.toContain('triggers:'); // Load and verify const loadedSkill = await skillManager.load(testSkillPath); expect(loadedSkill.metadata.triggers).toBeUndefined(); }); }); describe('Round-trip Consistency', () => { it('should maintain triggers through load-save-load cycle', async () => { const originalTriggers = ['analyze', 'optimize', 'refactor', 'debug']; const skillContent = `--- name: Round Trip Skill description: Testing round trip version: 1.0.0 triggers: ${JSON.stringify(originalTriggers)} --- Original instructions here.`; await fs.writeFile(path.join(skillsDir, testSkillPath), skillContent); // First load const skill1 = await skillManager.load(testSkillPath); expect(skill1.metadata.triggers).toEqual(originalTriggers); // Modify and save skill1.instructions = 'Modified instructions'; const newPath = 'round-trip-saved.md'; await skillManager.save(skill1, newPath); // Second load const skill2 = await skillManager.load(newPath); expect(skill2.metadata.triggers).toEqual(originalTriggers); expect(skill2.instructions).toContain('Modified instructions'); }); }); });

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