Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
PortfolioSyncComparer.test.ts10 kB
/** * Unit tests for PortfolioSyncComparer * Tests sync comparison logic for portfolio synchronization */ import { jest } from '@jest/globals'; import { PortfolioSyncComparer } from '../../../../src/sync/PortfolioSyncComparer.js'; import { PortfolioElementData } from '../../../../src/sync/types.js'; import { ElementType } from '../../../../src/portfolio/types.js'; describe('PortfolioSyncComparer', () => { let comparer: PortfolioSyncComparer; beforeEach(() => { comparer = new PortfolioSyncComparer(); }); describe('compareElements', () => { const createLocalElement = (name: string, sha?: string): PortfolioElementData => ({ name, type: ElementType.PERSONA, path: `personas/${name}.md`, sha, lastModified: new Date('2025-09-01').toISOString() }); const createRemoteElement = (name: string, sha?: string): PortfolioElementData => ({ name, type: ElementType.PERSONA, path: `personas/${name}.md`, sha: sha || `sha-${name}`, lastModified: new Date('2025-09-10').toISOString() }); describe('additive mode', () => { it('should only add missing elements', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('existing', 'sha-existing') ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ createRemoteElement('existing', 'sha-existing'), createRemoteElement('new', 'sha-new') ]); const result = comparer.compareElements(remoteElements, localElements, 'additive'); expect(result.toAdd).toHaveLength(1); expect(result.toAdd[0].name).toBe('new'); expect(result.toUpdate).toHaveLength(0); expect(result.toDelete).toHaveLength(0); expect(result.toSkip).toHaveLength(1); }); it('should not update existing elements even if different', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('existing', 'sha-old') ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ createRemoteElement('existing', 'sha-new') ]); const result = comparer.compareElements(remoteElements, localElements, 'additive'); expect(result.toAdd).toHaveLength(0); expect(result.toUpdate).toHaveLength(0); expect(result.toDelete).toHaveLength(0); expect(result.toSkip).toHaveLength(1); }); }); describe('mirror mode', () => { it('should add, update, and delete to match remote exactly', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('keep', 'sha-keep'), createLocalElement('update', 'sha-old'), createLocalElement('delete', 'sha-delete') ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ createRemoteElement('keep', 'sha-keep'), createRemoteElement('update', 'sha-new'), createRemoteElement('add', 'sha-add') ]); const result = comparer.compareElements(remoteElements, localElements, 'mirror'); expect(result.toAdd).toHaveLength(1); expect(result.toAdd[0].name).toBe('add'); expect(result.toUpdate).toHaveLength(1); expect(result.toUpdate[0].name).toBe('update'); expect(result.toDelete).toHaveLength(1); expect(result.toDelete[0].name).toBe('delete'); expect(result.toSkip).toHaveLength(1); expect(result.toSkip[0].name).toBe('keep'); }); it('should handle empty remote by deleting all local', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('delete1'), createLocalElement('delete2') ]); const remoteElements = new Map<ElementType, any[]>(); // Empty remote - no elements const result = comparer.compareElements(remoteElements, localElements, 'mirror'); expect(result.toAdd).toHaveLength(0); expect(result.toUpdate).toHaveLength(0); expect(result.toDelete).toHaveLength(2); expect(result.toSkip).toHaveLength(0); }); }); describe('backup mode', () => { it('should overwrite all local with remote', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('existing', 'sha-old'), createLocalElement('local-only', 'sha-local') ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ createRemoteElement('existing', 'sha-new'), createRemoteElement('new', 'sha-new') ]); const result = comparer.compareElements(remoteElements, localElements, 'backup'); expect(result.toAdd).toHaveLength(1); expect(result.toAdd[0].name).toBe('new'); expect(result.toUpdate).toHaveLength(1); expect(result.toUpdate[0].name).toBe('existing'); expect(result.toDelete).toHaveLength(0); // Backup mode doesn't delete local-only files expect(result.toSkip).toHaveLength(0); }); it('should update even if SHA matches (forced backup)', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('existing', 'sha-same') ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ createRemoteElement('existing', 'sha-same') ]); const result = comparer.compareElements(remoteElements, localElements, 'backup'); expect(result.toAdd).toHaveLength(0); expect(result.toUpdate).toHaveLength(1); expect(result.toUpdate[0].name).toBe('existing'); expect(result.toDelete).toHaveLength(0); expect(result.toSkip).toHaveLength(0); }); }); describe('edge cases', () => { it('should handle empty local and remote', () => { const localElements = new Map<ElementType, any[]>(); const remoteElements = new Map<ElementType, any[]>(); const result = comparer.compareElements(remoteElements, localElements, 'mirror'); expect(result.toAdd).toHaveLength(0); expect(result.toUpdate).toHaveLength(0); expect(result.toDelete).toHaveLength(0); expect(result.toSkip).toHaveLength(0); }); it('should handle elements without SHA', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('no-sha', undefined) ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ createRemoteElement('no-sha', undefined) ]); const result = comparer.compareElements(remoteElements, localElements, 'mirror'); // Without SHA, should compare by modified date expect(result.toUpdate).toHaveLength(1); expect(result.toUpdate[0].name).toBe('no-sha'); }); it('should handle different element types correctly', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.SKILL, [ { ...createLocalElement('test'), type: ElementType.SKILL } ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.SKILL, [ { ...createRemoteElement('test'), type: ElementType.SKILL } ]); const result = comparer.compareElements(remoteElements, localElements, 'additive'); expect(result.toSkip).toHaveLength(1); expect(result.toSkip[0].type).toBe(ElementType.SKILL); }); it('should normalize names for comparison', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, [ createLocalElement('Test-Element', 'sha-old') ]); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, [ { ...createRemoteElement('test-element', 'sha-new'), name: 'test-element' } ]); const result = comparer.compareElements(remoteElements, localElements, 'mirror'); // Should recognize as same element and update expect(result.toUpdate).toHaveLength(1); expect(result.toAdd).toHaveLength(0); expect(result.toDelete).toHaveLength(0); }); }); describe('performance', () => { it('should handle large element lists efficiently', () => { const localElements = new Map<ElementType, any[]>(); localElements.set(ElementType.PERSONA, Array.from({ length: 1000 }, (_, i) => createLocalElement(`element-${i}`, `sha-${i}`) ) ); const remoteElements = new Map<ElementType, any[]>(); remoteElements.set(ElementType.PERSONA, Array.from({ length: 1000 }, (_, i) => createRemoteElement(`element-${i}`, i % 2 === 0 ? `sha-${i}` : `sha-new-${i}`) ) ); const startTime = Date.now(); const result = comparer.compareElements(remoteElements, localElements, 'mirror'); const endTime = Date.now(); expect(endTime - startTime).toBeLessThan(100); // Should complete in under 100ms expect(result.toUpdate).toHaveLength(500); // Half should need updates expect(result.toSkip).toHaveLength(500); // Half should be skipped }); }); }); });

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