@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,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
|
+
})
|