@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,233 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+ import { http, HttpResponse } from 'msw'
3
+ import { setupServer } from 'msw/node'
4
+ import { Layer } from 'effect'
5
+ import { Effect } from 'effect'
6
+ import { GerritApiServiceLive } from '@/api/gerrit'
7
+ import { openCommand } from '@/cli/commands/open'
8
+ import { ConfigService } from '@/services/config'
9
+
10
+ import { createMockConfigService } from './helpers/config-mock'
11
+ // Mock child_process.exec
12
+ const mockExec = mock()
13
+ mock.module('node:child_process', () => ({
14
+ exec: mockExec,
15
+ }))
16
+
17
+ const server = setupServer()
18
+
19
+ beforeAll(() => {
20
+ server.listen()
21
+ })
22
+
23
+ beforeEach(() => {
24
+ mockExec.mockClear()
25
+ })
26
+
27
+ afterAll(() => {
28
+ server.close()
29
+ })
30
+
31
+ describe('open command', () => {
32
+ test('should open change URL in browser', async () => {
33
+ // Mock the exec function to simulate successful browser opening
34
+ mockExec.mockImplementation((cmd: string, callback: (error: null) => void) => {
35
+ expect(cmd).toMatch(
36
+ /^(open|start|xdg-open) "https:\/\/gerrit\.example\.com\/c\/test-project\/\+\/12345"$/,
37
+ )
38
+ callback(null)
39
+ })
40
+
41
+ server.use(
42
+ http.get('*/a/changes/12345', () => {
43
+ return HttpResponse.text(
44
+ `)]}'\n${JSON.stringify({
45
+ id: 'test-project~main~I1234567890abcdef',
46
+ project: 'test-project',
47
+ branch: 'main',
48
+ change_id: 'I1234567890abcdef',
49
+ subject: 'Test change',
50
+ status: 'NEW',
51
+ _number: 12345,
52
+ owner: {
53
+ _account_id: 1000000,
54
+ name: 'Test User',
55
+ email: 'test@example.com',
56
+ },
57
+ })}`,
58
+ )
59
+ }),
60
+ )
61
+
62
+ const mockConfigLayer = Layer.succeed(
63
+ ConfigService,
64
+ createMockConfigService({
65
+ host: 'https://gerrit.example.com',
66
+ username: 'testuser',
67
+ password: 'testpass',
68
+ }),
69
+ )
70
+
71
+ const consoleSpy = mock(() => {})
72
+ const originalLog = console.log
73
+ console.log = consoleSpy
74
+
75
+ const program = openCommand('12345').pipe(
76
+ Effect.provide(GerritApiServiceLive),
77
+ Effect.provide(mockConfigLayer),
78
+ )
79
+
80
+ await Effect.runPromise(program)
81
+
82
+ console.log = originalLog
83
+
84
+ expect(mockExec).toHaveBeenCalledTimes(1)
85
+ expect(consoleSpy).toHaveBeenCalledWith(
86
+ 'Opened: https://gerrit.example.com/c/test-project/+/12345',
87
+ )
88
+ })
89
+
90
+ test('should handle URLs and extract change number', async () => {
91
+ mockExec.mockImplementation((cmd: string, callback: (error: null) => void) => {
92
+ expect(cmd).toMatch(
93
+ /^(open|start|xdg-open) "https:\/\/gerrit\.example\.com\/c\/test-project\/\+\/12345"$/,
94
+ )
95
+ callback(null)
96
+ })
97
+
98
+ server.use(
99
+ http.get('*/a/changes/12345', () => {
100
+ return HttpResponse.text(
101
+ `)]}'\n${JSON.stringify({
102
+ id: 'test-project~main~I1234567890abcdef',
103
+ project: 'test-project',
104
+ branch: 'main',
105
+ change_id: 'I1234567890abcdef',
106
+ subject: 'Test change',
107
+ status: 'NEW',
108
+ _number: 12345,
109
+ owner: {
110
+ _account_id: 1000000,
111
+ name: 'Test User',
112
+ },
113
+ })}`,
114
+ )
115
+ }),
116
+ )
117
+
118
+ const mockConfigLayer = Layer.succeed(
119
+ ConfigService,
120
+ createMockConfigService({
121
+ host: 'https://gerrit.example.com',
122
+ username: 'testuser',
123
+ password: 'testpass',
124
+ }),
125
+ )
126
+
127
+ const consoleSpy = mock(() => {})
128
+ const originalLog = console.log
129
+ console.log = consoleSpy
130
+
131
+ // Test with a full Gerrit URL
132
+ const program = openCommand('https://gerrit.example.com/c/test-project/+/12345').pipe(
133
+ Effect.provide(GerritApiServiceLive),
134
+ Effect.provide(mockConfigLayer),
135
+ )
136
+
137
+ await Effect.runPromise(program)
138
+
139
+ console.log = originalLog
140
+
141
+ expect(mockExec).toHaveBeenCalledTimes(1)
142
+ expect(consoleSpy).toHaveBeenCalledWith(
143
+ 'Opened: https://gerrit.example.com/c/test-project/+/12345',
144
+ )
145
+ })
146
+
147
+ test('should handle invalid change ID', async () => {
148
+ const mockConfigLayer = Layer.succeed(
149
+ ConfigService,
150
+ createMockConfigService({
151
+ host: 'https://gerrit.example.com',
152
+ username: 'testuser',
153
+ password: 'testpass',
154
+ }),
155
+ )
156
+
157
+ // Use an empty string which is truly invalid according to isValidChangeId
158
+ const program = openCommand('').pipe(
159
+ Effect.provide(GerritApiServiceLive),
160
+ Effect.provide(mockConfigLayer),
161
+ )
162
+
163
+ await expect(Effect.runPromise(program)).rejects.toThrow('Invalid change ID: ')
164
+ })
165
+
166
+ test('should handle API errors gracefully', async () => {
167
+ server.use(
168
+ http.get('*/a/changes/12345', () => {
169
+ return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
170
+ }),
171
+ )
172
+
173
+ const mockConfigLayer = Layer.succeed(
174
+ ConfigService,
175
+ createMockConfigService({
176
+ host: 'https://gerrit.example.com',
177
+ username: 'testuser',
178
+ password: 'testpass',
179
+ }),
180
+ )
181
+
182
+ const program = openCommand('12345').pipe(
183
+ Effect.provide(GerritApiServiceLive),
184
+ Effect.provide(mockConfigLayer),
185
+ )
186
+
187
+ await expect(Effect.runPromise(program)).rejects.toThrow()
188
+ })
189
+
190
+ test('should handle browser opening errors', async () => {
191
+ mockExec.mockImplementation((cmd: string, callback: (error: Error) => void) => {
192
+ callback(new Error('Browser not found'))
193
+ })
194
+
195
+ server.use(
196
+ http.get('*/a/changes/12345', () => {
197
+ return HttpResponse.text(
198
+ `)]}'\n${JSON.stringify({
199
+ id: 'test-project~main~I1234567890abcdef',
200
+ project: 'test-project',
201
+ branch: 'main',
202
+ change_id: 'I1234567890abcdef',
203
+ subject: 'Test change',
204
+ status: 'NEW',
205
+ _number: 12345,
206
+ owner: {
207
+ _account_id: 1000000,
208
+ name: 'Test User',
209
+ },
210
+ })}`,
211
+ )
212
+ }),
213
+ )
214
+
215
+ const mockConfigLayer = Layer.succeed(
216
+ ConfigService,
217
+ createMockConfigService({
218
+ host: 'https://gerrit.example.com',
219
+ username: 'testuser',
220
+ password: 'testpass',
221
+ }),
222
+ )
223
+
224
+ const program = openCommand('12345').pipe(
225
+ Effect.provide(GerritApiServiceLive),
226
+ Effect.provide(mockConfigLayer),
227
+ )
228
+
229
+ await expect(Effect.runPromise(program)).rejects.toThrow(
230
+ 'Failed to open URL: Browser not found',
231
+ )
232
+ })
233
+ })