/** * ProjectFormatDetector Tests -- STRUCT-004 * * Uses Jasmine matchers (Electron test runner). * Integration tests using real filesystem are excluded here * (Electron renderer context — use createNodeDetector() in Node.js scripts). */ import { ProjectFormatDetector, DetectorFilesystem, V2_INDICATORS, LEGACY_INDICATORS } from '../../src/editor/src/io/ProjectFormatDetector'; // ---- Mock filesystem factory ------------------------------------------------ function makeMockFs(existingPaths: string[]): DetectorFilesystem { const set = new Set(existingPaths.map((p) => p.replace(/\\/g, '/'))); return { exists: (p: string) => set.has(p.replace(/\\/g, '/')), join: (...parts: string[]) => parts.join('/').replace(/\/+/g, '/') }; } // ---- detect() --------------------------------------------------------------- describe('ProjectFormatDetector.detect()', () => { it('returns v2/high when nodegx.project.json AND _registry.json exist', async () => { const mockFs = makeMockFs([ '/proj/nodegx.project.json', '/proj/components/_registry.json', '/proj/components' ]); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.format).toBe('v2'); expect(result.confidence).toBe('high'); }); it('returns v2/medium when only nodegx.project.json exists', async () => { const mockFs = makeMockFs(['/proj/nodegx.project.json']); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.format).toBe('v2'); expect(result.confidence).toBe('medium'); }); it('returns v2/medium when only _registry.json exists', async () => { const mockFs = makeMockFs(['/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.format).toBe('v2'); expect(result.confidence).toBe('medium'); }); it('returns legacy/high when only project.json exists', async () => { const mockFs = makeMockFs(['/proj/project.json']); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.format).toBe('legacy'); expect(result.confidence).toBe('high'); }); it('returns unknown/low when no project files exist', async () => { const mockFs = makeMockFs([]); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.format).toBe('unknown'); expect(result.confidence).toBe('low'); }); it('returns v2 when both project.json and nodegx.project.json exist (mixed)', async () => { const mockFs = makeMockFs([ '/proj/project.json', '/proj/nodegx.project.json', '/proj/components/_registry.json' ]); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.format).toBe('v2'); }); it('includes indicator for nodegx.project.json when found', async () => { const mockFs = makeMockFs(['/proj/nodegx.project.json', '/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.indicators.some((i) => i.includes('nodegx.project.json'))).toBe(true); }); it('includes indicator for project.json when found', async () => { const mockFs = makeMockFs(['/proj/project.json']); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.indicators.some((i) => i.includes('project.json'))).toBe(true); }); it('includes indicator for _registry.json when found', async () => { const mockFs = makeMockFs(['/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.indicators.some((i) => i.includes('_registry.json'))).toBe(true); }); it('includes "No project files found" indicator when nothing exists', async () => { const mockFs = makeMockFs([]); const detector = new ProjectFormatDetector(mockFs); const result = await detector.detect('/proj'); expect(result.indicators.some((i) => i.toLowerCase().includes('no project files'))).toBe(true); }); it('handles async filesystem.exists()', async () => { const asyncFs: DetectorFilesystem = { exists: async (p: string) => p.includes('nodegx.project.json') || p.includes('_registry.json'), join: (...parts: string[]) => parts.join('/') }; const detector = new ProjectFormatDetector(asyncFs); const result = await detector.detect('/proj'); expect(result.format).toBe('v2'); expect(result.confidence).toBe('high'); }); it('handles filesystem.exists() throwing (treats as not found)', async () => { const throwingFs: DetectorFilesystem = { exists: () => { throw new Error('permission denied'); }, join: (...parts: string[]) => parts.join('/') }; const detector = new ProjectFormatDetector(throwingFs); const result = await detector.detect('/proj'); expect(result.format).toBe('unknown'); }); }); // ---- detectSync() ----------------------------------------------------------- describe('ProjectFormatDetector.detectSync()', () => { it('returns v2/high for v2 project', () => { const mockFs = makeMockFs(['/proj/nodegx.project.json', '/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); const result = detector.detectSync('/proj'); expect(result.format).toBe('v2'); expect(result.confidence).toBe('high'); }); it('returns legacy/high for legacy project', () => { const mockFs = makeMockFs(['/proj/project.json']); const detector = new ProjectFormatDetector(mockFs); const result = detector.detectSync('/proj'); expect(result.format).toBe('legacy'); expect(result.confidence).toBe('high'); }); it('returns unknown for empty directory', () => { const mockFs = makeMockFs([]); const detector = new ProjectFormatDetector(mockFs); const result = detector.detectSync('/proj'); expect(result.format).toBe('unknown'); }); it('throws when filesystem.exists() returns a Promise', () => { const asyncFs: DetectorFilesystem = { exists: async () => false, join: (...parts: string[]) => parts.join('/') }; const detector = new ProjectFormatDetector(asyncFs); let threw = false; try { detector.detectSync('/proj'); } catch (e) { threw = true; expect((e as Error).message).toContain('synchronous'); } expect(threw).toBe(true); }); }); // ---- getFormat() / isV2() / isLegacy() -------------------------------------- describe('ProjectFormatDetector convenience methods', () => { it('getFormat() returns "v2" for v2 project', async () => { const mockFs = makeMockFs(['/proj/nodegx.project.json', '/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); expect(await detector.getFormat('/proj')).toBe('v2'); }); it('getFormat() returns "legacy" for legacy project', async () => { const mockFs = makeMockFs(['/proj/project.json']); const detector = new ProjectFormatDetector(mockFs); expect(await detector.getFormat('/proj')).toBe('legacy'); }); it('getFormat() returns "unknown" for empty dir', async () => { const mockFs = makeMockFs([]); const detector = new ProjectFormatDetector(mockFs); expect(await detector.getFormat('/proj')).toBe('unknown'); }); it('isV2() returns true for v2 project', async () => { const mockFs = makeMockFs(['/proj/nodegx.project.json', '/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); expect(await detector.isV2('/proj')).toBe(true); }); it('isV2() returns false for legacy project', async () => { const mockFs = makeMockFs(['/proj/project.json']); const detector = new ProjectFormatDetector(mockFs); expect(await detector.isV2('/proj')).toBe(false); }); it('isLegacy() returns true for legacy project', async () => { const mockFs = makeMockFs(['/proj/project.json']); const detector = new ProjectFormatDetector(mockFs); expect(await detector.isLegacy('/proj')).toBe(true); }); it('isLegacy() returns false for v2 project', async () => { const mockFs = makeMockFs(['/proj/nodegx.project.json', '/proj/components/_registry.json']); const detector = new ProjectFormatDetector(mockFs); expect(await detector.isLegacy('/proj')).toBe(false); }); }); // ---- V2_INDICATORS / LEGACY_INDICATORS constants ---------------------------- describe('Sentinel constants', () => { it('V2_INDICATORS.projectFile is nodegx.project.json', () => { expect(V2_INDICATORS.projectFile).toBe('nodegx.project.json'); }); it('V2_INDICATORS.registryFile is components/_registry.json', () => { expect(V2_INDICATORS.registryFile).toBe('components/_registry.json'); }); it('LEGACY_INDICATORS.projectFile is project.json', () => { expect(LEGACY_INDICATORS.projectFile).toBe('project.json'); }); });