@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.
Files changed (91) hide show
  1. package/.ast-grep/rules/no-as-casting.yml +13 -0
  2. package/.eslintrc.js +12 -0
  3. package/.github/workflows/ci-simple.yml +53 -0
  4. package/.github/workflows/ci.yml +171 -0
  5. package/.github/workflows/claude-code-review.yml +78 -0
  6. package/.github/workflows/claude.yml +64 -0
  7. package/.github/workflows/dependency-update.yml +84 -0
  8. package/.github/workflows/release.yml +166 -0
  9. package/.github/workflows/security-scan.yml +113 -0
  10. package/.github/workflows/security.yml +96 -0
  11. package/.husky/pre-commit +16 -0
  12. package/.husky/pre-push +25 -0
  13. package/.lintstagedrc.json +6 -0
  14. package/.tool-versions +1 -0
  15. package/CLAUDE.md +103 -0
  16. package/DEVELOPMENT.md +361 -0
  17. package/LICENSE +21 -0
  18. package/README.md +325 -0
  19. package/bin/ger +3 -0
  20. package/biome.json +36 -0
  21. package/bun.lock +688 -0
  22. package/bunfig.toml +8 -0
  23. package/oxlint.json +24 -0
  24. package/package.json +55 -0
  25. package/scripts/check-coverage.ts +69 -0
  26. package/scripts/check-file-size.ts +38 -0
  27. package/scripts/fix-test-mocks.ts +55 -0
  28. package/src/api/gerrit.ts +466 -0
  29. package/src/cli/commands/abandon.ts +65 -0
  30. package/src/cli/commands/comment.ts +460 -0
  31. package/src/cli/commands/comments.ts +85 -0
  32. package/src/cli/commands/diff.ts +71 -0
  33. package/src/cli/commands/incoming.ts +226 -0
  34. package/src/cli/commands/init.ts +164 -0
  35. package/src/cli/commands/mine.ts +115 -0
  36. package/src/cli/commands/open.ts +57 -0
  37. package/src/cli/commands/review.ts +593 -0
  38. package/src/cli/commands/setup.ts +230 -0
  39. package/src/cli/commands/show.ts +303 -0
  40. package/src/cli/commands/status.ts +35 -0
  41. package/src/cli/commands/workspace.ts +200 -0
  42. package/src/cli/index.ts +420 -0
  43. package/src/prompts/default-review.md +80 -0
  44. package/src/prompts/system-inline-review.md +88 -0
  45. package/src/prompts/system-overall-review.md +152 -0
  46. package/src/schemas/config.test.ts +245 -0
  47. package/src/schemas/config.ts +75 -0
  48. package/src/schemas/gerrit.ts +455 -0
  49. package/src/services/ai-enhanced.ts +167 -0
  50. package/src/services/ai.ts +182 -0
  51. package/src/services/config.test.ts +414 -0
  52. package/src/services/config.ts +206 -0
  53. package/src/test-utils/mock-generator.ts +73 -0
  54. package/src/utils/comment-formatters.ts +153 -0
  55. package/src/utils/diff-context.ts +103 -0
  56. package/src/utils/diff-formatters.ts +141 -0
  57. package/src/utils/formatters.ts +85 -0
  58. package/src/utils/message-filters.ts +26 -0
  59. package/src/utils/shell-safety.ts +117 -0
  60. package/src/utils/status-indicators.ts +100 -0
  61. package/src/utils/url-parser.test.ts +123 -0
  62. package/src/utils/url-parser.ts +91 -0
  63. package/tests/abandon.test.ts +163 -0
  64. package/tests/ai-service.test.ts +489 -0
  65. package/tests/comment-batch-advanced.test.ts +431 -0
  66. package/tests/comment-gerrit-api-compliance.test.ts +414 -0
  67. package/tests/comment.test.ts +707 -0
  68. package/tests/comments.test.ts +323 -0
  69. package/tests/config-service-simple.test.ts +100 -0
  70. package/tests/diff.test.ts +419 -0
  71. package/tests/helpers/config-mock.ts +27 -0
  72. package/tests/incoming.test.ts +357 -0
  73. package/tests/interactive-incoming.test.ts +173 -0
  74. package/tests/mine.test.ts +318 -0
  75. package/tests/mocks/fetch-mock.ts +139 -0
  76. package/tests/mocks/msw-handlers.ts +80 -0
  77. package/tests/open.test.ts +233 -0
  78. package/tests/review.test.ts +669 -0
  79. package/tests/setup.ts +13 -0
  80. package/tests/show.test.ts +439 -0
  81. package/tests/unit/schemas/gerrit.test.ts +85 -0
  82. package/tests/unit/test-utils/mock-generator.test.ts +154 -0
  83. package/tests/unit/utils/comment-formatters.test.ts +415 -0
  84. package/tests/unit/utils/diff-context.test.ts +171 -0
  85. package/tests/unit/utils/diff-formatters.test.ts +165 -0
  86. package/tests/unit/utils/formatters.test.ts +411 -0
  87. package/tests/unit/utils/message-filters.test.ts +227 -0
  88. package/tests/unit/utils/prompt-helpers.test.ts +175 -0
  89. package/tests/unit/utils/shell-safety.test.ts +230 -0
  90. package/tests/unit/utils/status-indicators.test.ts +137 -0
  91. package/tsconfig.json +40 -0
