@@ -22,15 +22,22 @@ const __dirname = path.dirname(__filename);
2222const TEST_DIR = path . join ( __dirname , 'test_crlf_preservation' ) ;
2323const CRLF_FILE = path . join ( TEST_DIR , 'crlf_file.txt' ) ;
2424const LF_FILE = path . join ( TEST_DIR , 'lf_file.txt' ) ;
25+ const CRLF_NO_TRAILING_FILE = path . join ( TEST_DIR , 'crlf_no_trailing.txt' ) ;
26+ const LF_NO_TRAILING_FILE = path . join ( TEST_DIR , 'lf_no_trailing.txt' ) ;
2527
2628// The raw CRLF content (5 lines, each ending with \r\n)
2729const CRLF_CONTENT = 'Line one\r\nLine two\r\nLine three\r\nLine four\r\nLine five\r\n' ;
2830const LF_CONTENT = 'Line one\nLine two\nLine three\nLine four\nLine five\n' ;
31+ // Files without trailing newline (last line has no newline after it)
32+ const CRLF_NO_TRAILING_CONTENT = 'Line one\r\nLine two\r\nLine three\r\nLine four\r\nLine five' ;
33+ const LF_NO_TRAILING_CONTENT = 'Line one\nLine two\nLine three\nLine four\nLine five' ;
2934
3035async function setup ( ) {
3136 await fs . mkdir ( TEST_DIR , { recursive : true } ) ;
3237 await fs . writeFile ( CRLF_FILE , CRLF_CONTENT , 'utf8' ) ;
3338 await fs . writeFile ( LF_FILE , LF_CONTENT , 'utf8' ) ;
39+ await fs . writeFile ( CRLF_NO_TRAILING_FILE , CRLF_NO_TRAILING_CONTENT , 'utf8' ) ;
40+ await fs . writeFile ( LF_NO_TRAILING_FILE , LF_NO_TRAILING_CONTENT , 'utf8' ) ;
3441
3542 const originalConfig = await configManager . getConfig ( ) ;
3643 await configManager . setValue ( 'allowedDirectories' , [ TEST_DIR ] ) ;
@@ -210,6 +217,116 @@ async function testReadWriteRoundtrip() {
210217 console . log ( ' PASS' ) ;
211218}
212219
220+ /**
221+ * Test 6: Partial read (not reaching EOF) preserves CRLF on last line
222+ * Edge case from CodeRabbit review: when reading only a subset of lines,
223+ * the last line read still has content after it in the file, so its
224+ * trailing newline must be preserved.
225+ */
226+ async function testPartialReadPreservesCRLF ( ) {
227+ console . log ( '\n Test 6: partial read (not reaching EOF) preserves CRLF' ) ;
228+
229+ // Read only first 2 lines of the 5-line CRLF file
230+ const result = await handleReadFile ( { path : CRLF_FILE , offset : 0 , length : 2 } ) ;
231+ const text = result . content [ 0 ] . text ;
232+
233+ const contentStart = text . indexOf ( 'Line one' ) ;
234+ const fileContent = text . substring ( contentStart ) ;
235+
236+ // Should contain CRLF, not bare LF
237+ assert . ok (
238+ fileContent . includes ( '\r\n' ) ,
239+ 'Partial read must preserve CRLF line endings'
240+ ) ;
241+ assert . ok (
242+ ! fileContent . match ( / (?< ! \r ) \n / ) ,
243+ 'Partial read must not contain bare LF'
244+ ) ;
245+ // The last line read ("Line two") should end with \r\n because
246+ // there are more lines after it in the file
247+ assert . ok (
248+ fileContent . endsWith ( '\r\n' ) ,
249+ 'Last line of partial read must end with CRLF (more content follows in file)'
250+ ) ;
251+ console . log ( ' PASS' ) ;
252+ }
253+
254+ /**
255+ * Test 7: Partial read (not reaching EOF) of LF file preserves LF on last line
256+ */
257+ async function testPartialReadPreservesLF ( ) {
258+ console . log ( '\n Test 7: partial read (not reaching EOF) preserves LF' ) ;
259+
260+ const result = await handleReadFile ( { path : LF_FILE , offset : 0 , length : 2 } ) ;
261+ const text = result . content [ 0 ] . text ;
262+
263+ const contentStart = text . indexOf ( 'Line one' ) ;
264+ const fileContent = text . substring ( contentStart ) ;
265+
266+ assert . ok (
267+ ! fileContent . includes ( '\r' ) ,
268+ 'LF partial read must not contain CR'
269+ ) ;
270+ assert . ok (
271+ fileContent . endsWith ( '\n' ) ,
272+ 'Last line of LF partial read must end with LF (more content follows in file)'
273+ ) ;
274+ console . log ( ' PASS' ) ;
275+ }
276+
277+ /**
278+ * Test 8: Full read of file without trailing newline must NOT add one
279+ */
280+ async function testNoTrailingNewlinePreserved ( ) {
281+ console . log ( '\n Test 8: full read of file without trailing newline does not add one' ) ;
282+
283+ // Read all 5 lines (the file has no trailing newline)
284+ const result = await handleReadFile ( { path : CRLF_NO_TRAILING_FILE , offset : 0 , length : 1000 } ) ;
285+ const text = result . content [ 0 ] . text ;
286+
287+ const contentStart = text . indexOf ( 'Line one' ) ;
288+ const fileContent = text . substring ( contentStart ) ;
289+
290+ // All internal line endings should be CRLF
291+ assert . ok (
292+ fileContent . includes ( '\r\n' ) ,
293+ 'Internal line endings must be CRLF'
294+ ) ;
295+ // But the content must NOT end with \r\n since the file doesn't
296+ assert . ok (
297+ ! fileContent . endsWith ( '\r\n' ) ,
298+ 'Must not add trailing CRLF when original file lacks one'
299+ ) ;
300+ assert . ok (
301+ fileContent . endsWith ( 'Line five' ) ,
302+ 'Content must end with last line text (no trailing newline)'
303+ ) ;
304+ console . log ( ' PASS' ) ;
305+ }
306+
307+ /**
308+ * Test 9: Partial read of file without trailing newline - mid-file read should still get newline
309+ */
310+ async function testPartialReadNoTrailingNewlineFile ( ) {
311+ console . log ( '\n Test 9: partial read of no-trailing-newline file still gets newline at mid-file' ) ;
312+
313+ // Read first 2 lines of the no-trailing-newline file
314+ const result = await handleReadFile ( { path : CRLF_NO_TRAILING_FILE , offset : 0 , length : 2 } ) ;
315+ const text = result . content [ 0 ] . text ;
316+
317+ const contentStart = text . indexOf ( 'Line one' ) ;
318+ const fileContent = text . substring ( contentStart ) ;
319+
320+ // Even though the file doesn't end with a newline, our partial read
321+ // is in the middle of the file — the last line we read ("Line two")
322+ // definitely has a newline after it.
323+ assert . ok (
324+ fileContent . endsWith ( '\r\n' ) ,
325+ 'Partial read from middle of file must end with CRLF regardless of file trailing newline'
326+ ) ;
327+ console . log ( ' PASS' ) ;
328+ }
329+
213330async function runTests ( ) {
214331 console . log ( '=== CRLF Preservation Tests (issue #97) ===' ) ;
215332 let originalConfig ;
@@ -220,6 +337,10 @@ async function runTests() {
220337 await testReadFileLFUnchanged ( ) ;
221338 await testEditBlockPreservesCRLF ( ) ;
222339 await testReadWriteRoundtrip ( ) ;
340+ await testPartialReadPreservesCRLF ( ) ;
341+ await testPartialReadPreservesLF ( ) ;
342+ await testNoTrailingNewlinePreserved ( ) ;
343+ await testPartialReadNoTrailingNewlineFile ( ) ;
223344 console . log ( '\n All CRLF preservation tests passed!\n' ) ;
224345 return true ;
225346 } catch ( error ) {
0 commit comments