From adc5cab3664cc332156cb555cb1db1e99a46edd4 Mon Sep 17 00:00:00 2001 From: Sean Doherty Date: Sat, 16 May 2026 16:06:40 -0500 Subject: [PATCH] fix(filesystem): ignore final newline in tail output --- src/filesystem/__tests__/lib.test.ts | 55 ++++++++++++++++++++-------- src/filesystem/lib.ts | 6 +++ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index bfe8987bfd..b994d98001 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -553,6 +553,23 @@ describe('Lib Functions', () => { }); describe('tailFile', () => { + const mockTailReadableFile = (content: string) => { + const fileBuffer = Buffer.from(content, 'utf-8'); + mockFs.stat.mockResolvedValue({ size: fileBuffer.length } as any); + + const mockFileHandle = { + read: vi.fn(async (buffer: Buffer, offset: number, length: number, position: number) => { + const chunk = fileBuffer.subarray(position, position + length); + chunk.copy(buffer, offset); + return { bytesRead: chunk.length, buffer }; + }), + close: vi.fn().mockResolvedValue(undefined) + } as any; + + mockFs.open.mockResolvedValue(mockFileHandle); + return mockFileHandle; + }; + it('handles empty files', async () => { mockFs.stat.mockResolvedValue({ size: 0 } as any); @@ -583,23 +600,29 @@ describe('Lib Functions', () => { }); it('handles files with content and returns last lines', async () => { - mockFs.stat.mockResolvedValue({ size: 50 } as any); - - const mockFileHandle = { - read: vi.fn(), - close: vi.fn() - } as any; - - // Simulate reading file content in chunks - mockFileHandle.read - .mockResolvedValueOnce({ bytesRead: 20, buffer: Buffer.from('line3\nline4\nline5\n') }) - .mockResolvedValueOnce({ bytesRead: 0 }); - mockFileHandle.close.mockResolvedValue(undefined); - - mockFs.open.mockResolvedValue(mockFileHandle); - + const mockFileHandle = mockTailReadableFile('line1\nline2\nline3\nline4\nline5'); + const result = await tailFile('/test/file.txt', 2); - + + expect(result).toBe('line4\nline5'); + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + + it('does not treat a final newline as an empty tail line', async () => { + const mockFileHandle = mockTailReadableFile('line1\nline2\nline3\n'); + + const result = await tailFile('/test/file.txt', 1); + + expect(result).toBe('line3'); + expect(mockFileHandle.close).toHaveBeenCalled(); + }); + + it('returns the requested lines when content ends in a newline', async () => { + const mockFileHandle = mockTailReadableFile('line1\nline2\nline3\n'); + + const result = await tailFile('/test/file.txt', 2); + + expect(result).toBe('line2\nline3'); expect(mockFileHandle.close).toHaveBeenCalled(); }); diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index 240ca0d476..04d51501e1 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -274,6 +274,7 @@ export async function tailFile(filePath: string, numLines: number): Promise 0 && linesFound < numLines) { @@ -289,6 +290,11 @@ export async function tailFile(filePath: string, numLines: number): Promise