Skip to main content
Glama
SkillService.test.ts6.84 kB
/** * SkillService Tests * * TESTING PATTERNS: * - Unit tests with temporary directories for file system operations * - Test each method independently * - Cover success cases, edge cases, and error handling * * CODING STANDARDS: * - Use descriptive test names (should...) * - Arrange-Act-Assert pattern * - Use temporary directories for file system tests * - Test behavior, not implementation */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { mkdir, writeFile, rm, appendFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { SkillService } from '../../src/services/SkillService'; describe('SkillService', () => { let tempDir: string; let skillsDir: string; beforeEach(async () => { // Create a unique temp directory for each test tempDir = join(tmpdir(), `skill-service-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); skillsDir = join(tempDir, 'skills'); await mkdir(skillsDir, { recursive: true }); }); afterEach(async () => { // Clean up temp directory await rm(tempDir, { recursive: true, force: true }); }); describe('cache behavior', () => { it('should cache skills after first load', async () => { // Arrange: Create a skill file const skillDir = join(skillsDir, 'test-skill'); await mkdir(skillDir, { recursive: true }); await writeFile( join(skillDir, 'SKILL.md'), `--- name: test-skill description: A test skill --- Test content` ); const service = new SkillService(tempDir, ['skills']); // Act: Load skills twice const firstLoad = await service.getSkills(); const secondLoad = await service.getSkills(); // Assert: Both should return same cached result expect(firstLoad).toHaveLength(1); expect(secondLoad).toBe(firstLoad); // Same reference (cached) }); it('should clear cache when clearCache is called', async () => { // Arrange: Create a skill file const skillDir = join(skillsDir, 'test-skill'); await mkdir(skillDir, { recursive: true }); await writeFile( join(skillDir, 'SKILL.md'), `--- name: test-skill description: A test skill --- Test content` ); const service = new SkillService(tempDir, ['skills']); // Act: Load skills, clear cache, load again const firstLoad = await service.getSkills(); service.clearCache(); const secondLoad = await service.getSkills(); // Assert: Different references after cache clear expect(firstLoad).toHaveLength(1); expect(secondLoad).toHaveLength(1); expect(secondLoad).not.toBe(firstLoad); // Different reference (reloaded) }); it('should call onCacheInvalidated callback when cache is cleared by file watcher', async () => { // Arrange const onCacheInvalidated = vi.fn(); const skillDir = join(skillsDir, 'test-skill'); await mkdir(skillDir, { recursive: true }); await writeFile( join(skillDir, 'SKILL.md'), `--- name: test-skill description: A test skill --- Test content` ); const service = new SkillService(tempDir, ['skills'], { onCacheInvalidated }); // Pre-populate the cache await service.getSkills(); // Act: Start watching and modify a SKILL.md file await service.startWatching(); // Give the watcher time to initialize await new Promise((resolve) => setTimeout(resolve, 100)); // Modify the skill file to trigger the watcher await appendFile(join(skillDir, 'SKILL.md'), '\nModified content'); // Wait for the file system event to be processed await new Promise((resolve) => setTimeout(resolve, 500)); // Stop watching service.stopWatching(); // Assert: Callback should have been called expect(onCacheInvalidated).toHaveBeenCalled(); }); it('should not call onCacheInvalidated for non-SKILL.md files', async () => { // Arrange const onCacheInvalidated = vi.fn(); const service = new SkillService(tempDir, ['skills'], { onCacheInvalidated }); // Act: Start watching and create a non-SKILL.md file await service.startWatching(); // Give the watcher time to initialize await new Promise((resolve) => setTimeout(resolve, 100)); // Create a non-skill file await writeFile(join(skillsDir, 'README.md'), 'Not a skill file'); // Wait for the file system event to be processed await new Promise((resolve) => setTimeout(resolve, 500)); // Stop watching service.stopWatching(); // Assert: Callback should not have been called expect(onCacheInvalidated).not.toHaveBeenCalled(); }); }); describe('startWatching and stopWatching', () => { it('should start and stop without errors', async () => { // Arrange const service = new SkillService(tempDir, ['skills']); // Act & Assert: Should not throw await expect(service.startWatching()).resolves.not.toThrow(); service.stopWatching(); }); it('should handle non-existent directories gracefully', async () => { // Arrange: Use a non-existent path const service = new SkillService(tempDir, ['non-existent-skills']); // Act & Assert: Should not throw await expect(service.startWatching()).resolves.not.toThrow(); service.stopWatching(); }); it('should stop existing watchers when startWatching is called again', async () => { // Arrange const service = new SkillService(tempDir, ['skills']); // Act: Start watching twice await service.startWatching(); await service.startWatching(); // Should stop first watcher // Assert: Should not throw and cleanup should work service.stopWatching(); }); }); describe('getSkill', () => { it('should return skill by name after cache is populated', async () => { // Arrange: Create a skill file const skillDir = join(skillsDir, 'my-skill'); await mkdir(skillDir, { recursive: true }); await writeFile( join(skillDir, 'SKILL.md'), `--- name: my-skill description: My test skill --- Skill content` ); const service = new SkillService(tempDir, ['skills']); // Act const skill = await service.getSkill('my-skill'); // Assert expect(skill).toBeDefined(); expect(skill?.name).toBe('my-skill'); expect(skill?.description).toBe('My test skill'); }); it('should return undefined for non-existent skill', async () => { // Arrange const service = new SkillService(tempDir, ['skills']); // Act const skill = await service.getSkill('non-existent'); // Assert expect(skill).toBeUndefined(); }); }); });

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/AgiFlow/aicode-toolkit'

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