@aaronshaf/ger
Version:
Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS
432 lines (375 loc) • 12.4 kB
text/typescript
import { test, expect, describe, beforeAll, afterEach, afterAll } from 'bun:test'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { Effect, Layer } from 'effect'
import { ConfigService } from '@/services/config'
import { GerritApiServiceLive } from '@/api/gerrit'
import { commentCommand } from '@/cli/commands/comment'
import { EventEmitter } from 'node:events'
import { createMockConfigService } from './helpers/config-mock'
// Create a mock process.stdin for testing
class MockProcessStdin extends EventEmitter {
isTTY = false
readable = true
emit(event: string, data?: any): boolean {
if (event === 'data') {
super.emit('data', Buffer.from(data))
// Automatically emit 'end' after data
setTimeout(() => super.emit('end'), 0)
return true
}
return super.emit(event, data)
}
}
const server = setupServer()
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('comment command - advanced batch features', () => {
const mockProcessStdin = new MockProcessStdin()
test('should handle batch comments with side parameter', async () => {
const originalStdin = process.stdin
Object.defineProperty(process, 'stdin', {
value: mockProcessStdin,
configurable: true,
})
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n{
"id": "test-project~main~I123abc",
"_number": 12345,
"project": "test-project",
"branch": "main",
"change_id": "I123abc",
"subject": "Test change",
"status": "NEW",
"created": "2024-01-15 10:00:00.000000000",
"updated": "2024-01-15 10:00:00.000000000"
}`)
}),
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
const body = (await request.json()) as {
message?: string
comments?: Record<string, unknown[]>
}
expect(body.comments).toBeDefined()
const fileComments = body.comments?.['src/main.js'] as Array<{
line?: number
side?: string
message: string
}>
expect(fileComments?.length).toBe(2)
expect(fileComments?.[0]).toMatchObject({
line: 10,
side: 'PARENT',
message: 'Why was this removed?',
})
expect(fileComments?.[1]).toMatchObject({
line: 10,
side: 'REVISION',
message: 'Good improvement',
})
return HttpResponse.json({})
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = commentCommand('12345', { batch: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
// Simulate stdin data with side parameter
setTimeout(() => {
mockProcessStdin.emit(
'data',
JSON.stringify([
{ file: 'src/main.js', line: 10, message: 'Why was this removed?', side: 'PARENT' },
{ file: 'src/main.js', line: 10, message: 'Good improvement', side: 'REVISION' },
]),
)
}, 10)
await Effect.runPromise(program)
// Restore process.stdin
Object.defineProperty(process, 'stdin', {
value: originalStdin,
configurable: true,
})
})
test('should handle batch comments with range parameter', async () => {
const originalStdin = process.stdin
Object.defineProperty(process, 'stdin', {
value: mockProcessStdin,
configurable: true,
})
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n{
"id": "test-project~main~I123abc",
"_number": 12345,
"project": "test-project",
"branch": "main",
"change_id": "I123abc",
"subject": "Test change",
"status": "NEW"
}`)
}),
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
const body = (await request.json()) as {
comments?: Record<string, unknown[]>
}
const fileComments = body.comments?.['src/Calculator.java'] as Array<{
range?: {
start_line: number
end_line: number
start_character?: number
end_character?: number
}
message: string
}>
expect(fileComments?.length).toBe(3)
// Multi-line range comment
expect(fileComments?.[0]).toMatchObject({
range: {
start_line: 50,
end_line: 55,
},
message: 'This block needs refactoring',
})
// Character-specific range
expect(fileComments?.[1]).toMatchObject({
range: {
start_line: 10,
start_character: 8,
end_line: 10,
end_character: 25,
},
message: 'Variable name is confusing',
})
// Mixed with regular line comment
expect(fileComments?.[2]).toMatchObject({
line: 42,
message: 'Add null check here',
})
return HttpResponse.json({})
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = commentCommand('12345', { batch: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
// Simulate stdin data with range parameter
setTimeout(() => {
mockProcessStdin.emit(
'data',
JSON.stringify([
{
file: 'src/Calculator.java',
range: { start_line: 50, end_line: 55 },
message: 'This block needs refactoring',
},
{
file: 'src/Calculator.java',
range: { start_line: 10, start_character: 8, end_line: 10, end_character: 25 },
message: 'Variable name is confusing',
},
{
file: 'src/Calculator.java',
line: 42,
message: 'Add null check here',
},
]),
)
}, 10)
await Effect.runPromise(program)
// Restore process.stdin
Object.defineProperty(process, 'stdin', {
value: originalStdin,
configurable: true,
})
})
test('should handle batch comments with both side and range', async () => {
const originalStdin = process.stdin
Object.defineProperty(process, 'stdin', {
value: mockProcessStdin,
configurable: true,
})
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n{
"id": "test-project~main~I123abc",
"_number": 12345,
"project": "test-project",
"branch": "main",
"change_id": "I123abc",
"subject": "Test change",
"status": "NEW"
}`)
}),
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
const body = (await request.json()) as {
comments?: Record<string, unknown[]>
}
const fileComments = body.comments?.['src/Service.java'] as Array<{
range?: {
start_line: number
end_line: number
}
side?: string
message: string
unresolved?: boolean
}>
expect(fileComments?.length).toBe(2)
// Range comment on PARENT side
expect(fileComments?.[0]).toMatchObject({
range: {
start_line: 20,
end_line: 35,
},
side: 'PARENT',
message: 'Why was this error handling removed?',
unresolved: true,
})
// Range comment on REVISION side
expect(fileComments?.[1]).toMatchObject({
range: {
start_line: 20,
end_line: 35,
},
side: 'REVISION',
message: 'New error handling looks good, but consider extracting',
})
return HttpResponse.json({})
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = commentCommand('12345', { batch: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
// Simulate stdin data with both range and side
setTimeout(() => {
mockProcessStdin.emit(
'data',
JSON.stringify([
{
file: 'src/Service.java',
range: { start_line: 20, end_line: 35 },
side: 'PARENT',
message: 'Why was this error handling removed?',
unresolved: true,
},
{
file: 'src/Service.java',
range: { start_line: 20, end_line: 35 },
side: 'REVISION',
message: 'New error handling looks good, but consider extracting',
},
]),
)
}, 10)
await Effect.runPromise(program)
// Restore process.stdin
Object.defineProperty(process, 'stdin', {
value: originalStdin,
configurable: true,
})
})
test('should validate side parameter values', async () => {
const originalStdin = process.stdin
Object.defineProperty(process, 'stdin', {
value: mockProcessStdin,
configurable: true,
})
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = commentCommand('12345', { batch: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
// Simulate invalid side value
setTimeout(() => {
mockProcessStdin.emit(
'data',
JSON.stringify([
{
file: 'src/main.js',
line: 10,
message: 'Test',
side: 'INVALID', // Invalid side value
},
]),
)
}, 10)
await expect(Effect.runPromise(program)).rejects.toThrow('Invalid batch input format')
// Restore process.stdin
Object.defineProperty(process, 'stdin', {
value: originalStdin,
configurable: true,
})
})
test('should require either line or range but not both', async () => {
const originalStdin = process.stdin
Object.defineProperty(process, 'stdin', {
value: mockProcessStdin,
configurable: true,
})
server.use(
http.get('*/a/changes/:changeId', () => {
return HttpResponse.text(`)]}'\n{
"id": "test-project~main~I123abc",
"_number": 12345,
"project": "test-project",
"branch": "main",
"change_id": "I123abc",
"subject": "Test change",
"status": "NEW"
}`)
}),
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
const body = (await request.json()) as {
comments?: Record<string, unknown[]>
}
const fileComments = body.comments?.['src/main.js'] as Array<{
line?: number
range?: unknown
message: string
}>
// Should use range when both are provided (range takes precedence)
expect(fileComments?.[0]).toMatchObject({
range: {
start_line: 10,
end_line: 15,
},
message: 'Test comment',
})
// line should NOT be included when range is present (Gerrit API preference)
expect(fileComments?.[0].line).toBeUndefined()
return HttpResponse.json({})
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = commentCommand('12345', { batch: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
// Both line and range provided - should work
setTimeout(() => {
mockProcessStdin.emit(
'data',
JSON.stringify([
{
file: 'src/main.js',
line: 10, // Will be included
range: { start_line: 10, end_line: 15 }, // Takes precedence
message: 'Test comment',
},
]),
)
}, 10)
await Effect.runPromise(program)
// Restore process.stdin
Object.defineProperty(process, 'stdin', {
value: originalStdin,
configurable: true,
})
})
})