import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { simpleGit } from 'simple-git' import { buildTreeFromPaths, createProjectFile, readAll, readProjectFile, readTree, writeProjectFile } from '../src/main/fs-service' let dir = '' afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }) }) async function fixture(): Promise { const d = await mkdtemp(join(tmpdir(), 'helder-fs-')) await mkdir(join(d, 'src'), { recursive: true }) await mkdir(join(d, 'node_modules/pkg'), { recursive: true }) await mkdir(join(d, '.git'), { recursive: true }) await writeFile(join(d, 'src', 'a.ts'), 'export const a = 1\n') await writeFile(join(d, 'README.md'), '# hi\n') await writeFile(join(d, 'node_modules', 'pkg', 'index.js'), 'module.exports = 1\n') await writeFile(join(d, '.git', 'HEAD'), 'ref: refs/heads/main\n') await writeFile(join(d, 'logo.bin'), Buffer.from([0x00, 0x01, 0x02, 0x00, 0x99])) return d } describe('readTree', () => { it('lists dirs before files, ignoring node_modules and .git', async () => { dir = await fixture() const tree = await readTree(dir) const top = (tree.children || []).map((c) => c.name) expect(top).not.toContain('node_modules') expect(top).not.toContain('.git') expect(top).toContain('src') // dirs first expect(top.indexOf('src')).toBeLessThan(top.indexOf('README.md')) expect(top.indexOf('src')).toBeLessThan(top.indexOf('logo.bin')) }) }) describe('readAll', () => { it('indexes text files, skipping ignored dirs and binary files', async () => { dir = await fixture() const files = await readAll(dir) const keys = Object.keys(files) expect(keys).toContain('src/a.ts') expect(keys).toContain('README.md') expect(keys.some((k) => k.includes('node_modules'))).toBe(false) expect(keys).not.toContain('logo.bin') // binary skipped expect(files['src/a.ts']).toContain('export const a') }) it('shows .gitignored files in a repo (files.followGitignore default off)', async () => { dir = await mkdtemp(join(tmpdir(), 'helder-fs-')) await simpleGit(dir).init() await writeFile(join(dir, '.gitignore'), 'secret.txt\n') await writeFile(join(dir, 'secret.txt'), 'shh') await writeFile(join(dir, 'keep.txt'), 'ok') const keys = Object.keys(await readAll(dir)) expect(keys).toContain('keep.txt') expect(keys).toContain('secret.txt') // gitignore not followed by default → shown }) }) describe('buildTreeFromPaths', () => { it('nests paths with dirs before files, alphabetical', () => { const t = buildTreeFromPaths('proj', ['src/b.ts', 'src/a.ts', 'README.md', 'src/util/x.ts']) expect((t.children || []).map((c) => c.name)).toEqual(['src', 'README.md']) const src = (t.children || []).find((c) => c.name === 'src')! expect((src.children || []).map((c) => c.name)).toEqual(['util', 'a.ts', 'b.ts']) expect((src.children || []).find((c) => c.name === 'util')!.path).toBe('src/util') }) }) describe('read/write round-trip', () => { it('writes then reads the same content', async () => { dir = await mkdtemp(join(tmpdir(), 'helder-fs-')) await writeProjectFile(dir, 'note.txt', 'hello world\n') expect(await readProjectFile(dir, 'note.txt')).toBe('hello world\n') }) }) describe('createProjectFile', () => { it('creates an empty file and makes missing parent folders', async () => { dir = await mkdtemp(join(tmpdir(), 'helder-fs-')) await createProjectFile(dir, 'src/new/fresh.ts') expect(await readProjectFile(dir, 'src/new/fresh.ts')).toBe('') }) it('refuses to overwrite an existing file', async () => { dir = await mkdtemp(join(tmpdir(), 'helder-fs-')) await writeProjectFile(dir, 'keep.txt', 'precious\n') await expect(createProjectFile(dir, 'keep.txt')).rejects.toThrow() expect(await readProjectFile(dir, 'keep.txt')).toBe('precious\n') }) it('refuses to escape the project root', async () => { dir = await mkdtemp(join(tmpdir(), 'helder-fs-')) await expect(createProjectFile(dir, '../escape.txt')).rejects.toThrow() }) })