read-render.spec.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * Cordis-free tests for the line-windowing module: offset/limit windows, byte
  3. * caps, per-line truncation, CRLF stripping, offset-past-EOF rejection, and the
  4. * capped line buffer for newline-free giant lines — all over an async-iterable
  5. * of decoded text chunks (so one code path serves whole-file and streamed reads).
  6. */
  7. import { describe, expect, it } from 'vitest'
  8. import { buildWindow, READ_MAX_LINE_LENGTH } from '@deepseek-ai/dsh-tool-fs'
  9. import type { ReadWindow } from '@deepseek-ai/dsh-tool-fs'
  10. const READ_ALL: ReadWindow = { offset: 1, limit: 2000 }
  11. /** Yield `text` as one chunk (whole-file read shape). */
  12. async function* whole(text: string): AsyncIterable<string> {
  13. yield text
  14. }
  15. /** Yield `text` split into fixed-size chunks (streamed read shape). */
  16. async function* chunked(text: string, size: number): AsyncIterable<string> {
  17. for (let i = 0; i < text.length; i += size) yield text.slice(i, i + size)
  18. }
  19. describe('buildWindow', () => {
  20. it('numbers lines and reports total for a whole-file read', async () => {
  21. const result = await buildWindow(whole('one\ntwo\nthree'), READ_ALL, 'f')
  22. expect(result.lines).toEqual([
  23. { number: 1, text: 'one' },
  24. { number: 2, text: 'two' },
  25. { number: 3, text: 'three' },
  26. ])
  27. expect(result.totalLines).toBe(3)
  28. expect(result.truncatedByBytes).toBe(false)
  29. })
  30. it('applies offset/limit', async () => {
  31. const result = await buildWindow(whole('one\ntwo\nthree\nfour'), { offset: 2, limit: 2 }, 'f')
  32. expect(result.lines.map(l => l.number)).toEqual([2, 3])
  33. expect(result.totalLines).toBe(4)
  34. })
  35. it('strips CRLF', async () => {
  36. const result = await buildWindow(whole('one\r\ntwo\r\n'), READ_ALL, 'f')
  37. expect(result.lines.map(l => l.text)).toEqual(['one', 'two'])
  38. })
  39. it('truncates an over-long line', async () => {
  40. const result = await buildWindow(whole('x'.repeat(3000)), READ_ALL, 'f')
  41. expect(result.lines[0]?.text).toContain(`... (line truncated to ${READ_MAX_LINE_LENGTH} chars)`)
  42. })
  43. it('caps output bytes and reports truncatedByBytes', async () => {
  44. const big = Array.from({ length: 2000 }, () => 'y'.repeat(100)).join('\n')
  45. const result = await buildWindow(whole(big), READ_ALL, 'f')
  46. expect(result.truncatedByBytes).toBe(true)
  47. })
  48. it('reads an empty file at offset 1 as zero lines', async () => {
  49. const result = await buildWindow(whole(''), READ_ALL, 'f')
  50. expect(result.lines).toEqual([])
  51. expect(result.totalLines).toBe(0)
  52. })
  53. it('rejects an offset past EOF', async () => {
  54. await expect(buildWindow(whole('one\ntwo'), { offset: 9, limit: 1 }, 'f')).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
  55. })
  56. it('flushes a final line with no trailing newline', async () => {
  57. const result = await buildWindow(whole('one\ntwo'), READ_ALL, 'f')
  58. expect(result.lines.map(l => l.text)).toEqual(['one', 'two'])
  59. })
  60. it('handles a trailing newline (no dangling empty line)', async () => {
  61. const result = await buildWindow(whole('one\ntwo\n'), READ_ALL, 'f')
  62. expect(result.lines.map(l => l.text)).toEqual(['one', 'two'])
  63. expect(result.totalLines).toBe(2)
  64. })
  65. describe('chunked input (streamed read shape)', () => {
  66. it('windows identically when text arrives in small chunks', async () => {
  67. const result = await buildWindow(chunked('one\ntwo\nthree', 2), { offset: 2, limit: 1 }, 'f')
  68. expect(result.lines).toEqual([{ number: 2, text: 'two' }])
  69. expect(result.totalLines).toBe(3)
  70. })
  71. it('caps a newline-free giant line split across chunks without unbounded buffering', async () => {
  72. const result = await buildWindow(chunked('z'.repeat(5000), 256), READ_ALL, 'f')
  73. expect(result.lines[0]?.text).toContain(`... (line truncated to ${READ_MAX_LINE_LENGTH} chars)`)
  74. })
  75. it('caps output bytes mid-stream', async () => {
  76. const big = Array.from({ length: 2000 }, () => 'y'.repeat(100)).join('\n')
  77. const result = await buildWindow(chunked(big, 512), READ_ALL, 'f')
  78. expect(result.truncatedByBytes).toBe(true)
  79. })
  80. it('flushes a final newline-terminated line across a chunk boundary', async () => {
  81. const result = await buildWindow(chunked('one\ntwo\n', 3), READ_ALL, 'f')
  82. expect(result.lines.map(l => l.text)).toEqual(['one', 'two'])
  83. })
  84. })
  85. })