fs.test.ts
3,223 bytes
| 1 | import { mkdir, mkdtemp, rm } from 'node:fs/promises' |
|---|---|
| 2 | import { tmpdir } from 'node:os' |
| 3 | import path from 'node:path' |
| 4 | import type { FastifyInstance } from 'fastify' |
| 5 | import { afterEach, beforeEach, describe, expect, it } from 'vitest' |
| 6 | import type { FsBrowseResponse, FsMkdirResponse } from '../../shared/types' |
| 7 | import { buildApp } from '../app' |
| 8 | import { createLlmMock } from '../providers/llmMock' |
| 9 | import { SessionStore } from '../store/sessionStore' |
| 10 | |
| 11 | describe('fs routes', () => { |
| 12 | let storeDir: string |
| 13 | let baseDir: string |
| 14 | let app: FastifyInstance |
| 15 | |
| 16 | beforeEach(async () => { |
| 17 | storeDir = await mkdtemp(path.join(tmpdir(), 'voicetask-store-')) |
| 18 | baseDir = await mkdtemp(path.join(tmpdir(), 'voicetask-fs-')) |
| 19 | await mkdir(path.join(baseDir, 'zeta')) |
| 20 | await mkdir(path.join(baseDir, 'alpha')) |
| 21 | await mkdir(path.join(baseDir, '.hidden')) |
| 22 | app = buildApp({ store: new SessionStore(storeDir), llm: createLlmMock() }) |
| 23 | }) |
| 24 | |
| 25 | afterEach(async () => { |
| 26 | await app.close() |
| 27 | await rm(storeDir, { recursive: true, force: true }) |
| 28 | await rm(baseDir, { recursive: true, force: true }) |
| 29 | }) |
| 30 | |
| 31 | it('lists subdirectories sorted, hides dotfiles, and reports the parent', async () => { |
| 32 | const res = await app.inject({ method: 'GET', url: `/api/fs/browse?path=${encodeURIComponent(baseDir)}` }) |
| 33 | expect(res.statusCode).toBe(200) |
| 34 | const body = res.json<FsBrowseResponse>() |
| 35 | expect(body.path).toBe(baseDir) |
| 36 | expect(body.parent).toBe(path.dirname(baseDir)) |
| 37 | expect(body.directories.map((d) => d.name)).toEqual(['alpha', 'zeta']) |
| 38 | expect(body.directories[0].path).toBe(path.join(baseDir, 'alpha')) |
| 39 | }) |
| 40 | |
| 41 | it('defaults to the home directory when no path is given', async () => { |
| 42 | const res = await app.inject({ method: 'GET', url: '/api/fs/browse' }) |
| 43 | expect(res.statusCode).toBe(200) |
| 44 | expect(res.json<FsBrowseResponse>().path.length).toBeGreaterThan(0) |
| 45 | }) |
| 46 | |
| 47 | it('returns a 4xx for a path that cannot be read', async () => { |
| 48 | const res = await app.inject({ |
| 49 | method: 'GET', |
| 50 | url: `/api/fs/browse?path=${encodeURIComponent(path.join(baseDir, 'does-not-exist'))}`, |
| 51 | }) |
| 52 | expect(res.statusCode).toBeGreaterThanOrEqual(400) |
| 53 | expect(res.statusCode).toBeLessThan(500) |
| 54 | }) |
| 55 | |
| 56 | it('creates a new directory', async () => { |
| 57 | const res = await app.inject({ |
| 58 | method: 'POST', |
| 59 | url: '/api/fs/mkdir', |
| 60 | payload: { path: baseDir, name: 'my-project' }, |
| 61 | }) |
| 62 | expect(res.statusCode).toBe(200) |
| 63 | const body = res.json<FsMkdirResponse>() |
| 64 | expect(body.path).toBe(path.join(baseDir, 'my-project')) |
| 65 | |
| 66 | const listing = await app |
| 67 | .inject({ method: 'GET', url: `/api/fs/browse?path=${encodeURIComponent(baseDir)}` }) |
| 68 | .then((r) => r.json<FsBrowseResponse>()) |
| 69 | expect(listing.directories.map((d) => d.name)).toContain('my-project') |
| 70 | }) |
| 71 | |
| 72 | it('returns a 4xx when creating a directory that already exists', async () => { |
| 73 | await app.inject({ method: 'POST', url: '/api/fs/mkdir', payload: { path: baseDir, name: 'alpha' } }) |
| 74 | const res = await app.inject({ method: 'POST', url: '/api/fs/mkdir', payload: { path: baseDir, name: 'alpha' } }) |
| 75 | expect(res.statusCode).toBeGreaterThanOrEqual(400) |
| 76 | expect(res.statusCode).toBeLessThan(500) |
| 77 | }) |
| 78 | }) |
| 79 | |