/** * Cordis-free tests for the raw local-filesystem I/O: path resolution, probe, * whole-file/streamed text reads, binary/UTF-8 rejection, atomic-write temp * safety, literal edit matching, and line-ending handling. Line WINDOWING is * policy and lives in `dsh-fs-policy`, so it is not tested here. */ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { chmod, mkdtemp, readFile, rename, rm, stat, symlink, unlink, writeFile, mkdir, readdir, realpath } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createServer } from 'node:net' import { applyLiteralEdit, listDirectory, probe, probeNoFollow, readForEdit, readWholeText, resolveLocalTarget, restoreLineEndings, streamWholeText, writeFileAtomic, } from '../src/fsio.ts' import type { LocalTarget } from '../src/fsio.ts' import { copyFileDaclWin32, readFileDaclWin32 } from '../src/win32.ts' import { FsError, FsTargetKey } from '@deepseek-ai/dsh-fs' let dir: string beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'dsh-fsio-')) }) afterEach(async () => { await rm(dir, { recursive: true, force: true }) }) const localTarget = (path: string): LocalTarget => ({ displayPath: path, targetKey: FsTargetKey(path) }) async function collect(chunks: AsyncIterable): Promise { let out = '' for await (const chunk of chunks) out += chunk return out } describe('resolveLocalTarget', () => { it('resolves a relative path from cwd and realpaths it', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'hi') const target = await resolveLocalTarget(dir, 'a.txt') expect(target.displayPath).toBe(file) expect(target.targetKey).toBe(await realpath(file)) }) it('uses the realpathed parent + basename when the file does not exist (stable across create)', async () => { const target = await resolveLocalTarget(dir, 'missing.txt') expect(target.targetKey).toBe(join(await realpath(dir), 'missing.txt')) }) it('two paths to the same file via a symlink share one targetKey', async () => { const real = join(dir, 'real.txt') await writeFile(real, 'hi') const link = join(dir, 'link.txt') await symlink(real, link) const viaReal = await resolveLocalTarget(dir, 'real.txt') const viaLink = await resolveLocalTarget(dir, 'link.txt') expect(viaLink.targetKey).toBe(viaReal.targetKey) expect(viaLink.displayPath).toBe(link) }) it('realpaths the nearest existing ancestor when intermediate dirs are missing', async () => { const target = await resolveLocalTarget(dir, 'no-such-dir/child.txt') expect(target.targetKey).toBe(join(await realpath(dir), 'no-such-dir', 'child.txt')) }) it('keeps the key stable across create when an ancestor is a symlink', async () => { // A symlinked workspace root with a not-yet-created subdirectory: the // pre-create key (via the symlink, missing parent) must equal the // post-create key (file exists, realpathed) so observed-state survives. const realRoot = join(dir, 'real-root') await mkdir(realRoot) const linkRoot = join(dir, 'link-root') await symlink(realRoot, linkRoot) const before = await resolveLocalTarget(linkRoot, 'sub/file.txt') await mkdir(join(realRoot, 'sub'), { recursive: true }) await writeFile(join(realRoot, 'sub', 'file.txt'), 'hi') // create through the real path const after = await resolveLocalTarget(linkRoot, 'sub/file.txt') expect(before.targetKey).toBe(after.targetKey) }) it('rejects a blank path', async () => { await expect(resolveLocalTarget(dir, ' ')).rejects.toMatchObject({ code: 'FS_NOT_FOUND' }) }) it('rejects a path whose ancestor is a file with a structured FsError (ENOTDIR)', async () => { // "afile" is a regular file, so "afile/child.txt" hits ENOTDIR on realpath; // the raw Node error must be translated into the FsError taxonomy so the tool // result keeps its { name, code } metadata. await writeFile(join(dir, 'afile'), 'i am a file') const err = await resolveLocalTarget(dir, 'afile/child.txt').then(() => undefined, (e: unknown) => e) expect(err).toBeInstanceOf(FsError) expect(err).toMatchObject({ code: 'FS_NOT_FOUND' }) }) }) describe('probe', () => { it('returns null for a missing path and metadata for a file', async () => { expect(await probe(join(dir, 'nope'))).toBeNull() const file = join(dir, 'a.txt') await writeFile(file, 'hi') const info = await probe(file) expect(info?.type).toBe('file') expect(info?.size).toBe(2) expect(typeof info?.version).toBe('string') }) it('reports a directory and a non-regular type', async () => { const sub = join(dir, 'sub') await mkdir(sub) expect((await probe(sub))?.type).toBe('directory') }) it('reports a socket/special file as type "other"', async () => { const sockPath = join(dir, 'sock') const server = createServer() try { await new Promise((resolve, reject) => { server.once('error', reject) server.listen(sockPath, () => { resolve() }) }) } catch (error: unknown) { // A restricted sandbox may forbid unix-domain sockets; that is an // environment limit, not a filesystem regression — skip rather than fail. const code = (error as NodeJS.ErrnoException).code if (code === 'EPERM' || code === 'EACCES' || code === 'ENOTSUP') return throw error } try { expect((await probe(sockPath))?.type).toBe('other') } finally { await new Promise((resolve) => { server.close(() => { resolve() }) }) } }) it('returns null when an ancestor path segment is a file (ENOTDIR), not a raw throw', async () => { await writeFile(join(dir, 'afile'), 'i am a file') expect(await probe(join(dir, 'afile', 'child.txt'))).toBeNull() }) }) describe('probeNoFollow', () => { it('reports symlinks without following them', async () => { const real = join(dir, 'real.txt') const link = join(dir, 'link.txt') await writeFile(real, 'hi') await symlink(real, link) expect((await probeNoFollow(real))?.type).toBe('file') const linkInfo = await probeNoFollow(link) expect(linkInfo?.type).toBe('symlink') expect(typeof linkInfo?.version).toBe('string') expect(linkInfo?.size).toBeGreaterThan(0) }) it('returns null for a missing path or a file-valued ancestor path segment', async () => { expect(await probeNoFollow(join(dir, 'missing'))).toBeNull() await writeFile(join(dir, 'afile'), 'i am a file') expect(await probeNoFollow(join(dir, 'afile', 'child.txt'))).toBeNull() }) }) 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('derives child target keys from the listed parent identity', async () => { const realOne = join(dir, 'real-one') const realTwo = join(dir, 'real-two') const link = join(dir, 'link') await mkdir(realOne) await mkdir(realTwo) await writeFile(join(realOne, 'same.txt'), 'one') await writeFile(join(realTwo, 'same.txt'), 'different two') await symlink(realOne, link) const target = await resolveLocalTarget(dir, 'link') await unlink(link) await symlink(realTwo, link) const entries = await listDirectory(target) expect(entries).toHaveLength(1) expect(entries[0]).toMatchObject({ name: 'same.txt', target: { displayPath: join(link, 'same.txt'), targetKey: await realpath(join(realOne, 'same.txt')), }, size: 3, }) }) 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' }) }) it('translates directory permission failures into FS_PERMISSION_DENIED', async () => { const root = join(dir, 'restricted') await mkdir(root) await chmod(root, 0o000) try { const error = await listDirectory(localTarget(root)).then(() => undefined, (caught: unknown) => caught) // Root-like environments may still be able to list mode-000 directories. if (error === undefined) return expect(error).toBeInstanceOf(FsError) expect(error).toMatchObject({ code: 'FS_PERMISSION_DENIED' }) } finally { await chmod(root, 0o700) } }) it('translates preflight metadata IO failures into FS_IO_ERROR', async () => { const loop = join(dir, 'loop') await symlink(loop, loop) await expect(listDirectory(localTarget(loop))).rejects.toMatchObject({ code: 'FS_IO_ERROR' }) }) it('translates child resolution failures into structured listing errors', async () => { const root = join(dir, 'listed') await mkdir(root) const loop = join(root, 'loop') await symlink(loop, loop) await expect(listDirectory(localTarget(root))).rejects.toMatchObject({ code: 'FS_IO_ERROR' }) }) it('translates child permission failures into FS_PERMISSION_DENIED', async () => { const root = join(dir, 'listed') const protectedRoot = join(dir, 'protected') const secret = join(protectedRoot, 'secret') await mkdir(root) await mkdir(secret, { recursive: true }) await symlink(secret, join(root, 'secret-link')) await chmod(protectedRoot, 0o000) try { const error = await listDirectory(localTarget(root)).then(() => undefined, (caught: unknown) => caught) // Root-like environments may still resolve through mode-000 directories. if (error === undefined) return expect(error).toBeInstanceOf(FsError) expect(error).toMatchObject({ code: 'FS_PERMISSION_DENIED' }) } finally { await chmod(protectedRoot, 0o700) } }) }) describe('readWholeText', () => { it('reads a small file', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo\nthree') expect(await readWholeText(localTarget(file))).toBe('one\ntwo\nthree') }) it('rejects a missing file and a directory', async () => { await expect(readWholeText(localTarget(join(dir, 'nope')))).rejects.toMatchObject({ code: 'FS_NOT_FOUND' }) await expect(readWholeText(localTarget(dir))).rejects.toMatchObject({ code: 'FS_NOT_REGULAR_FILE' }) }) it('rejects binary and invalid UTF-8', async () => { await writeFile(join(dir, 'bin'), Buffer.from([0x68, 0x00, 0x69])) await expect(readWholeText(localTarget(join(dir, 'bin')))).rejects.toMatchObject({ code: 'FS_NOT_TEXT' }) await writeFile(join(dir, 'bad'), Buffer.from([0x68, 0xff, 0x69])) await expect(readWholeText(localTarget(join(dir, 'bad')))).rejects.toMatchObject({ code: 'FS_NOT_TEXT' }) }) it('honors a pre-aborted signal', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one') await expect(readWholeText(localTarget(file), AbortSignal.abort())).rejects.toMatchObject({ code: 'FS_ABORTED' }) }) it('passes a live (non-aborted) signal through', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo') expect(await readWholeText(localTarget(file), new AbortController().signal)).toBe('one\ntwo') }) it('translates a mid-read AbortError into FS_ABORTED', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo') const ac = new AbortController() // Abort after the synchronous entry check but before readFile runs (the // stat await yields control back here), so readFile rejects AbortError. const pending = readWholeText(localTarget(file), ac.signal) ac.abort() await expect(pending).rejects.toMatchObject({ code: 'FS_ABORTED' }) }) }) describe('streamWholeText', () => { it('streams the whole file as decoded text', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo\nthree') expect(await collect(streamWholeText(localTarget(file)))).toBe('one\ntwo\nthree') }) it('streams a large multi-chunk file correctly', async () => { const file = join(dir, 'big.txt') const content = Array.from({ length: 50 }, (_, i) => `line ${i}: ${'x'.repeat(3000)}`).join('\n') await writeFile(file, content) expect(await collect(streamWholeText(localTarget(file)))).toBe(content) }) it('rejects a missing file, directory, binary, and invalid UTF-8', async () => { await expect(collect(streamWholeText(localTarget(join(dir, 'nope'))))).rejects.toMatchObject({ code: 'FS_NOT_FOUND' }) await expect(collect(streamWholeText(localTarget(dir)))).rejects.toMatchObject({ code: 'FS_NOT_REGULAR_FILE' }) await writeFile(join(dir, 'bin'), Buffer.from([0x68, 0x00, 0x69])) await expect(collect(streamWholeText(localTarget(join(dir, 'bin'))))).rejects.toMatchObject({ code: 'FS_NOT_TEXT' }) await writeFile(join(dir, 'bad'), Buffer.from([0x68, 0xff, 0x69])) await expect(collect(streamWholeText(localTarget(join(dir, 'bad'))))).rejects.toMatchObject({ code: 'FS_NOT_TEXT' }) }) it('honors a pre-aborted signal', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one') await expect(collect(streamWholeText(localTarget(file), AbortSignal.abort()))).rejects.toMatchObject({ code: 'FS_ABORTED' }) }) it('passes a live (non-aborted) signal through the stream', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo') expect(await collect(streamWholeText(localTarget(file), new AbortController().signal))).toBe('one\ntwo') }) it('translates a mid-stream abort into FS_ABORTED', async () => { // A multi-chunk file so the stream yields more than once; abort after the // first chunk and assert the structured code, not a raw AbortError. const file = join(dir, 'big.txt') await writeFile(file, 'x'.repeat(256 * 1024)) const ac = new AbortController() const run = async (): Promise => { let seen = 0 for await (const _chunk of streamWholeText(localTarget(file), ac.signal)) { seen += 1 if (seen === 1) ac.abort() } } await expect(run()).rejects.toMatchObject({ code: 'FS_ABORTED' }) }) }) // Windows drives only the read-only attribute through `chmod` and reports synthetic `stat` mode // bits, so mode assertions are POSIX-only; native DACL preservation is asserted separately. const posixModes = process.platform !== 'win32' function daclAcePolicy(descriptor: Buffer): string[] { const daclOffset = descriptor.readUInt32LE(16) if (daclOffset === 0) return [] const aceCount = descriptor.readUInt16LE(daclOffset + 4) const policy: string[] = [] const seen = new Set() let offset = daclOffset + 8 for (let index = 0; index < aceCount; index++) { const size = descriptor.readUInt16LE(offset + 2) const ace = Buffer.from(descriptor.subarray(offset, offset + size)) // INHERITED_ACE records provenance, not the entry's access policy. ace.writeUInt8(ace.readUInt8(1) & ~0x10, 1) const key = ace.toString('hex') if (!seen.has(key)) { seen.add(key) policy.push(key) } offset += size } return policy } describe('writeFileAtomic — temp-file safety', () => { it('writes through a private staging dir and owner-only temp file', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'old') if (posixModes) await chmod(file, 0o640) let inspected = false await writeFileAtomic(file, 'hello', 0o640, undefined, { inspectTemp: async ({ stagingDir, tempPath }) => { inspected = true const [staging, temp] = await Promise.all([stat(stagingDir), stat(tempPath)]) expect(staging.isDirectory()).toBe(true) expect(temp.isFile()).toBe(true) if (posixModes) { expect(staging.mode & 0o777).toBe(0o700) expect(temp.mode & 0o777).toBe(0o600) } }, }) expect(inspected).toBe(true) expect(await readFile(file, 'utf8')).toBe('hello') if (posixModes) expect((await stat(file)).mode & 0o777).toBe(0o640) expect((await readdir(dir)).filter(n => n.includes('.tmp'))).toEqual([]) }) it.skipIf(process.platform !== 'win32')('protects staged content with the existing target DACL and preserves it after replacement', async () => { const file = join(dir, 'protected.txt') await writeFile(file, 'old') await copyFileDaclWin32(file, file) const expectedDacl = await readFileDaclWin32(file) await writeFileAtomic(file, 'new', (await stat(file)).mode, undefined, { inspectTemp: async ({ tempPath }) => { expect(await readFileDaclWin32(tempPath)).toEqual(expectedDacl) }, }) expect(await readFile(file, 'utf8')).toBe('new') expect(daclAcePolicy(await readFileDaclWin32(file))).toEqual(daclAcePolicy(expectedDacl)) }) it('copies a Windows target DACL before content and publishes through secure replacement', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'old') const calls: string[] = [] await writeFileAtomic(file, 'new', 0o666, undefined, { platform: 'win32', copyFileDacl: async (source, temp) => { calls.push(`copy:${source}`) expect(await readFile(temp, 'utf8')).toBe('') }, replaceFile: async (target, temp) => { calls.push(`replace:${target}`) await rename(temp, target) }, }) expect(calls).toEqual([`copy:${file}`, `replace:${file}`]) expect(await readFile(file, 'utf8')).toBe('new') }) it('creates a new Windows file through directory inheritance without replacement calls', async () => { const file = join(dir, 'new.txt') const unexpected = async (): Promise => { throw new Error('unexpected native replacement call') } await writeFileAtomic(file, 'new', undefined, undefined, { platform: 'win32', copyFileDacl: unexpected, replaceFile: unexpected, }) expect(await readFile(file, 'utf8')).toBe('new') }) it('recreates a vanished Windows target with the already-protected temp', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'old') const missing = Object.assign(new Error('target vanished'), { code: 'ENOENT' }) await writeFileAtomic(file, 'new', 0o666, undefined, { platform: 'win32', copyFileDacl: () => Promise.resolve(), replaceFile: async () => { throw missing }, }) expect(await readFile(file, 'utf8')).toBe('new') }) it('surfaces a Windows secure-replacement failure and cleans the staging directory', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'old') const denied = Object.assign(new Error('replace denied'), { code: 'EACCES' }) await expect(writeFileAtomic(file, 'new', 0o666, undefined, { platform: 'win32', copyFileDacl: () => Promise.resolve(), replaceFile: async () => { throw denied }, })).rejects.toBe(denied) expect(await readFile(file, 'utf8')).toBe('old') expect((await readdir(dir)).filter(name => name.includes('.tmp'))).toEqual([]) }) it.skipIf(!posixModes)('creates new files owner-only by default', async () => { const file = join(dir, 'a.txt') await writeFileAtomic(file, 'hello', undefined, undefined) expect((await stat(file)).mode & 0o777).toBe(0o600) }) it('opens staging paths exclusively — a pre-existing path is never clobbered', async () => { const file = join(dir, 'a.txt') const tempDirName = '.fixed-temp.tmpdir' await mkdir(join(dir, tempDirName)) await writeFile(join(dir, tempDirName, 'PRECIOUS'), 'keep') await expect( writeFileAtomic(file, 'hello', undefined, undefined, { tempDirName: () => tempDirName }), ).rejects.toMatchObject({ code: 'EEXIST' }) expect(await readFile(join(dir, tempDirName, 'PRECIOUS'), 'utf8')).toBe('keep') await expect(stat(file)).rejects.toMatchObject({ code: 'ENOENT' }) }) it('creates parent directories as needed', async () => { const file = join(dir, 'nested', 'deep', 'a.txt') await writeFileAtomic(file, 'hi', undefined, undefined) expect(await readFile(file, 'utf8')).toBe('hi') }) it('passes a live (non-aborted) signal through the write', async () => { const file = join(dir, 'a.txt') await writeFileAtomic(file, 'hi', undefined, new AbortController().signal) expect(await readFile(file, 'utf8')).toBe('hi') }) it('aborts before writing when the signal is already aborted', async () => { const file = join(dir, 'a.txt') await expect(writeFileAtomic(file, 'hi', undefined, AbortSignal.abort())).rejects.toMatchObject({ code: 'FS_ABORTED' }) await expect(stat(file)).rejects.toMatchObject({ code: 'ENOENT' }) }) it('cleans up the temp file when the final rename fails', async () => { const sub = join(dir, 'occupied') await mkdir(sub) await expect(writeFileAtomic(sub, 'hi', undefined, undefined)).rejects.toBeInstanceOf(Error) expect((await readdir(dir)).filter(n => n.includes('.tmp'))).toEqual([]) }) }) describe('applyLiteralEdit', () => { it('replaces a unique match', () => { expect(applyLiteralEdit('a b c', 'b', 'X', false, 'f')).toEqual({ content: 'a X c', replacements: 1 }) }) it('rejects zero matches', () => { expect(() => applyLiteralEdit('a b c', 'z', 'X', false, 'f')).toThrow(expect.objectContaining({ code: 'FS_EDIT_NOT_FOUND' })) }) it('rejects an empty oldString without scanning forever', () => { expect(() => applyLiteralEdit('a b c', '', 'X', false, 'f')).toThrow(expect.objectContaining({ code: 'FS_EDIT_NOT_FOUND' })) }) it('rejects multiple matches without replaceAll', () => { expect(() => applyLiteralEdit('a a a', 'a', 'X', false, 'f')).toThrow(expect.objectContaining({ code: 'FS_AMBIGUOUS_EDIT' })) }) it('replaces all matches with replaceAll', () => { expect(applyLiteralEdit('a a a', 'a', 'X', true, 'f')).toEqual({ content: 'X X X', replacements: 3 }) }) it('matches across normalized line endings', () => { expect(applyLiteralEdit('one\ntwo', 'one\ntwo', 'x', false, 'f').replacements).toBe(1) }) }) describe('readForEdit + restoreLineEndings', () => { it('round-trips CRLF: matches on LF, writes back CRLF', async () => { const file = join(dir, 'crlf.txt') await writeFile(file, 'one\r\ntwo\r\n') const original = await readForEdit(file, file) expect(original.lineEndings).toBe('CRLF') const edited = applyLiteralEdit(original.content, 'two', 'TWO', false, file) expect(restoreLineEndings(edited.content, original.lineEndings)).toBe('one\r\nTWO\r\n') }) it('rejects a binary file and invalid UTF-8', async () => { await writeFile(join(dir, 'bin'), Buffer.from([0x00, 0x01])) await expect(readForEdit(join(dir, 'bin'), join(dir, 'bin'))).rejects.toMatchObject({ code: 'FS_NOT_TEXT' }) await writeFile(join(dir, 'bad'), Buffer.from([0x68, 0xff, 0x69])) await expect(readForEdit(join(dir, 'bad'), join(dir, 'bad'))).rejects.toMatchObject({ code: 'FS_NOT_TEXT' }) }) it('passes a live (non-aborted) signal through the read', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo') const original = await readForEdit(file, file, new AbortController().signal) expect(original.content).toBe('one\ntwo') }) it('translates a mid-read AbortError into FS_ABORTED', async () => { const file = join(dir, 'a.txt') await writeFile(file, 'one\ntwo') const ac = new AbortController() // Abort after the synchronous entry check, while readFile is pending. const pending = readForEdit(file, file, ac.signal) ac.abort() await expect(pending).rejects.toMatchObject({ code: 'FS_ABORTED' }) }) })