/** * bridgeLauncher — NotebookLM 로컬 백엔드 원클릭 실행/종료의 순수 로직 검증. * (터미널 생성·프로세스 kill 같은 부수효과는 VS Code/OS 의존이라 여기선 순수 함수만.) */ import * as path from 'path'; import { candidateProjectDirs, buildPortKillCommand, candidateNlmPaths } from '../src/features/datacollect/bridgeLauncher'; jest.mock('vscode', () => ({ workspace: { getConfiguration: jest.fn(), workspaceFolders: [] }, window: {}, }), { virtual: true }); describe('candidateProjectDirs — 프로젝트 자동 탐색 후보 순서', () => { it('설정값이 있으면 항상 1순위다', () => { const dirs = candidateProjectDirs('/custom/Datacollector', ['/ws/ConnectAI']); expect(dirs[0]).toBe('/custom/Datacollector'); }); it('워크스페이스 자신 → 부모의 형제 폴더(이름 후보들) 순으로 나열한다', () => { const dirs = candidateProjectDirs('', ['/proj/ConnectAI']); expect(dirs[0]).toBe('/proj/ConnectAI'); expect(dirs).toContain(path.join('/proj', 'Datacollector_MAC')); expect(dirs).toContain(path.join('/proj', 'Datacollector')); expect(dirs).toContain(path.join('/proj', 'Datacollect')); }); it('중복 경로는 한 번만 나온다', () => { const dirs = candidateProjectDirs('/proj/Datacollector_MAC', ['/proj/ConnectAI', '/proj/ConnectAI']); expect(dirs.filter(d => d === path.join('/proj', 'Datacollector_MAC')).length).toBe(1); expect(dirs.filter(d => d === '/proj/ConnectAI').length).toBe(1); }); it('설정값 공백/빈 문자열은 무시한다', () => { expect(candidateProjectDirs(' ', [])).toEqual([]); expect(candidateProjectDirs('', [])).toEqual([]); }); }); describe('candidateNlmPaths — nlm 실행 파일 탐색 순서', () => { it('설정값이 있으면 항상 1순위다', () => { const p = candidateNlmPaths('win32', {}, ['C:/py/Scripts'], 'D:/custom/nlm.exe'); expect(p[0]).toBe('D:/custom/nlm.exe'); }); it('윈도우는 파이썬 Scripts 폴더의 nlm.exe 를 찾는다 (버전 하드코딩 없이)', () => { const p = candidateNlmPaths('win32', {}, ['C:/AppData/Python/Python314/Scripts']); expect(p).toContain(path.join('C:/AppData/Python/Python314/Scripts', 'nlm.exe')); }); it('맥/리눅스는 확장자 없는 nlm 과 ~/.local/bin 을 본다', () => { const p = candidateNlmPaths('darwin', { HOME: '/Users/me' }, ['/Users/me/Library/Python/3.11/bin']); expect(p).toContain(path.join('/Users/me/Library/Python/3.11/bin', 'nlm')); expect(p).toContain(path.join('/Users/me', '.local', 'bin', 'nlm')); expect(p.some(x => x.endsWith('nlm.exe'))).toBe(false); }); it('마지막 후보는 PATH 폴백용 맨 이름이다', () => { expect(candidateNlmPaths('win32', {}, []).at(-1)).toBe('nlm'); expect(candidateNlmPaths('linux', {}, []).at(-1)).toBe('nlm'); }); it('중복 경로는 한 번만 나온다', () => { const p = candidateNlmPaths('win32', {}, ['C:/s', 'C:/s']); expect(p.filter(x => x === path.join('C:/s', 'nlm.exe')).length).toBe(1); }); it('설정값 공백은 무시한다', () => { expect(candidateNlmPaths('win32', {}, [], ' ')[0]).not.toBe(' '); }); }); describe('buildPortKillCommand — OS 자동 감지 분기', () => { it('윈도우는 PowerShell Stop-Process 를 쓴다', () => { const cmd = buildPortKillCommand('win32', 3002); expect(cmd).toContain('powershell'); expect(cmd).toContain('-LocalPort 3002'); expect(cmd).toContain('Stop-Process'); }); it('맥/리눅스는 lsof + kill 을 쓴다', () => { for (const platform of ['darwin', 'linux'] as NodeJS.Platform[]) { const cmd = buildPortKillCommand(platform, 3002); expect(cmd).toContain('lsof -ti :3002'); expect(cmd).toContain('kill'); } }); it('포트 번호가 명령에 반영된다', () => { expect(buildPortKillCommand('win32', 4100)).toContain('4100'); expect(buildPortKillCommand('darwin', 4100)).toContain(':4100'); }); });