@aaronshaf/ger 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ const server = setupServer(
27
27
  // Store captured output
28
28
  let capturedLogs: string[] = []
29
29
  let capturedErrors: string[] = []
30
+ let capturedStdout: string[] = []
30
31
 
31
32
  // Mock console.log and console.error
32
33
  const mockConsoleLog = mock((...args: any[]) => {
@@ -36,9 +37,20 @@ const mockConsoleError = mock((...args: any[]) => {
36
37
  capturedErrors.push(args.join(' '))
37
38
  })
38
39
 
39
- // Store original console methods
40
+ // Mock process.stdout.write to capture JSON output and handle callbacks
41
+ const mockStdoutWrite = mock((chunk: any, callback?: any) => {
42
+ capturedStdout.push(String(chunk))
43
+ // Call the callback synchronously if provided
44
+ if (typeof callback === 'function') {
45
+ callback()
46
+ }
47
+ return true
48
+ })
49
+
50
+ // Store original methods
40
51
  const originalConsoleLog = console.log
41
52
  const originalConsoleError = console.error
53
+ const originalStdoutWrite = process.stdout.write
42
54
 
43
55
  beforeAll(() => {
44
56
  server.listen({ onUnhandledRequest: 'bypass' })
@@ -46,20 +58,26 @@ beforeAll(() => {
46
58
  console.log = mockConsoleLog
47
59
  // @ts-ignore
48
60
  console.error = mockConsoleError
61
+ // @ts-ignore
62
+ process.stdout.write = mockStdoutWrite
49
63
  })
50
64
 
51
65
  afterAll(() => {
52
66
  server.close()
53
67
  console.log = originalConsoleLog
54
68
  console.error = originalConsoleError
69
+ // @ts-ignore
70
+ process.stdout.write = originalStdoutWrite
55
71
  })
56
72
 
57
73
  afterEach(() => {
58
74
  server.resetHandlers()
59
75
  mockConsoleLog.mockClear()
60
76
  mockConsoleError.mockClear()
77
+ mockStdoutWrite.mockClear()
61
78
  capturedLogs = []
62
79
  capturedErrors = []
80
+ capturedStdout = []
63
81
  })
64
82
 
65
83
  describe('show command', () => {
@@ -201,7 +219,7 @@ describe('show command', () => {
201
219
 
202
220
  await Effect.runPromise(program)
203
221
 
204
- const output = capturedLogs.join('\n')
222
+ const output = capturedStdout.join('')
205
223
 
206
224
  expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
207
225
  expect(output).toContain('<show_result>')
@@ -258,7 +276,7 @@ describe('show command', () => {
258
276
 
259
277
  await Effect.runPromise(program)
260
278
 
261
- const output = capturedLogs.join('\n')
279
+ const output = capturedStdout.join('')
262
280
 
263
281
  expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
264
282
  expect(output).toContain('<show_result>')
@@ -301,7 +319,7 @@ describe('show command', () => {
301
319
 
302
320
  await Effect.runPromise(program)
303
321
 
304
- const output = capturedLogs.join('\n')
322
+ const output = capturedStdout.join('')
305
323
 
306
324
  expect(output).toContain('<subject><![CDATA[Fix "quotes" & <tags> in auth]]></subject>')
307
325
  expect(output).toContain('<branch>feature/fix&amp;improve</branch>')
@@ -450,7 +468,7 @@ describe('show command', () => {
450
468
 
451
469
  await Effect.runPromise(program)
452
470
 
453
- const output = capturedLogs.join('\n')
471
+ const output = capturedStdout.join('')
454
472
 
455
473
  // Parse JSON to verify it's valid
456
474
  const parsed = JSON.parse(output)
@@ -495,7 +513,7 @@ describe('show command', () => {
495
513
 
496
514
  await Effect.runPromise(program)
497
515
 
498
- const output = capturedLogs.join('\n')
516
+ const output = capturedStdout.join('')
499
517
 
500
518
  // Parse JSON to verify it's valid
501
519
  const parsed = JSON.parse(output)
@@ -516,7 +534,7 @@ describe('show command', () => {
516
534
 
517
535
  await Effect.runPromise(program)
518
536
 
519
- const output = capturedLogs.join('\n')
537
+ const output = capturedStdout.join('')
520
538
 
521
539
  // Extract comment sections to verify order
522
540
  const commentMatches = output.matchAll(
@@ -581,7 +599,7 @@ describe('show command', () => {
581
599
 
582
600
  await Effect.runPromise(program)
583
601
 
584
- const output = capturedLogs.join('\n')
602
+ const output = capturedStdout.join('')
585
603
  const parsed = JSON.parse(output)
586
604
 
587
605
  expect(parsed.messages).toBeDefined()
@@ -592,4 +610,204 @@ describe('show command', () => {
592
610
  expect(parsed.messages[0].author.name).toBe('Jenkins Bot')
593
611
  expect(parsed.messages[0].revision).toBe(2)
594
612
  })
613
+
614
+ test('should handle large JSON output without truncation', async () => {
615
+ // Create a large diff to simulate output > 64KB
616
+ const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
617
+
618
+ const mockChange = generateMockChange({
619
+ _number: 12345,
620
+ subject: 'Large change with extensive diff',
621
+ })
622
+
623
+ // Create many comments to increase JSON size
624
+ const manyComments: Record<string, any[]> = {
625
+ 'src/file.js': Array.from({ length: 100 }, (_, i) => ({
626
+ id: `comment${i}`,
627
+ path: 'src/file.js',
628
+ line: i + 1,
629
+ message: `Comment ${i}: ${'a'.repeat(500)}`, // Make comments substantial
630
+ author: {
631
+ name: 'Reviewer',
632
+ email: 'reviewer@example.com',
633
+ },
634
+ updated: '2024-01-15 11:30:00.000000000',
635
+ unresolved: false,
636
+ })),
637
+ }
638
+
639
+ server.use(
640
+ http.get('*/a/changes/:changeId', () => {
641
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
642
+ }),
643
+ http.get('*/a/changes/:changeId/revisions/current/patch', () => {
644
+ return HttpResponse.text(btoa(largeDiff))
645
+ }),
646
+ http.get('*/a/changes/:changeId/revisions/current/comments', () => {
647
+ return HttpResponse.text(`)]}'\n${JSON.stringify(manyComments)}`)
648
+ }),
649
+ http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
650
+ return HttpResponse.text('context')
651
+ }),
652
+ )
653
+
654
+ const mockConfigLayer = createMockConfigLayer()
655
+ const program = showCommand('12345', { json: true }).pipe(
656
+ Effect.provide(GerritApiServiceLive),
657
+ Effect.provide(mockConfigLayer),
658
+ )
659
+
660
+ await Effect.runPromise(program)
661
+
662
+ const output = capturedStdout.join('')
663
+
664
+ // Verify output is larger than 64KB (the previous truncation point)
665
+ expect(output.length).toBeGreaterThan(65536)
666
+
667
+ // Verify JSON is valid and complete
668
+ const parsed = JSON.parse(output)
669
+ expect(parsed.status).toBe('success')
670
+ expect(parsed.diff).toContain('x'.repeat(100000))
671
+ expect(parsed.comments.length).toBe(100)
672
+
673
+ // Verify last comment is present (proves no truncation)
674
+ const lastComment = parsed.comments[parsed.comments.length - 1]
675
+ expect(lastComment.message).toContain('Comment 99')
676
+ })
677
+
678
+ test('should handle stdout drain event when buffer is full', async () => {
679
+ setupMockHandlers()
680
+
681
+ // Store original stdout.write
682
+ const originalStdoutWrite = process.stdout.write
683
+
684
+ let drainCallback: (() => void) | null = null
685
+ let errorCallback: ((err: Error) => void) | null = null
686
+ let writeCallbackFn: ((err?: Error) => void) | null = null
687
+
688
+ // Mock stdout.write to simulate full buffer
689
+ const mockWrite = mock((chunk: any, callback?: any) => {
690
+ capturedStdout.push(String(chunk))
691
+ writeCallbackFn = callback
692
+ // Return false to simulate full buffer
693
+ return false
694
+ })
695
+
696
+ // Mock stdout.once to capture drain and error listeners
697
+ const mockOnce = mock((event: string, callback: any) => {
698
+ if (event === 'drain') {
699
+ drainCallback = callback
700
+ // Simulate drain event after a short delay
701
+ setTimeout(() => {
702
+ if (drainCallback) {
703
+ drainCallback()
704
+ if (writeCallbackFn) {
705
+ writeCallbackFn()
706
+ }
707
+ }
708
+ }, 10)
709
+ } else if (event === 'error') {
710
+ errorCallback = callback
711
+ }
712
+ return process.stdout
713
+ })
714
+
715
+ // Apply mocks
716
+ // @ts-ignore
717
+ process.stdout.write = mockWrite
718
+ // @ts-ignore
719
+ process.stdout.once = mockOnce
720
+
721
+ const mockConfigLayer = createMockConfigLayer()
722
+ const program = showCommand('12345', { json: true }).pipe(
723
+ Effect.provide(GerritApiServiceLive),
724
+ Effect.provide(mockConfigLayer),
725
+ )
726
+
727
+ await Effect.runPromise(program)
728
+
729
+ // Restore original stdout.write
730
+ // @ts-ignore
731
+ process.stdout.write = originalStdoutWrite
732
+
733
+ // Verify that write returned false (buffer full)
734
+ expect(mockWrite).toHaveBeenCalled()
735
+
736
+ // Verify that drain listener was registered
737
+ expect(mockOnce).toHaveBeenCalledWith('drain', expect.any(Function))
738
+
739
+ // Verify that error listener was registered for robustness
740
+ expect(mockOnce).toHaveBeenCalledWith('error', expect.any(Function))
741
+
742
+ // Verify output is still valid JSON despite drain handling
743
+ const output = capturedStdout.join('')
744
+ const parsed = JSON.parse(output)
745
+ expect(parsed.status).toBe('success')
746
+ expect(parsed.change.id).toBe('I123abc456def')
747
+ })
748
+
749
+ test('should handle large XML output without truncation', async () => {
750
+ // Create a large diff to simulate output > 64KB
751
+ const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
752
+
753
+ const mockChange = generateMockChange({
754
+ _number: 12345,
755
+ subject: 'Large change with extensive diff',
756
+ })
757
+
758
+ // Create many comments to increase XML size
759
+ const manyComments: Record<string, any[]> = {
760
+ 'src/file.js': Array.from({ length: 100 }, (_, i) => ({
761
+ id: `comment${i}`,
762
+ path: 'src/file.js',
763
+ line: i + 1,
764
+ message: `Comment ${i}: ${'a'.repeat(500)}`,
765
+ author: {
766
+ name: 'Reviewer',
767
+ email: 'reviewer@example.com',
768
+ },
769
+ updated: '2024-01-15 11:30:00.000000000',
770
+ unresolved: false,
771
+ })),
772
+ }
773
+
774
+ server.use(
775
+ http.get('*/a/changes/:changeId', () => {
776
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
777
+ }),
778
+ http.get('*/a/changes/:changeId/revisions/current/patch', () => {
779
+ return HttpResponse.text(btoa(largeDiff))
780
+ }),
781
+ http.get('*/a/changes/:changeId/revisions/current/comments', () => {
782
+ return HttpResponse.text(`)]}'\n${JSON.stringify(manyComments)}`)
783
+ }),
784
+ http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
785
+ return HttpResponse.text('context')
786
+ }),
787
+ )
788
+
789
+ const mockConfigLayer = createMockConfigLayer()
790
+ const program = showCommand('12345', { xml: true }).pipe(
791
+ Effect.provide(GerritApiServiceLive),
792
+ Effect.provide(mockConfigLayer),
793
+ )
794
+
795
+ await Effect.runPromise(program)
796
+
797
+ const output = capturedStdout.join('')
798
+
799
+ // Verify output is larger than 64KB
800
+ expect(output.length).toBeGreaterThan(65536)
801
+
802
+ // Verify XML is valid and complete
803
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
804
+ expect(output).toContain('<show_result>')
805
+ expect(output).toContain('<status>success</status>')
806
+ expect(output).toContain('x'.repeat(100000))
807
+ expect(output).toContain('<count>100</count>')
808
+ expect(output).toContain('</show_result>')
809
+
810
+ // Verify last comment is present (proves no truncation)
811
+ expect(output).toContain('Comment 99')
812
+ })
595
813
  })