@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,414 @@
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('Gerrit API Compliance Tests', () => {
34
+ const mockProcessStdin = new MockProcessStdin()
35
+
36
+ test('should match exact Gerrit API format for batch comments', async () => {
37
+ const originalStdin = process.stdin
38
+ Object.defineProperty(process, 'stdin', {
39
+ value: mockProcessStdin,
40
+ configurable: true,
41
+ })
42
+
43
+ let capturedRequestBody: any = null
44
+
45
+ server.use(
46
+ http.get('*/a/changes/:changeId', () => {
47
+ return HttpResponse.text(`)]}'\n{
48
+ "id": "test-project~main~I123abc",
49
+ "_number": 12345,
50
+ "project": "test-project",
51
+ "branch": "main",
52
+ "change_id": "I123abc",
53
+ "subject": "Test change",
54
+ "status": "NEW"
55
+ }`)
56
+ }),
57
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
58
+ capturedRequestBody = await request.json()
59
+ return HttpResponse.json({})
60
+ }),
61
+ )
62
+
63
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
64
+
65
+ const program = commentCommand('12345', { batch: true }).pipe(
66
+ Effect.provide(GerritApiServiceLive),
67
+ Effect.provide(mockConfigLayer),
68
+ )
69
+
70
+ // Test exact Gerrit API example from documentation
71
+ setTimeout(() => {
72
+ mockProcessStdin.emit(
73
+ 'data',
74
+ JSON.stringify([
75
+ {
76
+ file: 'gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java',
77
+ line: 23,
78
+ message: '[nit] trailing whitespace',
79
+ },
80
+ {
81
+ file: 'gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java',
82
+ line: 49,
83
+ message: '[nit] s/conrtol/control',
84
+ },
85
+ {
86
+ file: 'gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java',
87
+ range: {
88
+ start_line: 50,
89
+ start_character: 0,
90
+ end_line: 55,
91
+ end_character: 20,
92
+ },
93
+ message: 'Incorrect indentation',
94
+ },
95
+ ]),
96
+ )
97
+ }, 10)
98
+
99
+ await Effect.runPromise(program)
100
+
101
+ // Verify the request body matches Gerrit API format
102
+ expect(capturedRequestBody).toBeDefined()
103
+ expect(capturedRequestBody.comments).toBeDefined()
104
+
105
+ const comments =
106
+ capturedRequestBody.comments[
107
+ 'gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java'
108
+ ]
109
+ expect(comments).toBeDefined()
110
+ expect(comments.length).toBe(3)
111
+
112
+ // Verify first comment (line-based)
113
+ expect(comments[0]).toEqual({
114
+ line: 23,
115
+ message: '[nit] trailing whitespace',
116
+ })
117
+
118
+ // Verify second comment (line-based)
119
+ expect(comments[1]).toEqual({
120
+ line: 49,
121
+ message: '[nit] s/conrtol/control',
122
+ })
123
+
124
+ // Verify third comment (range-based)
125
+ expect(comments[2]).toEqual({
126
+ range: {
127
+ start_line: 50,
128
+ start_character: 0,
129
+ end_line: 55,
130
+ end_character: 20,
131
+ },
132
+ message: 'Incorrect indentation',
133
+ })
134
+
135
+ // Restore process.stdin
136
+ Object.defineProperty(process, 'stdin', {
137
+ value: originalStdin,
138
+ configurable: true,
139
+ })
140
+ })
141
+
142
+ test('should handle all Gerrit comment features combined', async () => {
143
+ const originalStdin = process.stdin
144
+ Object.defineProperty(process, 'stdin', {
145
+ value: mockProcessStdin,
146
+ configurable: true,
147
+ })
148
+
149
+ let capturedRequestBody: any = null
150
+
151
+ server.use(
152
+ http.get('*/a/changes/:changeId', () => {
153
+ return HttpResponse.text(`)]}'\n{
154
+ "id": "test-project~main~I123abc",
155
+ "_number": 12345,
156
+ "project": "test-project",
157
+ "branch": "main",
158
+ "change_id": "I123abc",
159
+ "subject": "Test change",
160
+ "status": "NEW"
161
+ }`)
162
+ }),
163
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
164
+ capturedRequestBody = await request.json()
165
+ return HttpResponse.json({})
166
+ }),
167
+ )
168
+
169
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
170
+
171
+ const program = commentCommand('12345', { batch: true }).pipe(
172
+ Effect.provide(GerritApiServiceLive),
173
+ Effect.provide(mockConfigLayer),
174
+ )
175
+
176
+ // Comprehensive test with all features
177
+ setTimeout(() => {
178
+ mockProcessStdin.emit(
179
+ 'data',
180
+ JSON.stringify([
181
+ // Simple line comment
182
+ {
183
+ file: 'src/main/java/com/example/MyClass.java',
184
+ line: 15,
185
+ message: 'Could you refactor this method to improve readability?',
186
+ },
187
+ // Range comment with character positions
188
+ {
189
+ file: 'src/main/java/com/example/MyClass.java',
190
+ range: {
191
+ start_line: 30,
192
+ start_character: 12,
193
+ end_line: 30,
194
+ end_character: 15,
195
+ },
196
+ message: "The variable name 'tmp' is not very descriptive. Can we rename it?",
197
+ },
198
+ // Multi-line range comment
199
+ {
200
+ file: 'README.md',
201
+ range: {
202
+ start_line: 20,
203
+ end_line: 25,
204
+ },
205
+ message: 'This entire section needs updating',
206
+ },
207
+ // Comment with side parameter (PARENT)
208
+ {
209
+ file: 'config.xml',
210
+ line: 10,
211
+ side: 'PARENT',
212
+ message: 'Why was this configuration removed?',
213
+ },
214
+ // Comment with side parameter (REVISION)
215
+ {
216
+ file: 'config.xml',
217
+ line: 10,
218
+ side: 'REVISION',
219
+ message: 'Good improvement to the configuration',
220
+ },
221
+ // Unresolved comment
222
+ {
223
+ file: 'src/utils.js',
224
+ line: 42,
225
+ message: 'This needs to be fixed before merge',
226
+ unresolved: true,
227
+ },
228
+ // Range with side and unresolved
229
+ {
230
+ file: 'src/service.java',
231
+ range: {
232
+ start_line: 100,
233
+ start_character: 0,
234
+ end_line: 110,
235
+ end_character: 0,
236
+ },
237
+ side: 'REVISION',
238
+ message: 'This block has a potential memory leak',
239
+ unresolved: true,
240
+ },
241
+ ]),
242
+ )
243
+ }, 10)
244
+
245
+ await Effect.runPromise(program)
246
+
247
+ // Verify the request body structure
248
+ expect(capturedRequestBody).toBeDefined()
249
+ expect(capturedRequestBody.comments).toBeDefined()
250
+
251
+ // Check MyClass.java comments
252
+ const myClassComments = capturedRequestBody.comments['src/main/java/com/example/MyClass.java']
253
+ expect(myClassComments).toBeDefined()
254
+ expect(myClassComments.length).toBe(2)
255
+
256
+ expect(myClassComments[0]).toEqual({
257
+ line: 15,
258
+ message: 'Could you refactor this method to improve readability?',
259
+ })
260
+
261
+ expect(myClassComments[1]).toEqual({
262
+ range: {
263
+ start_line: 30,
264
+ start_character: 12,
265
+ end_line: 30,
266
+ end_character: 15,
267
+ },
268
+ message: "The variable name 'tmp' is not very descriptive. Can we rename it?",
269
+ })
270
+
271
+ // Check README.md comments
272
+ const readmeComments = capturedRequestBody.comments['README.md']
273
+ expect(readmeComments).toBeDefined()
274
+ expect(readmeComments.length).toBe(1)
275
+
276
+ expect(readmeComments[0]).toEqual({
277
+ range: {
278
+ start_line: 20,
279
+ end_line: 25,
280
+ },
281
+ message: 'This entire section needs updating',
282
+ })
283
+
284
+ // Check config.xml comments with side parameters
285
+ const configComments = capturedRequestBody.comments['config.xml']
286
+ expect(configComments).toBeDefined()
287
+ expect(configComments.length).toBe(2)
288
+
289
+ expect(configComments[0]).toEqual({
290
+ line: 10,
291
+ side: 'PARENT',
292
+ message: 'Why was this configuration removed?',
293
+ })
294
+
295
+ expect(configComments[1]).toEqual({
296
+ line: 10,
297
+ side: 'REVISION',
298
+ message: 'Good improvement to the configuration',
299
+ })
300
+
301
+ // Check utils.js unresolved comment
302
+ const utilsComments = capturedRequestBody.comments['src/utils.js']
303
+ expect(utilsComments).toBeDefined()
304
+ expect(utilsComments.length).toBe(1)
305
+
306
+ expect(utilsComments[0]).toEqual({
307
+ line: 42,
308
+ message: 'This needs to be fixed before merge',
309
+ unresolved: true,
310
+ })
311
+
312
+ // Check service.java range with side and unresolved
313
+ const serviceComments = capturedRequestBody.comments['src/service.java']
314
+ expect(serviceComments).toBeDefined()
315
+ expect(serviceComments.length).toBe(1)
316
+
317
+ expect(serviceComments[0]).toEqual({
318
+ range: {
319
+ start_line: 100,
320
+ start_character: 0,
321
+ end_line: 110,
322
+ end_character: 0,
323
+ },
324
+ side: 'REVISION',
325
+ message: 'This block has a potential memory leak',
326
+ unresolved: true,
327
+ })
328
+
329
+ // Restore process.stdin
330
+ Object.defineProperty(process, 'stdin', {
331
+ value: originalStdin,
332
+ configurable: true,
333
+ })
334
+ })
335
+
336
+ test('should handle comment without line when using range', async () => {
337
+ const originalStdin = process.stdin
338
+ Object.defineProperty(process, 'stdin', {
339
+ value: mockProcessStdin,
340
+ configurable: true,
341
+ })
342
+
343
+ let capturedRequestBody: any = null
344
+
345
+ server.use(
346
+ http.get('*/a/changes/:changeId', () => {
347
+ return HttpResponse.text(`)]}'\n{
348
+ "id": "test-project~main~I123abc",
349
+ "_number": 12345,
350
+ "project": "test-project",
351
+ "branch": "main",
352
+ "change_id": "I123abc",
353
+ "subject": "Test change",
354
+ "status": "NEW"
355
+ }`)
356
+ }),
357
+ http.post('*/a/changes/:changeId/revisions/current/review', async ({ request }) => {
358
+ capturedRequestBody = await request.json()
359
+ return HttpResponse.json({})
360
+ }),
361
+ )
362
+
363
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
364
+
365
+ const program = commentCommand('12345', { batch: true }).pipe(
366
+ Effect.provide(GerritApiServiceLive),
367
+ Effect.provide(mockConfigLayer),
368
+ )
369
+
370
+ // Test comment with only range, no line
371
+ setTimeout(() => {
372
+ mockProcessStdin.emit(
373
+ 'data',
374
+ JSON.stringify([
375
+ {
376
+ file: 'src/main.java',
377
+ range: {
378
+ start_line: 10,
379
+ end_line: 15,
380
+ },
381
+ message: 'This should work without a line property',
382
+ },
383
+ ]),
384
+ )
385
+ }, 10)
386
+
387
+ await Effect.runPromise(program)
388
+
389
+ // Verify the comment has range but no line property
390
+ expect(capturedRequestBody).toBeDefined()
391
+ expect(capturedRequestBody.comments).toBeDefined()
392
+
393
+ const comments = capturedRequestBody.comments['src/main.java']
394
+ expect(comments).toBeDefined()
395
+ expect(comments.length).toBe(1)
396
+
397
+ expect(comments[0]).toEqual({
398
+ range: {
399
+ start_line: 10,
400
+ end_line: 15,
401
+ },
402
+ message: 'This should work without a line property',
403
+ })
404
+
405
+ // Ensure no 'line' property is present when using range
406
+ expect(comments[0].line).toBeUndefined()
407
+
408
+ // Restore process.stdin
409
+ Object.defineProperty(process, 'stdin', {
410
+ value: originalStdin,
411
+ configurable: true,
412
+ })
413
+ })
414
+ })