@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.
- package/EXAMPLES.md +409 -0
- package/index.ts +219 -0
- package/package.json +46 -1
- package/src/cli/commands/show.ts +138 -64
- package/src/services/git-worktree.ts +14 -8
- package/src/utils/index.ts +55 -0
- package/tests/show-auto-detect.test.ts +20 -2
- package/tests/show.test.ts +226 -8
package/tests/show.test.ts
CHANGED
|
@@ -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
|
-
//
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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&improve</branch>')
|
|
@@ -450,7 +468,7 @@ describe('show command', () => {
|
|
|
450
468
|
|
|
451
469
|
await Effect.runPromise(program)
|
|
452
470
|
|
|
453
|
-
const output =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
})
|