@aaronshaf/ger 0.2.2 → 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/package.json +1 -1
- package/src/cli/commands/show.ts +138 -64
- package/tests/show-auto-detect.test.ts +20 -2
- package/tests/show.test.ts +226 -8
package/package.json
CHANGED
package/src/cli/commands/show.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { formatDiffPretty } from '@/utils/diff-formatters'
|
|
|
7
7
|
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
8
8
|
import { formatDate } from '@/utils/formatters'
|
|
9
9
|
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
10
|
+
import { writeFileSync } from 'node:fs'
|
|
10
11
|
|
|
11
12
|
interface ShowOptions {
|
|
12
13
|
xml?: boolean
|
|
@@ -184,12 +185,12 @@ const removeUndefined = <T extends Record<string, any>>(obj: T): Partial<T> => {
|
|
|
184
185
|
) as Partial<T>
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
const formatShowJson = (
|
|
188
|
+
const formatShowJson = async (
|
|
188
189
|
changeDetails: ChangeDetails,
|
|
189
190
|
diff: string,
|
|
190
191
|
commentsWithContext: Array<{ comment: CommentInfo; context?: any }>,
|
|
191
192
|
messages: MessageInfo[],
|
|
192
|
-
): void => {
|
|
193
|
+
): Promise<void> => {
|
|
193
194
|
const output = {
|
|
194
195
|
status: 'success',
|
|
195
196
|
change: removeUndefined({
|
|
@@ -242,82 +243,118 @@ const formatShowJson = (
|
|
|
242
243
|
),
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
const jsonOutput = JSON.stringify(output, null, 2) + '\n'
|
|
247
|
+
// Write to stdout and ensure all data is flushed before process exits
|
|
248
|
+
// Using process.stdout.write with drain handling for large payloads
|
|
249
|
+
return new Promise<void>((resolve, reject) => {
|
|
250
|
+
const written = process.stdout.write(jsonOutput, (err) => {
|
|
251
|
+
if (err) {
|
|
252
|
+
reject(err)
|
|
253
|
+
} else {
|
|
254
|
+
resolve()
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (!written) {
|
|
259
|
+
// If write returned false, buffer is full, wait for drain
|
|
260
|
+
process.stdout.once('drain', resolve)
|
|
261
|
+
process.stdout.once('error', reject)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
246
264
|
}
|
|
247
265
|
|
|
248
|
-
const formatShowXml = (
|
|
266
|
+
const formatShowXml = async (
|
|
249
267
|
changeDetails: ChangeDetails,
|
|
250
268
|
diff: string,
|
|
251
269
|
commentsWithContext: Array<{ comment: CommentInfo; context?: any }>,
|
|
252
270
|
messages: MessageInfo[],
|
|
253
|
-
): void => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
): Promise<void> => {
|
|
272
|
+
// Build complete XML output as a single string to avoid multiple writes
|
|
273
|
+
const xmlParts: string[] = []
|
|
274
|
+
xmlParts.push(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
275
|
+
xmlParts.push(`<show_result>`)
|
|
276
|
+
xmlParts.push(` <status>success</status>`)
|
|
277
|
+
xmlParts.push(` <change>`)
|
|
278
|
+
xmlParts.push(` <id>${escapeXML(changeDetails.id)}</id>`)
|
|
279
|
+
xmlParts.push(` <number>${changeDetails.number}</number>`)
|
|
280
|
+
xmlParts.push(` <subject><![CDATA[${sanitizeCDATA(changeDetails.subject)}]]></subject>`)
|
|
281
|
+
xmlParts.push(` <status>${escapeXML(changeDetails.status)}</status>`)
|
|
282
|
+
xmlParts.push(` <project>${escapeXML(changeDetails.project)}</project>`)
|
|
283
|
+
xmlParts.push(` <branch>${escapeXML(changeDetails.branch)}</branch>`)
|
|
284
|
+
xmlParts.push(` <owner>`)
|
|
265
285
|
if (changeDetails.owner.name) {
|
|
266
|
-
|
|
286
|
+
xmlParts.push(` <name><![CDATA[${sanitizeCDATA(changeDetails.owner.name)}]]></name>`)
|
|
267
287
|
}
|
|
268
288
|
if (changeDetails.owner.email) {
|
|
269
|
-
|
|
289
|
+
xmlParts.push(` <email>${escapeXML(changeDetails.owner.email)}</email>`)
|
|
270
290
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
xmlParts.push(` </owner>`)
|
|
292
|
+
xmlParts.push(` <created>${escapeXML(changeDetails.created || '')}</created>`)
|
|
293
|
+
xmlParts.push(` <updated>${escapeXML(changeDetails.updated || '')}</updated>`)
|
|
294
|
+
xmlParts.push(` </change>`)
|
|
295
|
+
xmlParts.push(` <diff><![CDATA[${sanitizeCDATA(diff)}]]></diff>`)
|
|
276
296
|
|
|
277
297
|
// Comments section
|
|
278
|
-
|
|
279
|
-
|
|
298
|
+
xmlParts.push(` <comments>`)
|
|
299
|
+
xmlParts.push(` <count>${commentsWithContext.length}</count>`)
|
|
280
300
|
for (const { comment } of commentsWithContext) {
|
|
281
|
-
|
|
282
|
-
if (comment.id)
|
|
283
|
-
if (comment.path)
|
|
284
|
-
if (comment.line)
|
|
301
|
+
xmlParts.push(` <comment>`)
|
|
302
|
+
if (comment.id) xmlParts.push(` <id>${escapeXML(comment.id)}</id>`)
|
|
303
|
+
if (comment.path) xmlParts.push(` <path><![CDATA[${sanitizeCDATA(comment.path)}]]></path>`)
|
|
304
|
+
if (comment.line) xmlParts.push(` <line>${comment.line}</line>`)
|
|
285
305
|
if (comment.author?.name) {
|
|
286
|
-
|
|
306
|
+
xmlParts.push(` <author><![CDATA[${sanitizeCDATA(comment.author.name)}]]></author>`)
|
|
287
307
|
}
|
|
288
|
-
if (comment.updated)
|
|
308
|
+
if (comment.updated) xmlParts.push(` <updated>${escapeXML(comment.updated)}</updated>`)
|
|
289
309
|
if (comment.message) {
|
|
290
|
-
|
|
310
|
+
xmlParts.push(` <message><![CDATA[${sanitizeCDATA(comment.message)}]]></message>`)
|
|
291
311
|
}
|
|
292
|
-
if (comment.unresolved)
|
|
293
|
-
|
|
312
|
+
if (comment.unresolved) xmlParts.push(` <unresolved>true</unresolved>`)
|
|
313
|
+
xmlParts.push(` </comment>`)
|
|
294
314
|
}
|
|
295
|
-
|
|
315
|
+
xmlParts.push(` </comments>`)
|
|
296
316
|
|
|
297
317
|
// Messages section
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
xmlParts.push(` <messages>`)
|
|
319
|
+
xmlParts.push(` <count>${messages.length}</count>`)
|
|
300
320
|
for (const message of messages) {
|
|
301
|
-
|
|
302
|
-
|
|
321
|
+
xmlParts.push(` <message>`)
|
|
322
|
+
xmlParts.push(` <id>${escapeXML(message.id)}</id>`)
|
|
303
323
|
if (message.author?.name) {
|
|
304
|
-
|
|
324
|
+
xmlParts.push(` <author><![CDATA[${sanitizeCDATA(message.author.name)}]]></author>`)
|
|
305
325
|
}
|
|
306
326
|
if (message.author?._account_id) {
|
|
307
|
-
|
|
327
|
+
xmlParts.push(` <author_id>${message.author._account_id}</author_id>`)
|
|
308
328
|
}
|
|
309
|
-
|
|
329
|
+
xmlParts.push(` <date>${escapeXML(message.date)}</date>`)
|
|
310
330
|
if (message._revision_number) {
|
|
311
|
-
|
|
331
|
+
xmlParts.push(` <revision>${message._revision_number}</revision>`)
|
|
312
332
|
}
|
|
313
333
|
if (message.tag) {
|
|
314
|
-
|
|
334
|
+
xmlParts.push(` <tag>${escapeXML(message.tag)}</tag>`)
|
|
315
335
|
}
|
|
316
|
-
|
|
317
|
-
|
|
336
|
+
xmlParts.push(` <message><![CDATA[${sanitizeCDATA(message.message)}]]></message>`)
|
|
337
|
+
xmlParts.push(` </message>`)
|
|
318
338
|
}
|
|
319
|
-
|
|
320
|
-
|
|
339
|
+
xmlParts.push(` </messages>`)
|
|
340
|
+
xmlParts.push(`</show_result>`)
|
|
341
|
+
|
|
342
|
+
const xmlOutput = xmlParts.join('\n') + '\n'
|
|
343
|
+
// Write to stdout with proper drain handling for large payloads
|
|
344
|
+
return new Promise<void>((resolve, reject) => {
|
|
345
|
+
const written = process.stdout.write(xmlOutput, (err) => {
|
|
346
|
+
if (err) {
|
|
347
|
+
reject(err)
|
|
348
|
+
} else {
|
|
349
|
+
resolve()
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
if (!written) {
|
|
354
|
+
process.stdout.once('drain', resolve)
|
|
355
|
+
process.stdout.once('error', reject)
|
|
356
|
+
}
|
|
357
|
+
})
|
|
321
358
|
}
|
|
322
359
|
|
|
323
360
|
export const showCommand = (
|
|
@@ -358,9 +395,11 @@ export const showCommand = (
|
|
|
358
395
|
|
|
359
396
|
// Format output
|
|
360
397
|
if (options.json) {
|
|
361
|
-
|
|
398
|
+
yield* Effect.promise(() =>
|
|
399
|
+
formatShowJson(changeDetails, diff, commentsWithContext, messages),
|
|
400
|
+
)
|
|
362
401
|
} else if (options.xml) {
|
|
363
|
-
formatShowXml(changeDetails, diff, commentsWithContext, messages)
|
|
402
|
+
yield* Effect.promise(() => formatShowXml(changeDetails, diff, commentsWithContext, messages))
|
|
364
403
|
} else {
|
|
365
404
|
formatShowPretty(changeDetails, diff, commentsWithContext, messages)
|
|
366
405
|
}
|
|
@@ -373,22 +412,57 @@ export const showCommand = (
|
|
|
373
412
|
: String(error)
|
|
374
413
|
|
|
375
414
|
if (options.json) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
415
|
+
return Effect.promise(
|
|
416
|
+
() =>
|
|
417
|
+
new Promise<void>((resolve, reject) => {
|
|
418
|
+
const errorOutput =
|
|
419
|
+
JSON.stringify(
|
|
420
|
+
{
|
|
421
|
+
status: 'error',
|
|
422
|
+
error: errorMessage,
|
|
423
|
+
},
|
|
424
|
+
null,
|
|
425
|
+
2,
|
|
426
|
+
) + '\n'
|
|
427
|
+
const written = process.stdout.write(errorOutput, (err) => {
|
|
428
|
+
if (err) {
|
|
429
|
+
reject(err)
|
|
430
|
+
} else {
|
|
431
|
+
resolve()
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
if (!written) {
|
|
436
|
+
// Wait for drain if buffer is full
|
|
437
|
+
process.stdout.once('drain', resolve)
|
|
438
|
+
process.stdout.once('error', reject)
|
|
439
|
+
}
|
|
440
|
+
}),
|
|
385
441
|
)
|
|
386
442
|
} else if (options.xml) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
return Effect.promise(
|
|
444
|
+
() =>
|
|
445
|
+
new Promise<void>((resolve, reject) => {
|
|
446
|
+
const xmlError =
|
|
447
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
448
|
+
`<show_result>\n` +
|
|
449
|
+
` <status>error</status>\n` +
|
|
450
|
+
` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>\n` +
|
|
451
|
+
`</show_result>\n`
|
|
452
|
+
const written = process.stdout.write(xmlError, (err) => {
|
|
453
|
+
if (err) {
|
|
454
|
+
reject(err)
|
|
455
|
+
} else {
|
|
456
|
+
resolve()
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
if (!written) {
|
|
461
|
+
process.stdout.once('drain', resolve)
|
|
462
|
+
process.stdout.once('error', reject)
|
|
463
|
+
}
|
|
464
|
+
}),
|
|
465
|
+
)
|
|
392
466
|
} else {
|
|
393
467
|
console.error(`✗ Error: ${errorMessage}`)
|
|
394
468
|
}
|
|
@@ -75,6 +75,7 @@ ${JSON.stringify(mockChange)}`)
|
|
|
75
75
|
|
|
76
76
|
let capturedLogs: string[] = []
|
|
77
77
|
let capturedErrors: string[] = []
|
|
78
|
+
let capturedStdout: string[] = []
|
|
78
79
|
|
|
79
80
|
const mockConsoleLog = mock((...args: any[]) => {
|
|
80
81
|
capturedLogs.push(args.join(' '))
|
|
@@ -83,8 +84,19 @@ const mockConsoleError = mock((...args: any[]) => {
|
|
|
83
84
|
capturedErrors.push(args.join(' '))
|
|
84
85
|
})
|
|
85
86
|
|
|
87
|
+
// Mock process.stdout.write to capture JSON/XML output and handle callbacks
|
|
88
|
+
const mockStdoutWrite = mock((chunk: any, callback?: any) => {
|
|
89
|
+
capturedStdout.push(String(chunk))
|
|
90
|
+
// Call the callback synchronously if provided
|
|
91
|
+
if (typeof callback === 'function') {
|
|
92
|
+
callback()
|
|
93
|
+
}
|
|
94
|
+
return true
|
|
95
|
+
})
|
|
96
|
+
|
|
86
97
|
const originalConsoleLog = console.log
|
|
87
98
|
const originalConsoleError = console.error
|
|
99
|
+
const originalStdoutWrite = process.stdout.write
|
|
88
100
|
|
|
89
101
|
let spawnSpy: ReturnType<typeof spyOn>
|
|
90
102
|
|
|
@@ -94,20 +106,26 @@ beforeAll(() => {
|
|
|
94
106
|
console.log = mockConsoleLog
|
|
95
107
|
// @ts-ignore
|
|
96
108
|
console.error = mockConsoleError
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
process.stdout.write = mockStdoutWrite
|
|
97
111
|
})
|
|
98
112
|
|
|
99
113
|
afterAll(() => {
|
|
100
114
|
server.close()
|
|
101
115
|
console.log = originalConsoleLog
|
|
102
116
|
console.error = originalConsoleError
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
process.stdout.write = originalStdoutWrite
|
|
103
119
|
})
|
|
104
120
|
|
|
105
121
|
afterEach(() => {
|
|
106
122
|
server.resetHandlers()
|
|
107
123
|
mockConsoleLog.mockClear()
|
|
108
124
|
mockConsoleError.mockClear()
|
|
125
|
+
mockStdoutWrite.mockClear()
|
|
109
126
|
capturedLogs = []
|
|
110
127
|
capturedErrors = []
|
|
128
|
+
capturedStdout = []
|
|
111
129
|
|
|
112
130
|
if (spawnSpy) {
|
|
113
131
|
spawnSpy.mockRestore()
|
|
@@ -184,7 +202,7 @@ Change-Id: If5a3ae8cb5a107e187447802358417f311d0c4b1`
|
|
|
184
202
|
|
|
185
203
|
await resultPromise
|
|
186
204
|
|
|
187
|
-
const output =
|
|
205
|
+
const output = capturedStdout.join('')
|
|
188
206
|
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
189
207
|
expect(output).toContain('<show_result>')
|
|
190
208
|
expect(output).toContain('<status>success</status>')
|
|
@@ -297,7 +315,7 @@ This commit has no Change-ID footer.`
|
|
|
297
315
|
|
|
298
316
|
await resultPromise
|
|
299
317
|
|
|
300
|
-
const output =
|
|
318
|
+
const output = capturedStdout.join('')
|
|
301
319
|
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
302
320
|
expect(output).toContain('<show_result>')
|
|
303
321
|
expect(output).toContain('<status>error</status>')
|
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
|
})
|