@aaronshaf/ger
Version:
Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS
940 lines (803 loc) • 31.3 kB
text/typescript
import { describe, test, expect, beforeAll, afterAll, afterEach, mock } from 'bun:test'
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { Effect, Layer } from 'effect'
import { showCommand } from '@/cli/commands/show'
import { GerritApiServiceLive } from '@/api/gerrit'
import { ConfigService } from '@/services/config'
import { generateMockChange } from '@/test-utils/mock-generator'
import type { MessageInfo } from '@/schemas/gerrit'
import { createMockConfigService } from './helpers/config-mock'
const server = setupServer(
// Default handler for auth check
http.get('*/a/accounts/self', ({ request }) => {
const auth = request.headers.get('Authorization')
if (!auth || !auth.startsWith('Basic ')) {
return HttpResponse.text('Unauthorized', { status: 401 })
}
return HttpResponse.json({
_account_id: 1000,
name: 'Test User',
email: 'test@example.com',
})
}),
)
// Store captured output
let capturedLogs: string[] = []
let capturedErrors: string[] = []
let capturedStdout: string[] = []
// Mock console.log and console.error
const mockConsoleLog = mock((...args: any[]) => {
capturedLogs.push(args.join(' '))
})
const mockConsoleError = mock((...args: any[]) => {
capturedErrors.push(args.join(' '))
})
// Mock process.stdout.write to capture JSON output and handle callbacks
const mockStdoutWrite = mock((chunk: any, callback?: any) => {
capturedStdout.push(String(chunk))
// Call the callback synchronously if provided
if (typeof callback === 'function') {
callback()
}
return true
})
// Store original methods
const originalConsoleLog = console.log
const originalConsoleError = console.error
const originalStdoutWrite = process.stdout.write
beforeAll(() => {
server.listen({ onUnhandledRequest: 'bypass' })
// @ts-ignore
console.log = mockConsoleLog
// @ts-ignore
console.error = mockConsoleError
// @ts-ignore
process.stdout.write = mockStdoutWrite
})
afterAll(() => {
server.close()
console.log = originalConsoleLog
console.error = originalConsoleError
// @ts-ignore
process.stdout.write = originalStdoutWrite
})
afterEach(() => {
server.resetHandlers()
mockConsoleLog.mockClear()
mockConsoleError.mockClear()
mockStdoutWrite.mockClear()
capturedLogs = []
capturedErrors = []
capturedStdout = []
})
describe('show command', () => {
const mockChange = generateMockChange({
_number: 12345,
change_id: 'I123abc456def',
subject: 'Fix authentication bug',
status: 'NEW',
project: 'test-project',
branch: 'main',
created: '2024-01-15 10:00:00.000000000',
updated: '2024-01-15 12:00:00.000000000',
owner: {
_account_id: 1001,
name: 'John Doe',
email: 'john@example.com',
},
reviewers: {
REVIEWER: [
{
_account_id: 2001,
name: 'Jane Reviewer',
email: 'jane.reviewer@example.com',
username: 'jreviewer',
},
{
email: 'second.reviewer@example.com',
username: 'sreviewer',
},
],
CC: [
{
_account_id: 2003,
name: 'Team Observer',
email: 'observer@example.com',
},
],
},
})
const mockDiff = `--- a/src/auth.js
+++ b/src/auth.js
@@ -10,7 +10,8 @@ function authenticate(user) {
if (!user) {
- return false
+ throw new Error('User required')
}
+ // Added validation
return validateUser(user)
}`
const mockComments = {
'src/auth.js': [
{
id: 'comment1',
path: 'src/auth.js',
line: 12,
message: 'Good improvement!',
author: {
name: 'Jane Reviewer',
email: 'jane@example.com',
},
updated: '2024-01-15 11:30:00.000000000',
unresolved: false,
},
{
id: 'comment2',
path: 'src/auth.js',
line: 14,
message: 'Consider adding JSDoc',
author: {
name: 'Bob Reviewer',
email: 'bob@example.com',
},
updated: '2024-01-15 11:45:00.000000000',
unresolved: true,
},
],
'/COMMIT_MSG': [
{
id: 'comment3',
path: '/COMMIT_MSG',
line: 1,
message: 'Clear commit message',
author: {
name: 'Alice Lead',
email: 'alice@example.com',
},
updated: '2024-01-15 11:00:00.000000000',
unresolved: false,
},
],
}
const setupMockHandlers = () => {
server.use(
// Get change details
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
}),
// Get diff (returns base64-encoded content)
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(btoa(mockDiff))
}),
// Get comments
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
}),
// Get file diff for context (optional, may fail gracefully)
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
return HttpResponse.text(mockDiff)
}),
)
}
const createMockConfigLayer = () => Layer.succeed(ConfigService, createMockConfigService())
test('should display comprehensive change information in pretty format', async () => {
setupMockHandlers()
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedLogs.join('\n')
// Check that all sections are present
expect(output).toContain('📋 Change 12345: Fix authentication bug')
expect(output).toContain('📝 Details:')
expect(output).toContain('Project: test-project')
expect(output).toContain('Branch: main')
expect(output).toContain('Status: NEW')
expect(output).toContain('Owner: John Doe')
expect(output).toContain('Reviewers: Jane Reviewer <jane.reviewer@example.com>')
expect(output).toContain('second.reviewer@example.com')
expect(output).toContain('CCs: Team Observer <observer@example.com>')
expect(output).toContain('Change-Id: I123abc456def')
expect(output).toContain('🔍 Diff:')
expect(output).toContain('💬 Inline Comments:')
// Check diff content is included
expect(output).toContain('src/auth.js')
expect(output).toContain('authenticate(user)')
// Check comments are included
expect(output).toContain('Good improvement!')
expect(output).toContain('Consider adding JSDoc')
expect(output).toContain('Clear commit message')
})
test('should output XML format when --xml flag is used', async () => {
setupMockHandlers()
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { xml: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
expect(output).toContain('<show_result>')
expect(output).toContain('<status>success</status>')
expect(output).toContain('<change>')
expect(output).toContain('<id>I123abc456def</id>')
expect(output).toContain('<number>12345</number>')
expect(output).toContain('<subject><![CDATA[Fix authentication bug]]></subject>')
expect(output).toContain('<status>NEW</status>')
expect(output).toContain('<project>test-project</project>')
expect(output).toContain('<branch>main</branch>')
expect(output).toContain('<owner>')
expect(output).toContain('<name><![CDATA[John Doe]]></name>')
expect(output).toContain('<email>john@example.com</email>')
expect(output).toContain('<reviewers>')
expect(output).toContain('<count>2</count>')
expect(output).toContain('<name><![CDATA[Jane Reviewer]]></name>')
expect(output).toContain('<ccs>')
expect(output).toContain('<count>1</count>')
expect(output).toContain('<name><![CDATA[Team Observer]]></name>')
expect(output).not.toContain('<account_id>undefined</account_id>')
expect(output).toContain('<diff><![CDATA[')
expect(output).toContain('<comments>')
expect(output).toContain('<count>3</count>')
expect(output).toContain('</show_result>')
})
test('should handle API errors gracefully in pretty format', async () => {
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedErrors.join('\n')
expect(output).toContain('✗ Error:')
// The error message will be from the network layer
expect(output.length).toBeGreaterThan(0)
})
test('should handle API errors gracefully in XML format', async () => {
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { xml: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
expect(output).toContain('<show_result>')
expect(output).toContain('<status>error</status>')
expect(output).toContain('<error><![CDATA[')
expect(output).toContain('</show_result>')
})
test('should properly escape XML special characters', async () => {
const changeWithSpecialChars = generateMockChange({
_number: 12345,
change_id: 'I123abc456def',
subject: 'Fix "quotes" & <tags> in auth',
project: 'test-project',
branch: 'feature/fix&improve',
owner: {
_account_id: 1002,
name: 'User <with> & "special" chars',
email: 'user@example.com',
},
})
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithSpecialChars)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('diff content')
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n{}`)
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { xml: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
expect(output).toContain('<subject><![CDATA[Fix "quotes" & <tags> in auth]]></subject>')
expect(output).toContain('<branch>feature/fix&improve</branch>')
expect(output).toContain('<name><![CDATA[User <with> & "special" chars]]></name>')
})
test('should handle mixed file and commit message comments', async () => {
setupMockHandlers()
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedLogs.join('\n')
// Should show comments from both files and commit message
expect(output).toContain('Good improvement!')
expect(output).toContain('Consider adding JSDoc')
expect(output).toContain('Clear commit message')
// Commit message path should be renamed
expect(output).toContain('Commit Message')
expect(output).not.toContain('/COMMIT_MSG')
})
test('should handle changes with missing optional fields', async () => {
const minimalChange = generateMockChange({
_number: 12345,
change_id: 'I123abc456def',
subject: 'Minimal change',
status: 'NEW',
project: 'test-project',
branch: 'main',
owner: {
_account_id: 1003,
email: 'user@example.com',
},
})
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(minimalChange)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('minimal diff')
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n{}`)
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedLogs.join('\n')
expect(output).toContain('📋 Change 12345: Minimal change')
expect(output).toContain('Owner: user@example.com') // Should fallback to email
})
test('should display review activity messages', async () => {
const mockChange = generateMockChange({
_number: 12345,
subject: 'Fix authentication bug',
})
const mockMessages: MessageInfo[] = [
{
id: 'msg1',
message: 'Patch Set 2: Code-Review+2',
author: { _account_id: 1001, name: 'Jane Reviewer' },
date: '2024-01-15 11:30:00.000000000',
_revision_number: 2,
},
{
id: 'msg2',
message: 'Patch Set 2: Verified+1\\n\\nBuild Successful',
author: { _account_id: 1002, name: 'Jenkins Bot' },
date: '2024-01-15 11:31:00.000000000',
_revision_number: 2,
},
{
id: 'msg3',
message: 'Uploaded patch set 1.',
author: { _account_id: 1000, name: 'Author' },
date: '2024-01-15 11:29:00.000000000',
tag: 'autogenerated:gerrit:newPatchSet',
_revision_number: 1,
},
]
server.use(
http.get('*/a/changes/:changeId', ({ request }) => {
const url = new URL(request.url)
if (url.searchParams.get('o') === 'MESSAGES') {
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: mockMessages })}`)
}
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('diff content')
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n{}`)
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedLogs.join('\n')
// Should display review activity section
expect(output).toContain('📝 Review Activity:')
expect(output).toContain('Jane Reviewer')
expect(output).toContain('Code-Review+2')
expect(output).toContain('Jenkins Bot')
expect(output).toContain('Build Successful')
// Should filter out autogenerated messages
expect(output).not.toContain('Uploaded patch set')
})
test('should output JSON format when --json flag is used', async () => {
setupMockHandlers()
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { json: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
// Parse JSON to verify it's valid
const parsed = JSON.parse(output)
expect(parsed.status).toBe('success')
expect(parsed.change.id).toBe('I123abc456def')
expect(parsed.change.number).toBe(12345)
expect(parsed.change.subject).toBe('Fix authentication bug')
expect(parsed.change.status).toBe('NEW')
expect(parsed.change.project).toBe('test-project')
expect(parsed.change.branch).toBe('main')
expect(parsed.change.owner.name).toBe('John Doe')
expect(parsed.change.owner.email).toBe('john@example.com')
expect(Array.isArray(parsed.change.reviewers)).toBe(true)
expect(parsed.change.reviewers.length).toBe(2)
expect(parsed.change.reviewers[0].name).toBe('Jane Reviewer')
expect(parsed.change.reviewers[1].email).toBe('second.reviewer@example.com')
expect(parsed.change.reviewers[1].account_id).toBeUndefined()
expect(Array.isArray(parsed.change.ccs)).toBe(true)
expect(parsed.change.ccs.length).toBe(1)
expect(parsed.change.ccs[0].name).toBe('Team Observer')
// Check diff is present
expect(parsed.diff).toContain('src/auth.js')
expect(parsed.diff).toContain('authenticate(user)')
// Check comments array
expect(Array.isArray(parsed.comments)).toBe(true)
expect(parsed.comments.length).toBe(3)
expect(parsed.comments[0].message).toContain('Clear commit message')
expect(parsed.comments[1].message).toBe('Good improvement!')
expect(parsed.comments[2].message).toBe('Consider adding JSDoc')
// Check messages array (should be empty for this test)
expect(Array.isArray(parsed.messages)).toBe(true)
})
test('should handle API errors gracefully in JSON format', async () => {
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { json: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
// Parse JSON to verify it's valid
const parsed = JSON.parse(output)
expect(parsed.status).toBe('error')
expect(parsed.error).toBeDefined()
expect(typeof parsed.error).toBe('string')
})
test('should sort comments by date in ascending order in XML output', async () => {
setupMockHandlers()
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { xml: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
// Extract comment sections to verify order
const commentMatches = output.matchAll(
/<comment>[\s\S]*?<updated>(.*?)<\/updated>[\s\S]*?<message><!\[CDATA\[(.*?)\]\]><\/message>[\s\S]*?<\/comment>/g,
)
const comments = Array.from(commentMatches).map((match) => ({
updated: match[1],
message: match[2],
}))
// Should have 3 comments
expect(comments.length).toBe(3)
// Comments should be in ascending date order (oldest first)
expect(comments[0].updated).toBe('2024-01-15 11:00:00.000000000')
expect(comments[0].message).toBe('Clear commit message')
expect(comments[1].updated).toBe('2024-01-15 11:30:00.000000000')
expect(comments[1].message).toBe('Good improvement!')
expect(comments[2].updated).toBe('2024-01-15 11:45:00.000000000')
expect(comments[2].message).toBe('Consider adding JSDoc')
})
test('should include messages in JSON output', async () => {
const mockChange = generateMockChange({
_number: 12345,
subject: 'Fix authentication bug',
})
const mockMessages: MessageInfo[] = [
{
id: 'msg1',
message: 'Patch Set 2: Verified-1\\n\\nBuild Failed https://jenkins.example.com/job/123',
author: { _account_id: 1001, name: 'Jenkins Bot' },
date: '2024-01-15 11:30:00.000000000',
_revision_number: 2,
},
]
server.use(
http.get('*/a/changes/:changeId', ({ request }) => {
const url = new URL(request.url)
if (url.searchParams.get('o') === 'MESSAGES') {
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: mockMessages })}`)
}
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('diff content')
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n{}`)
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { json: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
const parsed = JSON.parse(output)
expect(parsed.messages).toBeDefined()
expect(Array.isArray(parsed.messages)).toBe(true)
expect(parsed.messages.length).toBe(1)
expect(parsed.messages[0].message).toContain('Build Failed')
expect(parsed.messages[0].message).toContain('https://jenkins.example.com')
expect(parsed.messages[0].author.name).toBe('Jenkins Bot')
expect(parsed.messages[0].revision).toBe(2)
})
test('should fetch reviewers from listChanges when getChange lacks reviewer data', async () => {
let listChangesOptions: string[] = []
let listChangesQuery = ''
const changeWithoutReviewers = {
...mockChange,
reviewers: undefined,
}
server.use(
http.get('*/a/changes/:changeId', ({ request }) => {
const url = new URL(request.url)
if (url.searchParams.get('o') === 'MESSAGES') {
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: [] })}`)
}
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithoutReviewers)}`)
}),
http.get('*/a/changes/', ({ request }) => {
const url = new URL(request.url)
listChangesOptions = url.searchParams.getAll('o')
listChangesQuery = url.searchParams.get('q') || ''
return HttpResponse.text(`)]}'\n${JSON.stringify([mockChange])}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(btoa(mockDiff))
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
expect(listChangesQuery).toBe('change:12345')
expect(listChangesOptions).toContain('LABELS')
expect(listChangesOptions).toContain('DETAILED_LABELS')
expect(listChangesOptions).toContain('DETAILED_ACCOUNTS')
})
test('should not fetch listChanges when reviewer data is explicitly present but empty', async () => {
let listChangesCalled = false
const changeWithEmptyReviewerLists = {
...mockChange,
reviewers: {
REVIEWER: [],
CC: [],
},
}
server.use(
http.get('*/a/changes/:changeId', ({ request }) => {
const url = new URL(request.url)
if (url.searchParams.get('o') === 'MESSAGES') {
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: [] })}`)
}
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithEmptyReviewerLists)}`)
}),
http.get('*/a/changes/', () => {
listChangesCalled = true
return HttpResponse.text(`)]}'\n[]`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(btoa(mockDiff))
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
expect(listChangesCalled).toBe(false)
})
test('should handle large JSON output without truncation', async () => {
// Create a large diff to simulate output > 64KB
const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
const mockChange = generateMockChange({
_number: 12345,
subject: 'Large change with extensive diff',
})
// Create many comments to increase JSON size
const manyComments: Record<string, any[]> = {
'src/file.js': Array.from({ length: 100 }, (_, i) => ({
id: `comment${i}`,
path: 'src/file.js',
line: i + 1,
message: `Comment ${i}: ${'a'.repeat(500)}`, // Make comments substantial
author: {
name: 'Reviewer',
email: 'reviewer@example.com',
},
updated: '2024-01-15 11:30:00.000000000',
unresolved: false,
})),
}
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(btoa(largeDiff))
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(manyComments)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
return HttpResponse.text('context')
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { json: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
// Verify output is larger than 64KB (the previous truncation point)
expect(output.length).toBeGreaterThan(65536)
// Verify JSON is valid and complete
const parsed = JSON.parse(output)
expect(parsed.status).toBe('success')
expect(parsed.diff).toContain('x'.repeat(100000))
expect(parsed.comments.length).toBe(100)
// Verify last comment is present (proves no truncation)
const lastComment = parsed.comments[parsed.comments.length - 1]
expect(lastComment.message).toContain('Comment 99')
})
test('should handle stdout drain event when buffer is full', async () => {
setupMockHandlers()
// Store original stdout.write
const originalStdoutWrite = process.stdout.write
let drainCallback: (() => void) | null = null
let errorCallback: ((err: Error) => void) | null = null
let writeCallbackFn: ((err?: Error) => void) | null = null
// Mock stdout.write to simulate full buffer
const mockWrite = mock((chunk: any, callback?: any) => {
capturedStdout.push(String(chunk))
writeCallbackFn = callback
// Return false to simulate full buffer
return false
})
// Mock stdout.once to capture drain and error listeners
const mockOnce = mock((event: string, callback: any) => {
if (event === 'drain') {
drainCallback = callback
// Simulate drain event after a short delay
setTimeout(() => {
if (drainCallback) {
drainCallback()
if (writeCallbackFn) {
writeCallbackFn()
}
}
}, 10)
} else if (event === 'error') {
errorCallback = callback
}
return process.stdout
})
// Apply mocks
// @ts-ignore
process.stdout.write = mockWrite
// @ts-ignore
process.stdout.once = mockOnce
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { json: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
// Restore original stdout.write
// @ts-ignore
process.stdout.write = originalStdoutWrite
// Verify that write returned false (buffer full)
expect(mockWrite).toHaveBeenCalled()
// Verify that drain listener was registered
expect(mockOnce).toHaveBeenCalledWith('drain', expect.any(Function))
// Verify that error listener was registered for robustness
expect(mockOnce).toHaveBeenCalledWith('error', expect.any(Function))
// Verify output is still valid JSON despite drain handling
const output = capturedStdout.join('')
const parsed = JSON.parse(output)
expect(parsed.status).toBe('success')
expect(parsed.change.id).toBe('I123abc456def')
})
test('should handle large XML output without truncation', async () => {
// Create a large diff to simulate output > 64KB
const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
const mockChange = generateMockChange({
_number: 12345,
subject: 'Large change with extensive diff',
})
// Create many comments to increase XML size
const manyComments: Record<string, any[]> = {
'src/file.js': Array.from({ length: 100 }, (_, i) => ({
id: `comment${i}`,
path: 'src/file.js',
line: i + 1,
message: `Comment ${i}: ${'a'.repeat(500)}`,
author: {
name: 'Reviewer',
email: 'reviewer@example.com',
},
updated: '2024-01-15 11:30:00.000000000',
unresolved: false,
})),
}
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(btoa(largeDiff))
}),
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(manyComments)}`)
}),
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
return HttpResponse.text('context')
}),
)
const mockConfigLayer = createMockConfigLayer()
const program = showCommand('12345', { xml: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await Effect.runPromise(program)
const output = capturedStdout.join('')
// Verify output is larger than 64KB
expect(output.length).toBeGreaterThan(65536)
// Verify XML is valid and complete
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
expect(output).toContain('<show_result>')
expect(output).toContain('<status>success</status>')
expect(output).toContain('x'.repeat(100000))
expect(output).toContain('<count>100</count>')
expect(output).toContain('</show_result>')
// Verify last comment is present (proves no truncation)
expect(output).toContain('Comment 99')
})
})