feat(fs): add directory listing seam

This commit is contained in:
Yichen Jiang
2026-07-03 14:36:20 +08:00
parent d8ea99756d
commit 803ed4bd95
15 changed files with 268 additions and 23 deletions

View File

@@ -7,7 +7,7 @@
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdtemp, readFile, rm, stat, symlink, writeFile, unlink } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile, unlink } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from 'cordis'
@@ -118,6 +118,50 @@ describe('readText / streamText', () => {
})
})
describe('listDir', () => {
it('lists files and directories in stable name order with resolved child targets', async () => {
await mkdir(join(dir, 'skills', 'dir-skill'), { recursive: true })
await writeFile(join(dir, 'skills', 'zeta.md'), 'zeta')
await writeFile(join(dir, 'skills', 'alpha.md'), 'alpha')
await symlink(join(dir, 'skills', 'missing-target'), join(dir, 'skills', 'broken-link'))
const entries = await fs.listDir(await fs.resolve('skills'))
expect(entries.map(entry => [entry.name, entry.type])).toEqual([
['alpha.md', 'file'],
['broken-link', 'other'],
['dir-skill', 'directory'],
['zeta.md', 'file'],
])
expect(entries.map(entry => entry.target.displayPath)).toEqual([
join(dir, 'skills', 'alpha.md'),
join(dir, 'skills', 'broken-link'),
join(dir, 'skills', 'dir-skill'),
join(dir, 'skills', 'zeta.md'),
])
const materializedEntries = entries.filter(entry => entry.version !== undefined)
expect(materializedEntries.map(entry => entry.target.targetKey))
.toEqual(await Promise.all(materializedEntries.map(entry => realpath(entry.target.displayPath))))
expect(entries.find(entry => entry.name === 'alpha.md')?.size).toBe(5)
expect(typeof entries.find(entry => entry.name === 'alpha.md')?.version).toBe('string')
expect(entries.find(entry => entry.name === 'broken-link')?.version).toBeUndefined()
expect(entries.find(entry => entry.name === 'dir-skill')?.size).toBeUndefined()
})
it('reports a missing directory as FS_NOT_FOUND', async () => {
await expect(fs.listDir(await fs.resolve('missing'))).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
})
it('reports a file target as FS_NOT_DIRECTORY', async () => {
await writeFile(join(dir, 'a.txt'), 'text')
await expect(fs.listDir(await fs.resolve('a.txt'))).rejects.toMatchObject({ code: 'FS_NOT_DIRECTORY' })
})
it('honors a pre-aborted signal', async () => {
await mkdir(join(dir, 'skills'), { recursive: true })
await expect(fs.listDir(await fs.resolve('skills'), AbortSignal.abort())).rejects.toMatchObject({ code: 'FS_ABORTED' })
})
})
describe('writeText', () => {
it('createIfAbsent creates a new file', async () => {
const target = await fs.resolve('new.txt')

View File

@@ -12,6 +12,7 @@ import { join } from 'node:path'
import { createServer } from 'node:net'
import {
applyLiteralEdit,
listDirectory,
probe,
readForEdit,
readWholeText,
@@ -145,6 +146,36 @@ describe('probe', () => {
})
})
describe('listDirectory', () => {
it('lists direct children in stable order without reading content', async () => {
const root = join(dir, 'skills')
await mkdir(join(root, 'dir-skill'), { recursive: true })
await writeFile(join(root, 'zeta.md'), 'zeta')
await writeFile(join(root, 'alpha.md'), 'alpha')
await symlink(join(root, 'missing-target'), join(root, 'broken-link'))
const entries = await listDirectory(localTarget(root))
expect(entries.map(entry => [entry.name, entry.type])).toEqual([
['alpha.md', 'file'],
['broken-link', 'other'],
['dir-skill', 'directory'],
['zeta.md', 'file'],
])
expect(entries.find(entry => entry.name === 'alpha.md')?.size).toBe(5)
expect(typeof entries.find(entry => entry.name === 'alpha.md')?.version).toBe('string')
expect(entries.find(entry => entry.name === 'broken-link')?.version).toBeUndefined()
expect(entries.find(entry => entry.name === 'dir-skill')?.size).toBeUndefined()
})
it('rejects missing, non-directory, and aborted listing requests', async () => {
await expect(listDirectory(localTarget(join(dir, 'missing')))).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
const file = join(dir, 'a.txt')
await writeFile(file, 'hi')
await expect(listDirectory(localTarget(file))).rejects.toMatchObject({ code: 'FS_NOT_DIRECTORY' })
await expect(listDirectory(localTarget(dir), AbortSignal.abort())).rejects.toMatchObject({ code: 'FS_ABORTED' })
})
})
describe('readWholeText', () => {
it('reads a small file', async () => {
const file = join(dir, 'a.txt')