@aaronshaf/ger 0.1.0
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/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.eslintrc.js +12 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +78 -0
- package/.github/workflows/claude.yml +64 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +103 -0
- package/DEVELOPMENT.md +361 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/bin/ger +3 -0
- package/biome.json +36 -0
- package/bun.lock +688 -0
- package/bunfig.toml +8 -0
- package/oxlint.json +24 -0
- package/package.json +55 -0
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/src/api/gerrit.ts +466 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/review.ts +593 -0
- package/src/cli/commands/setup.ts +230 -0
- package/src/cli/commands/show.ts +303 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +420 -0
- package/src/prompts/default-review.md +80 -0
- package/src/prompts/system-inline-review.md +88 -0
- package/src/prompts/system-overall-review.md +152 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +75 -0
- package/src/schemas/gerrit.ts +455 -0
- package/src/services/ai-enhanced.ts +167 -0
- package/src/services/ai.ts +182 -0
- package/src/services/config.test.ts +414 -0
- package/src/services/config.ts +206 -0
- package/src/test-utils/mock-generator.ts +73 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +123 -0
- package/src/utils/url-parser.ts +91 -0
- package/tests/abandon.test.ts +163 -0
- package/tests/ai-service.test.ts +489 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +707 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +318 -0
- package/tests/mocks/fetch-mock.ts +139 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/review.test.ts +669 -0
- package/tests/setup.ts +13 -0
- package/tests/show.test.ts +439 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/prompt-helpers.test.ts +175 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { Effect, Layer } from 'effect'
|
|
3
|
+
import { HttpResponse, http } from 'msw'
|
|
4
|
+
import { setupServer } from 'msw/node'
|
|
5
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
6
|
+
import { commentCommand } from '@/cli/commands/comment'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
|
|
9
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
10
|
+
// Create MSW server
|
|
11
|
+
const server = setupServer(
|
|
12
|
+
// Default handler for auth check
|
|
13
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
14
|
+
const auth = request.headers.get('Authorization')
|
|
15
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
16
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
17
|
+
}
|
|
18
|
+
return HttpResponse.json({
|
|
19
|
+
_account_id: 1000,
|
|
20
|
+
name: 'Test User',
|
|
21
|
+
email: 'test@example.com',
|
|
22
|
+
})
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
describe('comment command', () => {
|
|
27
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
28
|
+
let mockConsoleError: ReturnType<typeof mock>
|
|
29
|
+
let mockProcessStdin: {
|
|
30
|
+
on: ReturnType<typeof mock>
|
|
31
|
+
emit: (data: string) => void
|
|
32
|
+
dataCallback?: (...args: unknown[]) => void
|
|
33
|
+
endCallback?: (...args: unknown[]) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
server.close()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
server.resetHandlers()
|
|
46
|
+
|
|
47
|
+
mockConsoleLog = mock(() => {})
|
|
48
|
+
mockConsoleError = mock(() => {})
|
|
49
|
+
console.log = mockConsoleLog
|
|
50
|
+
console.error = mockConsoleError
|
|
51
|
+
|
|
52
|
+
// Mock process.stdin for batch tests
|
|
53
|
+
mockProcessStdin = {
|
|
54
|
+
on: mock((event: string, callback: (...args: unknown[]) => void) => {
|
|
55
|
+
if (event === 'data') {
|
|
56
|
+
mockProcessStdin.dataCallback = callback
|
|
57
|
+
} else if (event === 'end') {
|
|
58
|
+
mockProcessStdin.endCallback = callback
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
emit: (data: string) => {
|
|
62
|
+
if (mockProcessStdin.dataCallback) {
|
|
63
|
+
mockProcessStdin.dataCallback(data)
|
|
64
|
+
}
|
|
65
|
+
if (mockProcessStdin.endCallback) {
|
|
66
|
+
mockProcessStdin.endCallback()
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
server.resetHandlers()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should post an overall comment', async () => {
|
|
77
|
+
server.use(
|
|
78
|
+
http.get('*/a/changes/:changeId', () => {
|
|
79
|
+
return HttpResponse.text(`)]}'\n{
|
|
80
|
+
"id": "test-project~main~I123abc",
|
|
81
|
+
"_number": 12345,
|
|
82
|
+
"project": "test-project",
|
|
83
|
+
"branch": "main",
|
|
84
|
+
"change_id": "I123abc",
|
|
85
|
+
"subject": "Test change",
|
|
86
|
+
"status": "NEW",
|
|
87
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
88
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
89
|
+
}`)
|
|
90
|
+
}),
|
|
91
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
|
|
92
|
+
const body = (await request.json()) as { message?: string; comments?: unknown }
|
|
93
|
+
expect(body.message).toBe('This is a test comment')
|
|
94
|
+
expect(body.comments).toBeUndefined()
|
|
95
|
+
return HttpResponse.json({})
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
100
|
+
|
|
101
|
+
const program = commentCommand('12345', {
|
|
102
|
+
message: 'This is a test comment',
|
|
103
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
104
|
+
|
|
105
|
+
await Effect.runPromise(program)
|
|
106
|
+
|
|
107
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
108
|
+
expect(output).toContain('✓ Comment posted successfully!')
|
|
109
|
+
expect(output).toContain('Test change')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should post a line-specific comment', async () => {
|
|
113
|
+
server.use(
|
|
114
|
+
http.get('*/a/changes/:changeId', () => {
|
|
115
|
+
return HttpResponse.text(`)]}'\n{
|
|
116
|
+
"id": "test-project~main~I123abc",
|
|
117
|
+
"_number": 12345,
|
|
118
|
+
"project": "test-project",
|
|
119
|
+
"branch": "main",
|
|
120
|
+
"change_id": "I123abc",
|
|
121
|
+
"subject": "Test change",
|
|
122
|
+
"status": "NEW",
|
|
123
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
124
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
125
|
+
}`)
|
|
126
|
+
}),
|
|
127
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
|
|
128
|
+
const body = (await request.json()) as {
|
|
129
|
+
message?: string
|
|
130
|
+
comments?: Record<string, Array<{ line: number; message: string; unresolved?: boolean }>>
|
|
131
|
+
}
|
|
132
|
+
expect(body.message).toBeUndefined()
|
|
133
|
+
expect(body.comments).toBeDefined()
|
|
134
|
+
expect(body.comments?.['src/main.js']).toBeDefined()
|
|
135
|
+
expect(body.comments?.['src/main.js']?.[0].line).toBe(42)
|
|
136
|
+
expect(body.comments?.['src/main.js']?.[0].message).toBe('Fix this issue')
|
|
137
|
+
expect(body.comments?.['src/main.js']?.[0].unresolved).toBe(true)
|
|
138
|
+
return HttpResponse.json({})
|
|
139
|
+
}),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
143
|
+
|
|
144
|
+
const program = commentCommand('12345', {
|
|
145
|
+
message: 'Fix this issue',
|
|
146
|
+
file: 'src/main.js',
|
|
147
|
+
line: 42,
|
|
148
|
+
unresolved: true,
|
|
149
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
150
|
+
|
|
151
|
+
await Effect.runPromise(program)
|
|
152
|
+
|
|
153
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
154
|
+
expect(output).toContain('✓ Comment posted successfully!')
|
|
155
|
+
expect(output).toContain('File: src/main.js, Line: 42')
|
|
156
|
+
expect(output).toContain('Status: Unresolved')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should handle batch comments', async () => {
|
|
160
|
+
// Override process.stdin temporarily
|
|
161
|
+
const originalStdin = process.stdin
|
|
162
|
+
Object.defineProperty(process, 'stdin', {
|
|
163
|
+
value: mockProcessStdin,
|
|
164
|
+
configurable: true,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
server.use(
|
|
168
|
+
http.get('*/a/changes/:changeId', () => {
|
|
169
|
+
return HttpResponse.text(`)]}'\n{
|
|
170
|
+
"id": "test-project~main~I123abc",
|
|
171
|
+
"_number": 12345,
|
|
172
|
+
"project": "test-project",
|
|
173
|
+
"branch": "main",
|
|
174
|
+
"change_id": "I123abc",
|
|
175
|
+
"subject": "Test change",
|
|
176
|
+
"status": "NEW",
|
|
177
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
178
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
179
|
+
}`)
|
|
180
|
+
}),
|
|
181
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
|
|
182
|
+
const body = (await request.json()) as {
|
|
183
|
+
message?: string
|
|
184
|
+
comments?: Record<string, unknown[]>
|
|
185
|
+
}
|
|
186
|
+
// Array format doesn't include overall message
|
|
187
|
+
expect(body.message).toBeUndefined()
|
|
188
|
+
expect(body.comments).toBeDefined()
|
|
189
|
+
expect(body.comments?.['src/main.js']?.length).toBe(2)
|
|
190
|
+
expect(body.comments?.['src/utils.js']?.length).toBe(1)
|
|
191
|
+
return HttpResponse.json({})
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
196
|
+
|
|
197
|
+
const program = commentCommand('12345', { batch: true }).pipe(
|
|
198
|
+
Effect.provide(GerritApiServiceLive),
|
|
199
|
+
Effect.provide(mockConfigLayer),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Simulate stdin data (array format)
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
mockProcessStdin.emit(
|
|
205
|
+
JSON.stringify([
|
|
206
|
+
{ file: 'src/main.js', line: 10, message: 'First comment' },
|
|
207
|
+
{ file: 'src/main.js', line: 20, message: 'Second comment', unresolved: true },
|
|
208
|
+
{ file: 'src/utils.js', line: 5, message: 'Utils comment' },
|
|
209
|
+
]),
|
|
210
|
+
)
|
|
211
|
+
}, 10)
|
|
212
|
+
|
|
213
|
+
await Effect.runPromise(program)
|
|
214
|
+
|
|
215
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
216
|
+
expect(output).toContain('✓ Comment posted successfully!')
|
|
217
|
+
expect(output).toContain('Posted 3 line comment(s)')
|
|
218
|
+
|
|
219
|
+
// Restore process.stdin
|
|
220
|
+
Object.defineProperty(process, 'stdin', {
|
|
221
|
+
value: originalStdin,
|
|
222
|
+
configurable: true,
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should output XML format for line comments', async () => {
|
|
227
|
+
server.use(
|
|
228
|
+
http.get('*/a/changes/:changeId', () => {
|
|
229
|
+
return HttpResponse.text(`)]}'\n{
|
|
230
|
+
"id": "test-project~main~I123abc",
|
|
231
|
+
"_number": 12345,
|
|
232
|
+
"project": "test-project",
|
|
233
|
+
"branch": "main",
|
|
234
|
+
"change_id": "I123abc",
|
|
235
|
+
"subject": "Test change",
|
|
236
|
+
"status": "NEW",
|
|
237
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
238
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
239
|
+
}`)
|
|
240
|
+
}),
|
|
241
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async () => {
|
|
242
|
+
return HttpResponse.json({})
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
247
|
+
|
|
248
|
+
const program = commentCommand('12345', {
|
|
249
|
+
message: 'Fix this',
|
|
250
|
+
file: 'test.js',
|
|
251
|
+
line: 10,
|
|
252
|
+
xml: true,
|
|
253
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
254
|
+
|
|
255
|
+
await Effect.runPromise(program)
|
|
256
|
+
|
|
257
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
258
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
259
|
+
expect(output).toContain('<comment_result>')
|
|
260
|
+
expect(output).toContain('<status>success</status>')
|
|
261
|
+
expect(output).toContain('<file>test.js</file>')
|
|
262
|
+
expect(output).toContain('<line>10</line>')
|
|
263
|
+
expect(output).toContain('<message><![CDATA[Fix this]]></message>')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should provide detailed error for invalid JSON with input preview', async () => {
|
|
267
|
+
const originalStdin = process.stdin
|
|
268
|
+
Object.defineProperty(process, 'stdin', {
|
|
269
|
+
value: mockProcessStdin,
|
|
270
|
+
configurable: true,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
274
|
+
|
|
275
|
+
const program = commentCommand('12345', { batch: true }).pipe(
|
|
276
|
+
Effect.provide(GerritApiServiceLive),
|
|
277
|
+
Effect.provide(mockConfigLayer),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const malformedJson = `[
|
|
281
|
+
{
|
|
282
|
+
"file": "src/main.js",
|
|
283
|
+
"line": 10,
|
|
284
|
+
"message": "This is an unterminated string
|
|
285
|
+
}
|
|
286
|
+
]`
|
|
287
|
+
|
|
288
|
+
// Simulate invalid JSON input
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
mockProcessStdin.emit(malformedJson)
|
|
291
|
+
}, 10)
|
|
292
|
+
|
|
293
|
+
await expect(Effect.runPromise(program)).rejects.toThrow(
|
|
294
|
+
/Invalid JSON input: .*Unterminated string.*\nInput \(\d+ chars, \d+ lines\):\n.*src\/main\.js.*\nExpected format:/s,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
// Restore process.stdin
|
|
298
|
+
Object.defineProperty(process, 'stdin', {
|
|
299
|
+
value: originalStdin,
|
|
300
|
+
configurable: true,
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('should reject invalid batch JSON', async () => {
|
|
305
|
+
const originalStdin = process.stdin
|
|
306
|
+
Object.defineProperty(process, 'stdin', {
|
|
307
|
+
value: mockProcessStdin,
|
|
308
|
+
configurable: true,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
312
|
+
|
|
313
|
+
const program = commentCommand('12345', { batch: true }).pipe(
|
|
314
|
+
Effect.provide(GerritApiServiceLive),
|
|
315
|
+
Effect.provide(mockConfigLayer),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
// Simulate invalid JSON
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
mockProcessStdin.emit('not valid json')
|
|
321
|
+
}, 10)
|
|
322
|
+
|
|
323
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Invalid batch input format')
|
|
324
|
+
|
|
325
|
+
// Restore process.stdin
|
|
326
|
+
Object.defineProperty(process, 'stdin', {
|
|
327
|
+
value: originalStdin,
|
|
328
|
+
configurable: true,
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should reject invalid batch schema', async () => {
|
|
333
|
+
const originalStdin = process.stdin
|
|
334
|
+
Object.defineProperty(process, 'stdin', {
|
|
335
|
+
value: mockProcessStdin,
|
|
336
|
+
configurable: true,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
340
|
+
|
|
341
|
+
const program = commentCommand('12345', { batch: true }).pipe(
|
|
342
|
+
Effect.provide(GerritApiServiceLive),
|
|
343
|
+
Effect.provide(mockConfigLayer),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
// Simulate invalid schema (array format)
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
mockProcessStdin.emit(
|
|
349
|
+
JSON.stringify([
|
|
350
|
+
{ message: 'Missing file path' }, // Invalid: missing file
|
|
351
|
+
]),
|
|
352
|
+
)
|
|
353
|
+
}, 10)
|
|
354
|
+
|
|
355
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Invalid batch input format')
|
|
356
|
+
|
|
357
|
+
// Restore process.stdin
|
|
358
|
+
Object.defineProperty(process, 'stdin', {
|
|
359
|
+
value: originalStdin,
|
|
360
|
+
configurable: true,
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should require message for line comments', async () => {
|
|
365
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
366
|
+
|
|
367
|
+
const program = commentCommand('12345', {
|
|
368
|
+
file: 'test.js',
|
|
369
|
+
line: 10,
|
|
370
|
+
// Missing message
|
|
371
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
372
|
+
|
|
373
|
+
await expect(Effect.runPromise(program)).rejects.toThrow(
|
|
374
|
+
'Message is required for line comments',
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('should require message for overall comments when stdin is empty', async () => {
|
|
379
|
+
const originalStdin = process.stdin
|
|
380
|
+
Object.defineProperty(process, 'stdin', {
|
|
381
|
+
value: mockProcessStdin,
|
|
382
|
+
configurable: true,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
386
|
+
|
|
387
|
+
const program = commentCommand('12345', {}).pipe(
|
|
388
|
+
Effect.provide(GerritApiServiceLive),
|
|
389
|
+
Effect.provide(mockConfigLayer),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
// Simulate empty stdin
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
mockProcessStdin.emit('')
|
|
395
|
+
}, 10)
|
|
396
|
+
|
|
397
|
+
await expect(Effect.runPromise(program)).rejects.toThrow(
|
|
398
|
+
'Message is required. Use -m "your message" or pipe content to stdin',
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// Restore process.stdin
|
|
402
|
+
Object.defineProperty(process, 'stdin', {
|
|
403
|
+
value: originalStdin,
|
|
404
|
+
configurable: true,
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should handle API errors gracefully', async () => {
|
|
409
|
+
server.use(
|
|
410
|
+
http.get('*/a/changes/:changeId', () => {
|
|
411
|
+
return HttpResponse.text('Not found', { status: 404 })
|
|
412
|
+
}),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
416
|
+
|
|
417
|
+
const program = commentCommand('12345', {
|
|
418
|
+
message: 'Test comment',
|
|
419
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
420
|
+
|
|
421
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get change')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should handle post review API errors', async () => {
|
|
425
|
+
server.use(
|
|
426
|
+
http.get('*/a/changes/:changeId', () => {
|
|
427
|
+
return HttpResponse.text(`)]}'\n{
|
|
428
|
+
"id": "test-project~main~I123abc",
|
|
429
|
+
"_number": 12345,
|
|
430
|
+
"project": "test-project",
|
|
431
|
+
"branch": "main",
|
|
432
|
+
"change_id": "I123abc",
|
|
433
|
+
"subject": "Test change",
|
|
434
|
+
"status": "NEW",
|
|
435
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
436
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
437
|
+
}`)
|
|
438
|
+
}),
|
|
439
|
+
http.post('*/a/changes/:changeId/revisions/current/review', () => {
|
|
440
|
+
return HttpResponse.text('Forbidden', { status: 403 })
|
|
441
|
+
}),
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
445
|
+
|
|
446
|
+
const program = commentCommand('12345', {
|
|
447
|
+
message: 'Test comment',
|
|
448
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
449
|
+
|
|
450
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to post comment')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('should output XML for batch comments', async () => {
|
|
454
|
+
const originalStdin = process.stdin
|
|
455
|
+
Object.defineProperty(process, 'stdin', {
|
|
456
|
+
value: mockProcessStdin,
|
|
457
|
+
configurable: true,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
server.use(
|
|
461
|
+
http.get('*/a/changes/:changeId', () => {
|
|
462
|
+
return HttpResponse.text(`)]}'\n{
|
|
463
|
+
"id": "test-project~main~I123abc",
|
|
464
|
+
"_number": 12345,
|
|
465
|
+
"project": "test-project",
|
|
466
|
+
"branch": "main",
|
|
467
|
+
"change_id": "I123abc",
|
|
468
|
+
"subject": "Test change",
|
|
469
|
+
"status": "NEW",
|
|
470
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
471
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
472
|
+
}`)
|
|
473
|
+
}),
|
|
474
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async () => {
|
|
475
|
+
return HttpResponse.json({})
|
|
476
|
+
}),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
480
|
+
|
|
481
|
+
const program = commentCommand('12345', { batch: true, xml: true }).pipe(
|
|
482
|
+
Effect.provide(GerritApiServiceLive),
|
|
483
|
+
Effect.provide(mockConfigLayer),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
// Simulate stdin data (array format)
|
|
487
|
+
setTimeout(() => {
|
|
488
|
+
mockProcessStdin.emit(
|
|
489
|
+
JSON.stringify([
|
|
490
|
+
{ file: 'src/main.js', line: 10, message: 'First comment' },
|
|
491
|
+
{ file: 'src/main.js', line: 20, message: 'Second comment', unresolved: true },
|
|
492
|
+
]),
|
|
493
|
+
)
|
|
494
|
+
}, 10)
|
|
495
|
+
|
|
496
|
+
await Effect.runPromise(program)
|
|
497
|
+
|
|
498
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
499
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
500
|
+
expect(output).toContain('<comments>')
|
|
501
|
+
expect(output).toContain('<file>src/main.js</file>')
|
|
502
|
+
expect(output).toContain('<line>10</line>')
|
|
503
|
+
expect(output).toContain('<line>20</line>')
|
|
504
|
+
expect(output).toContain('<unresolved>true</unresolved>')
|
|
505
|
+
expect(output).toContain('</comments>')
|
|
506
|
+
|
|
507
|
+
// Restore process.stdin
|
|
508
|
+
Object.defineProperty(process, 'stdin', {
|
|
509
|
+
value: originalStdin,
|
|
510
|
+
configurable: true,
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('should accept piped input for overall comments', async () => {
|
|
515
|
+
const originalStdin = process.stdin
|
|
516
|
+
Object.defineProperty(process, 'stdin', {
|
|
517
|
+
value: mockProcessStdin,
|
|
518
|
+
configurable: true,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
server.use(
|
|
522
|
+
http.get('*/a/changes/:changeId', () => {
|
|
523
|
+
return HttpResponse.text(`)]}'\n{
|
|
524
|
+
"id": "test-project~main~I123abc",
|
|
525
|
+
"_number": 12345,
|
|
526
|
+
"project": "test-project",
|
|
527
|
+
"branch": "main",
|
|
528
|
+
"change_id": "I123abc",
|
|
529
|
+
"subject": "Test change",
|
|
530
|
+
"status": "NEW",
|
|
531
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
532
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
533
|
+
}`)
|
|
534
|
+
}),
|
|
535
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
|
|
536
|
+
const body = (await request.json()) as { message?: string }
|
|
537
|
+
expect(body.message).toBe('Piped comment message')
|
|
538
|
+
return HttpResponse.json({})
|
|
539
|
+
}),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
543
|
+
|
|
544
|
+
// Test comment without message option (should read from stdin)
|
|
545
|
+
const program = commentCommand('12345', {}).pipe(
|
|
546
|
+
Effect.provide(GerritApiServiceLive),
|
|
547
|
+
Effect.provide(mockConfigLayer),
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
// Simulate piped input
|
|
551
|
+
setTimeout(() => {
|
|
552
|
+
mockProcessStdin.emit('Piped comment message')
|
|
553
|
+
}, 10)
|
|
554
|
+
|
|
555
|
+
await Effect.runPromise(program)
|
|
556
|
+
|
|
557
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
558
|
+
expect(output).toContain('✓ Comment posted successfully!')
|
|
559
|
+
expect(output).toContain('Message: Piped comment message')
|
|
560
|
+
|
|
561
|
+
// Restore process.stdin
|
|
562
|
+
Object.defineProperty(process, 'stdin', {
|
|
563
|
+
value: originalStdin,
|
|
564
|
+
configurable: true,
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('should trim whitespace from piped input', async () => {
|
|
569
|
+
const originalStdin = process.stdin
|
|
570
|
+
Object.defineProperty(process, 'stdin', {
|
|
571
|
+
value: mockProcessStdin,
|
|
572
|
+
configurable: true,
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
server.use(
|
|
576
|
+
http.get('*/a/changes/:changeId', () => {
|
|
577
|
+
return HttpResponse.text(`)]}'\n{
|
|
578
|
+
"id": "test-project~main~I123abc",
|
|
579
|
+
"_number": 12345,
|
|
580
|
+
"project": "test-project",
|
|
581
|
+
"branch": "main",
|
|
582
|
+
"change_id": "I123abc",
|
|
583
|
+
"subject": "Test change",
|
|
584
|
+
"status": "NEW",
|
|
585
|
+
"created": "2024-01-15 10:00:00.000000000",
|
|
586
|
+
"updated": "2024-01-15 10:00:00.000000000"
|
|
587
|
+
}`)
|
|
588
|
+
}),
|
|
589
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
|
|
590
|
+
const body = (await request.json()) as { message?: string }
|
|
591
|
+
expect(body.message).toBe('Trimmed message')
|
|
592
|
+
return HttpResponse.json({})
|
|
593
|
+
}),
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
597
|
+
|
|
598
|
+
const program = commentCommand('12345', {}).pipe(
|
|
599
|
+
Effect.provide(GerritApiServiceLive),
|
|
600
|
+
Effect.provide(mockConfigLayer),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
// Simulate piped input with whitespace
|
|
604
|
+
setTimeout(() => {
|
|
605
|
+
mockProcessStdin.emit(' \n Trimmed message \n ')
|
|
606
|
+
}, 10)
|
|
607
|
+
|
|
608
|
+
await Effect.runPromise(program)
|
|
609
|
+
|
|
610
|
+
// Restore process.stdin
|
|
611
|
+
Object.defineProperty(process, 'stdin', {
|
|
612
|
+
value: originalStdin,
|
|
613
|
+
configurable: true,
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('should provide detailed error context for batch comment failures', async () => {
|
|
618
|
+
const originalStdin = process.stdin
|
|
619
|
+
Object.defineProperty(process, 'stdin', {
|
|
620
|
+
value: mockProcessStdin,
|
|
621
|
+
configurable: true,
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
server.use(
|
|
625
|
+
http.get('*/a/changes/:changeId', () => {
|
|
626
|
+
return HttpResponse.text(`)]}'\n{
|
|
627
|
+
"id": "test-project~main~I123abc",
|
|
628
|
+
"_number": 12345,
|
|
629
|
+
"project": "test-project",
|
|
630
|
+
"branch": "main",
|
|
631
|
+
"change_id": "I123abc",
|
|
632
|
+
"subject": "Test change",
|
|
633
|
+
"status": "NEW"
|
|
634
|
+
}`)
|
|
635
|
+
}),
|
|
636
|
+
http.post('*/a/changes/:changeId/revisions/current/review', () => {
|
|
637
|
+
return HttpResponse.text(
|
|
638
|
+
'file app/models/auto_grade_result.rb not found in revision 386823,6',
|
|
639
|
+
{ status: 400 },
|
|
640
|
+
)
|
|
641
|
+
}),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
645
|
+
|
|
646
|
+
const program = commentCommand('12345', { batch: true }).pipe(
|
|
647
|
+
Effect.provide(GerritApiServiceLive),
|
|
648
|
+
Effect.provide(mockConfigLayer),
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
// Simulate batch input
|
|
652
|
+
setTimeout(() => {
|
|
653
|
+
mockProcessStdin.emit(
|
|
654
|
+
JSON.stringify([
|
|
655
|
+
{ file: 'app/models/auto_grade_result.rb', line: 23, message: 'This needs improvement' },
|
|
656
|
+
{
|
|
657
|
+
file: 'src/utils.js',
|
|
658
|
+
line: 45,
|
|
659
|
+
message:
|
|
660
|
+
'This is a very long comment message that should be truncated in the error output to keep it readable',
|
|
661
|
+
},
|
|
662
|
+
]),
|
|
663
|
+
)
|
|
664
|
+
}, 10)
|
|
665
|
+
|
|
666
|
+
await expect(Effect.runPromise(program)).rejects.toThrow(
|
|
667
|
+
/Failed to post comment: file app\/models\/auto_grade_result\.rb not found in revision 386823,6\nTried to post: app\/models\/auto_grade_result\.rb:23 "This needs improvement", src\/utils\.js:45 "This is a very long comment message that should be\.\.\."/,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
// Restore process.stdin
|
|
671
|
+
Object.defineProperty(process, 'stdin', {
|
|
672
|
+
value: originalStdin,
|
|
673
|
+
configurable: true,
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('should provide detailed error context for line comment failures', async () => {
|
|
678
|
+
server.use(
|
|
679
|
+
http.get('*/a/changes/:changeId', () => {
|
|
680
|
+
return HttpResponse.text(`)]}'\n{
|
|
681
|
+
"id": "test-project~main~I123abc",
|
|
682
|
+
"_number": 12345,
|
|
683
|
+
"project": "test-project",
|
|
684
|
+
"branch": "main",
|
|
685
|
+
"change_id": "I123abc",
|
|
686
|
+
"subject": "Test change",
|
|
687
|
+
"status": "NEW"
|
|
688
|
+
}`)
|
|
689
|
+
}),
|
|
690
|
+
http.post('*/a/changes/:changeId/revisions/current/review', () => {
|
|
691
|
+
return HttpResponse.text('file not found', { status: 400 })
|
|
692
|
+
}),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
696
|
+
|
|
697
|
+
const program = commentCommand('12345', {
|
|
698
|
+
file: 'missing-file.rb',
|
|
699
|
+
line: 42,
|
|
700
|
+
message: 'Test comment on missing file',
|
|
701
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
702
|
+
|
|
703
|
+
await expect(Effect.runPromise(program)).rejects.toThrow(
|
|
704
|
+
'Failed to post comment: file not found\nTried to post to missing-file.rb:42: "Test comment on missing file"',
|
|
705
|
+
)
|
|
706
|
+
})
|
|
707
|
+
})
|