@@ -0,0 +1,431 @@
1
+ import { test, expect, describe, beforeAll, afterEach, afterAll } from 'bun:test'
2
+ import { http, HttpResponse } from 'msw'
3
+ import { setupServer } from 'msw/node'
4
+ import { Effect, Layer } from 'effect'
5
+ import { ConfigService } from '@/services/config'
6
+ import { GerritApiServiceLive } from '@/api/gerrit'
7
+ import { commentCommand } from '@/cli/commands/comment'
8
+ import { EventEmitter } from 'node:events'
9
+
10
+ import { createMockConfigService } from './helpers/config-mock'
11
+ // Create a mock process.stdin for testing
12
+ class MockProcessStdin extends EventEmitter {
13
+ isTTY = false
14
+ readable = true
15
+
16
+ emit(event: string, data?: any): boolean {
17
+ if (event === 'data') {
18
+ super.emit('data', Buffer.from(data))
19
+ // Automatically emit 'end' after data
20
+ setTimeout(() => super.emit('end'), 0)
21
+ return true
22
+ }
23
+ return super.emit(event, data)
24
+ }
25
+ }
26
+
27
+ const server = setupServer()
28
+
29
+ beforeAll(() => server.listen())
30
+ afterEach(() => server.resetHandlers())
31
+ afterAll(() => server.close())
32
+
33
+ describe('comment command - advanced batch features', () => {
34
+ const mockProcessStdin = new MockProcessStdin()
35
+
36
+ test('should handle batch comments with side parameter', async () => {
37
+ const originalStdin = process.stdin
38
+ Object.defineProperty(process, 'stdin', {
39
+ value: mockProcessStdin,
40
+ configurable: true,
41
+ })
42
+
43
+ server.use(
44
+ http.get('*/a/changes/:changeId', () => {
45
+ return HttpResponse.text(`)]}'\n{
46
+ "id": "test-project~main~I123abc",
47
+ "_number": 12345,
48
+ "project": "test-project",
49
+ "branch": "main",
50
+ "change_id": "I123abc",
51
+ "subject": "Test change",
52
+ "status": "NEW",
53
+ "created": "2024-01-15 10:00:00.000000000",
54
+ "updated": "2024-01-15 10:00:00.000000000"
55
+ }`)
56
+ }),
57
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
58
+ const body = (await request.json()) as {
59
+ message?: string
60
+ comments?: Record<string, unknown[]>
61
+ }
62
+ expect(body.comments).toBeDefined()
63
+
64
+ const fileComments = body.comments?.['src/main.js'] as Array<{
65
+ line?: number
66
+ side?: string
67
+ message: string
68
+ }>
69
+
70
+ expect(fileComments?.length).toBe(2)
71
+ expect(fileComments?.[0]).toMatchObject({
72
+ line: 10,
73
+ side: 'PARENT',
74
+ message: 'Why was this removed?',
75
+ })
76
+ expect(fileComments?.[1]).toMatchObject({
77
+ line: 10,
78
+ side: 'REVISION',
79
+ message: 'Good improvement',
80
+ })
81
+
82
+ return HttpResponse.json({})
83
+ }),
84
+ )
85
+
86
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
87
+
88
+ const program = commentCommand('12345', { batch: true }).pipe(
89
+ Effect.provide(GerritApiServiceLive),
90
+ Effect.provide(mockConfigLayer),
91
+ )
92
+
93
+ // Simulate stdin data with side parameter
94
+ setTimeout(() => {
95
+ mockProcessStdin.emit(
96
+ 'data',
97
+ JSON.stringify([
98
+ { file: 'src/main.js', line: 10, message: 'Why was this removed?', side: 'PARENT' },
99
+ { file: 'src/main.js', line: 10, message: 'Good improvement', side: 'REVISION' },
100
+ ]),
101
+ )
102
+ }, 10)
103
+
104
+ await Effect.runPromise(program)
105
+
106
+ // Restore process.stdin
107
+ Object.defineProperty(process, 'stdin', {
108
+ value: originalStdin,
109
+ configurable: true,
110
+ })
111
+ })
112
+
113
+ test('should handle batch comments with range parameter', async () => {
114
+ const originalStdin = process.stdin
115
+ Object.defineProperty(process, 'stdin', {
116
+ value: mockProcessStdin,
117
+ configurable: true,
118
+ })
119
+
120
+ server.use(
121
+ http.get('*/a/changes/:changeId', () => {
122
+ return HttpResponse.text(`)]}'\n{
123
+ "id": "test-project~main~I123abc",
124
+ "_number": 12345,
125
+ "project": "test-project",
126
+ "branch": "main",
127
+ "change_id": "I123abc",
128
+ "subject": "Test change",
129
+ "status": "NEW"
130
+ }`)
131
+ }),
132
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
133
+ const body = (await request.json()) as {
134
+ comments?: Record<string, unknown[]>
135
+ }
136
+
137
+ const fileComments = body.comments?.['src/Calculator.java'] as Array<{
138
+ range?: {
139
+ start_line: number
140
+ end_line: number
141
+ start_character?: number
142
+ end_character?: number
143
+ }
144
+ message: string
145
+ }>
146
+
147
+ expect(fileComments?.length).toBe(3)
148
+
149
+ // Multi-line range comment
150
+ expect(fileComments?.[0]).toMatchObject({
151
+ range: {
152
+ start_line: 50,
153
+ end_line: 55,
154
+ },
155
+ message: 'This block needs refactoring',
156
+ })
157
+
158
+ // Character-specific range
159
+ expect(fileComments?.[1]).toMatchObject({
160
+ range: {
161
+ start_line: 10,
162
+ start_character: 8,
163
+ end_line: 10,
164
+ end_character: 25,
165
+ },
166
+ message: 'Variable name is confusing',
167
+ })
168
+
169
+ // Mixed with regular line comment
170
+ expect(fileComments?.[2]).toMatchObject({
171
+ line: 42,
172
+ message: 'Add null check here',
173
+ })
174
+
175
+ return HttpResponse.json({})
176
+ }),
177
+ )
178
+
179
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
180
+
181
+ const program = commentCommand('12345', { batch: true }).pipe(
182
+ Effect.provide(GerritApiServiceLive),
183
+ Effect.provide(mockConfigLayer),
184
+ )
185
+
186
+ // Simulate stdin data with range parameter
187
+ setTimeout(() => {
188
+ mockProcessStdin.emit(
189
+ 'data',
190
+ JSON.stringify([
191
+ {
192
+ file: 'src/Calculator.java',
193
+ range: { start_line: 50, end_line: 55 },
194
+ message: 'This block needs refactoring',
195
+ },
196
+ {
197
+ file: 'src/Calculator.java',
198
+ range: { start_line: 10, start_character: 8, end_line: 10, end_character: 25 },
199
+ message: 'Variable name is confusing',
200
+ },
201
+ {
202
+ file: 'src/Calculator.java',
203
+ line: 42,
204
+ message: 'Add null check here',
205
+ },
206
+ ]),
207
+ )
208
+ }, 10)
209
+
210
+ await Effect.runPromise(program)
211
+
212
+ // Restore process.stdin
213
+ Object.defineProperty(process, 'stdin', {
214
+ value: originalStdin,
215
+ configurable: true,
216
+ })
217
+ })
218
+
219
+ test('should handle batch comments with both side and range', async () => {
220
+ const originalStdin = process.stdin
221
+ Object.defineProperty(process, 'stdin', {
222
+ value: mockProcessStdin,
223
+ configurable: true,
224
+ })
225
+
226
+ server.use(
227
+ http.get('*/a/changes/:changeId', () => {
228
+ return HttpResponse.text(`)]}'\n{
229
+ "id": "test-project~main~I123abc",
230
+ "_number": 12345,
231
+ "project": "test-project",
232
+ "branch": "main",
233
+ "change_id": "I123abc",
234
+ "subject": "Test change",
235
+ "status": "NEW"
236
+ }`)
237
+ }),
238
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
239
+ const body = (await request.json()) as {
240
+ comments?: Record<string, unknown[]>
241
+ }
242
+
243
+ const fileComments = body.comments?.['src/Service.java'] as Array<{
244
+ range?: {
245
+ start_line: number
246
+ end_line: number
247
+ }
248
+ side?: string
249
+ message: string
250
+ unresolved?: boolean
251
+ }>
252
+
253
+ expect(fileComments?.length).toBe(2)
254
+
255
+ // Range comment on PARENT side
256
+ expect(fileComments?.[0]).toMatchObject({
257
+ range: {
258
+ start_line: 20,
259
+ end_line: 35,
260
+ },
261
+ side: 'PARENT',
262
+ message: 'Why was this error handling removed?',
263
+ unresolved: true,
264
+ })
265
+
266
+ // Range comment on REVISION side
267
+ expect(fileComments?.[1]).toMatchObject({
268
+ range: {
269
+ start_line: 20,
270
+ end_line: 35,
271
+ },
272
+ side: 'REVISION',
273
+ message: 'New error handling looks good, but consider extracting',
274
+ })
275
+
276
+ return HttpResponse.json({})
277
+ }),
278
+ )
279
+
280
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
281
+
282
+ const program = commentCommand('12345', { batch: true }).pipe(
283
+ Effect.provide(GerritApiServiceLive),
284
+ Effect.provide(mockConfigLayer),
285
+ )
286
+
287
+ // Simulate stdin data with both range and side
288
+ setTimeout(() => {
289
+ mockProcessStdin.emit(
290
+ 'data',
291
+ JSON.stringify([
292
+ {
293
+ file: 'src/Service.java',
294
+ range: { start_line: 20, end_line: 35 },
295
+ side: 'PARENT',
296
+ message: 'Why was this error handling removed?',
297
+ unresolved: true,
298
+ },
299
+ {
300
+ file: 'src/Service.java',
301
+ range: { start_line: 20, end_line: 35 },
302
+ side: 'REVISION',
303
+ message: 'New error handling looks good, but consider extracting',
304
+ },
305
+ ]),
306
+ )
307
+ }, 10)
308
+
309
+ await Effect.runPromise(program)
310
+
311
+ // Restore process.stdin
312
+ Object.defineProperty(process, 'stdin', {
313
+ value: originalStdin,
314
+ configurable: true,
315
+ })
316
+ })
317
+
318
+ test('should validate side parameter values', async () => {
319
+ const originalStdin = process.stdin
320
+ Object.defineProperty(process, 'stdin', {
321
+ value: mockProcessStdin,
322
+ configurable: true,
323
+ })
324
+
325
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
326
+
327
+ const program = commentCommand('12345', { batch: true }).pipe(
328
+ Effect.provide(GerritApiServiceLive),
329
+ Effect.provide(mockConfigLayer),
330
+ )
331
+
332
+ // Simulate invalid side value
333
+ setTimeout(() => {
334
+ mockProcessStdin.emit(
335
+ 'data',
336
+ JSON.stringify([
337
+ {
338
+ file: 'src/main.js',
339
+ line: 10,
340
+ message: 'Test',
341
+ side: 'INVALID', // Invalid side value
342
+ },
343
+ ]),
344
+ )
345
+ }, 10)
346
+
347
+ await expect(Effect.runPromise(program)).rejects.toThrow('Invalid batch input format')
348
+
349
+ // Restore process.stdin
350
+ Object.defineProperty(process, 'stdin', {
351
+ value: originalStdin,
352
+ configurable: true,
353
+ })
354
+ })
355
+
356
+ test('should require either line or range but not both', async () => {
357
+ const originalStdin = process.stdin
358
+ Object.defineProperty(process, 'stdin', {
359
+ value: mockProcessStdin,
360
+ configurable: true,
361
+ })
362
+
363
+ server.use(
364
+ http.get('*/a/changes/:changeId', () => {
365
+ return HttpResponse.text(`)]}'\n{
366
+ "id": "test-project~main~I123abc",
367
+ "_number": 12345,
368
+ "project": "test-project",
369
+ "branch": "main",
370
+ "change_id": "I123abc",
371
+ "subject": "Test change",
372
+ "status": "NEW"
373
+ }`)
374
+ }),
375
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
376
+ const body = (await request.json()) as {
377
+ comments?: Record<string, unknown[]>
378
+ }
379
+
380
+ const fileComments = body.comments?.['src/main.js'] as Array<{
381
+ line?: number
382
+ range?: unknown
383
+ message: string
384
+ }>
385
+
386
+ // Should use range when both are provided (range takes precedence)
387
+ expect(fileComments?.[0]).toMatchObject({
388
+ range: {
389
+ start_line: 10,
390
+ end_line: 15,
391
+ },
392
+ message: 'Test comment',
393
+ })
394
+ // line should NOT be included when range is present (Gerrit API preference)
395
+ expect(fileComments?.[0].line).toBeUndefined()
396
+
397
+ return HttpResponse.json({})
398
+ }),
399
+ )
400
+
401
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
402
+
403
+ const program = commentCommand('12345', { batch: true }).pipe(
404
+ Effect.provide(GerritApiServiceLive),
405
+ Effect.provide(mockConfigLayer),
406
+ )
407
+
408
+ // Both line and range provided - should work
409
+ setTimeout(() => {
410
+ mockProcessStdin.emit(
411
+ 'data',
412
+ JSON.stringify([
413
+ {
414
+ file: 'src/main.js',
415
+ line: 10, // Will be included
416
+ range: { start_line: 10, end_line: 15 }, // Takes precedence
417
+ message: 'Test comment',
418
+ },
419
+ ]),
420
+ )
421
+ }, 10)
422
+
423
+ await Effect.runPromise(program)
424
+
425
+ // Restore process.stdin
426
+ Object.defineProperty(process, 'stdin', {
427
+ value: originalStdin,
428
+ configurable: true,
429
+ })
430
+ })
431
+ })