Files
helder/test/fs.test.ts
Jonathan van Rij 5e5fc53dde
Some checks failed
CI / check (push) Has been cancelled
improvements
2026-06-19 09:59:03 +02:00

153 lines
6.6 KiB
TypeScript

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, readDirChildren, 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<string> {
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'))
})
it('shows an empty folder (no files yet) that rg --files can not emit', async () => {
dir = await fixture()
await mkdir(join(dir, 'empty'), { recursive: true })
await mkdir(join(dir, 'src', 'nested', 'deep'), { recursive: true }) // file-empty nested chain
const tree = await readTree(dir)
const top = (tree.children || []).map((c) => c.name)
expect(top).toContain('empty')
const empty = (tree.children || []).find((c) => c.name === 'empty')!
expect(empty.type).toBe('dir')
// the nested empty chain shows under the (file-bearing) src folder
const src = (tree.children || []).find((c) => c.name === 'src')!
const nested = (src.children || []).find((c) => c.name === 'nested')!
expect(nested.type).toBe('dir')
expect((nested.children || []).map((c) => c.name)).toEqual(['deep'])
})
})
describe('readDirChildren', () => {
it('returns a single folder\'s children (root and subdir), ignoring node_modules', async () => {
dir = await fixture()
const top = (await readDirChildren(dir, '')).map((c) => c.name)
expect(top).toContain('src')
expect(top).toContain('README.md')
expect(top).not.toContain('node_modules')
const src = await readDirChildren(dir, 'src')
expect(src.map((c) => c.name)).toEqual(['a.ts'])
expect(src[0].path).toBe('src/a.ts')
})
it('shows an empty subfolder created since the initial tree read', async () => {
dir = await fixture()
await mkdir(join(dir, 'src', 'fresh'), { recursive: true })
const src = await readDirChildren(dir, 'src')
const fresh = src.find((c) => c.name === 'fresh')
expect(fresh?.type).toBe('dir')
expect(fresh?.path).toBe('src/fresh')
})
it('reflects files added/removed since the initial tree read', async () => {
dir = await fixture()
await writeFile(join(dir, 'src', 'b.ts'), 'export const b = 2\n')
await rm(join(dir, 'src', 'a.ts'))
expect((await readDirChildren(dir, 'src')).map((c) => c.name)).toEqual(['b.ts'])
})
})
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')
})
it('forces dirPaths in as empty folders, deduped against file-derived dirs', () => {
const t = buildTreeFromPaths('proj', ['src/a.ts'], ['empty', 'src/sub', 'src'])
const top = (t.children || []).map((c) => c.name)
expect(top).toEqual(['empty', 'src']) // dirs alphabetical, both present once
const src = (t.children || []).find((c) => c.name === 'src')!
expect((src.children || []).map((c) => c.name)).toEqual(['sub', 'a.ts'])
expect((src.children || []).find((c) => c.name === 'sub')!.type).toBe('dir')
})
})
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()
})
})