/** * Unit tests for FileSystemProjectScaffolder. * * Drives against a real temp directory so end-to-end file IO + path-traversal * defenses are exercised. */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { FileSystemProjectScaffolder, validateProjectName, } from '../src/scaffolder/projectScaffolder'; function tmp(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'astra-scaffold-test-')); } describe('validateProjectName', () => { test('accepts allowed names', () => { expect(validateProjectName('foo')).toBe('foo'); expect(validateProjectName('foo-bar_v2')).toBe('foo-bar_v2'); expect(validateProjectName(' trimmed ')).toBe('trimmed'); }); test('rejects too short', () => { expect(validateProjectName('a')).toBeNull(); }); test('rejects too long', () => { expect(validateProjectName('a'.repeat(41))).toBeNull(); }); test('rejects invalid chars', () => { expect(validateProjectName('foo bar')).toBeNull(); expect(validateProjectName('foo/bar')).toBeNull(); expect(validateProjectName('foo.bar')).toBeNull(); expect(validateProjectName('한글이름')).toBeNull(); }); test('rejects empty / non-string', () => { expect(validateProjectName('')).toBeNull(); expect(validateProjectName(undefined as any)).toBeNull(); }); }); describe('FileSystemProjectScaffolder', () => { let root: string; let scaffolder: FileSystemProjectScaffolder; beforeEach(() => { root = tmp(); scaffolder = new FileSystemProjectScaffolder(); }); afterEach(() => { try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ } }); test('listTemplates returns the catalog', () => { const list = scaffolder.listTemplates(); const ids = list.map(t => t.id); expect(ids).toContain('static'); expect(ids).toContain('vite-vanilla'); expect(ids).toContain('vite-react'); }); test('static template writes index.html + README.md', async () => { const result = await scaffolder.scaffold({ name: 'demo-static', template: 'static', rootDir: root }); expect(result.ok).toBe(true); if (!result.ok) return; expect(fs.existsSync(path.join(result.projectPath, 'site', 'index.html'))).toBe(true); expect(fs.existsSync(path.join(result.projectPath, 'README.md'))).toBe(true); const html = fs.readFileSync(path.join(result.projectPath, 'site', 'index.html'), 'utf8'); expect(html).toContain('demo-static'); }); test('vite-vanilla template writes package.json + main.js', async () => { const result = await scaffolder.scaffold({ name: 'vv', template: 'vite-vanilla', rootDir: root }); expect(result.ok).toBe(true); if (!result.ok) return; const pkg = JSON.parse(fs.readFileSync(path.join(result.projectPath, 'site', 'package.json'), 'utf8')); expect(pkg.name).toBe('vv'); expect(pkg.devDependencies.vite).toBeDefined(); expect(fs.existsSync(path.join(result.projectPath, 'site', 'main.js'))).toBe(true); }); test('vite-react template includes tsconfig + main.tsx', async () => { const result = await scaffolder.scaffold({ name: 'vr', template: 'vite-react', rootDir: root }); expect(result.ok).toBe(true); if (!result.ok) return; expect(fs.existsSync(path.join(result.projectPath, 'site', 'tsconfig.json'))).toBe(true); expect(fs.existsSync(path.join(result.projectPath, 'site', 'src', 'main.tsx'))).toBe(true); const tsx = fs.readFileSync(path.join(result.projectPath, 'site', 'src', 'main.tsx'), 'utf8'); expect(tsx).toContain('