@aaronshaf/ger 0.1.10 → 0.2.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/.github/workflows/claude-code-review.yml +61 -56
- package/.github/workflows/claude.yml +10 -24
- package/README.md +53 -6
- package/bun.lock +8 -8
- package/package.json +3 -3
- package/src/api/gerrit.ts +54 -16
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/review.ts +13 -2
- package/src/cli/commands/setup.ts +1 -1
- package/src/cli/commands/show.ts +112 -18
- package/src/cli/index.ts +140 -23
- package/src/schemas/config.ts +13 -4
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +60 -16
- package/src/services/git-worktree.ts +73 -16
- package/src/services/review-strategy.ts +40 -22
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/extract-url.test.ts +518 -0
- package/tests/mocks/fetch-mock.ts +5 -2
- package/tests/mocks/msw-handlers.ts +3 -3
- package/tests/show-auto-detect.test.ts +306 -0
- package/tests/show.test.ts +157 -1
- package/tests/unit/git-worktree.test.ts +2 -1
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach, mock } from 'bun:test'
|
|
2
|
+
import { setupServer } from 'msw/node'
|
|
3
|
+
import { http, HttpResponse } from 'msw'
|
|
4
|
+
import { Effect, Layer } from 'effect'
|
|
5
|
+
import { showCommand } from '@/cli/commands/show'
|
|
6
|
+
import { commentCommand } from '@/cli/commands/comment'
|
|
7
|
+
import { diffCommand } from '@/cli/commands/diff'
|
|
8
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
9
|
+
import { ConfigService } from '@/services/config'
|
|
10
|
+
import { generateMockChange } from '@/test-utils/mock-generator'
|
|
11
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Integration tests to verify that commands accept both change number and Change-ID formats
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const CHANGE_NUMBER = '392385'
|
|
18
|
+
const CHANGE_ID = 'If5a3ae8cb5a107e187447802358417f311d0c4b1'
|
|
19
|
+
|
|
20
|
+
const mockChange = generateMockChange({
|
|
21
|
+
_number: 392385,
|
|
22
|
+
change_id: CHANGE_ID,
|
|
23
|
+
subject: 'WIP: test',
|
|
24
|
+
status: 'NEW',
|
|
25
|
+
project: 'canvas-lms',
|
|
26
|
+
branch: 'master',
|
|
27
|
+
created: '2024-01-15 10:00:00.000000000',
|
|
28
|
+
updated: '2024-01-15 12:00:00.000000000',
|
|
29
|
+
owner: {
|
|
30
|
+
_account_id: 1001,
|
|
31
|
+
name: 'Test User',
|
|
32
|
+
email: 'test@example.com',
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const mockDiff = `--- a/test.txt
|
|
37
|
+
+++ b/test.txt
|
|
38
|
+
@@ -1,1 +1,2 @@
|
|
39
|
+
original line
|
|
40
|
+
+new line`
|
|
41
|
+
|
|
42
|
+
const server = setupServer(
|
|
43
|
+
http.get('*/a/accounts/self', () => {
|
|
44
|
+
return HttpResponse.json({
|
|
45
|
+
_account_id: 1000,
|
|
46
|
+
name: 'Test User',
|
|
47
|
+
email: 'test@example.com',
|
|
48
|
+
})
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
// Handler that matches both change number and Change-ID
|
|
52
|
+
http.get('*/a/changes/:changeId', ({ params }) => {
|
|
53
|
+
const { changeId } = params
|
|
54
|
+
// Accept both formats
|
|
55
|
+
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
|
|
56
|
+
return HttpResponse.text(`)]}'
|
|
57
|
+
${JSON.stringify(mockChange)}`)
|
|
58
|
+
}
|
|
59
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', ({ params }) => {
|
|
63
|
+
const { changeId } = params
|
|
64
|
+
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
|
|
65
|
+
return HttpResponse.text(btoa(mockDiff))
|
|
66
|
+
}
|
|
67
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', ({ params }) => {
|
|
71
|
+
const { changeId } = params
|
|
72
|
+
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
|
|
73
|
+
return HttpResponse.text(`)]}'
|
|
74
|
+
{}`)
|
|
75
|
+
}
|
|
76
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
http.post('*/a/changes/:changeId/revisions/current/review', async ({ params, request }) => {
|
|
80
|
+
const { changeId } = params
|
|
81
|
+
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
|
|
82
|
+
return HttpResponse.text(`)]}'
|
|
83
|
+
{}`)
|
|
84
|
+
}
|
|
85
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
let capturedLogs: string[] = []
|
|
90
|
+
let capturedErrors: string[] = []
|
|
91
|
+
|
|
92
|
+
const mockConsoleLog = mock((...args: any[]) => {
|
|
93
|
+
capturedLogs.push(args.join(' '))
|
|
94
|
+
})
|
|
95
|
+
const mockConsoleError = mock((...args: any[]) => {
|
|
96
|
+
capturedErrors.push(args.join(' '))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const originalConsoleLog = console.log
|
|
100
|
+
const originalConsoleError = console.error
|
|
101
|
+
|
|
102
|
+
beforeAll(() => {
|
|
103
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
console.log = mockConsoleLog
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
console.error = mockConsoleError
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
afterAll(() => {
|
|
111
|
+
server.close()
|
|
112
|
+
console.log = originalConsoleLog
|
|
113
|
+
console.error = originalConsoleError
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
server.resetHandlers()
|
|
118
|
+
mockConsoleLog.mockClear()
|
|
119
|
+
mockConsoleError.mockClear()
|
|
120
|
+
capturedLogs = []
|
|
121
|
+
capturedErrors = []
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const createMockConfigLayer = (): Layer.Layer<ConfigService, never, never> =>
|
|
125
|
+
Layer.succeed(ConfigService, createMockConfigService())
|
|
126
|
+
|
|
127
|
+
describe('Change ID format support', () => {
|
|
128
|
+
describe('show command', () => {
|
|
129
|
+
test('accepts numeric change number', async () => {
|
|
130
|
+
const effect = showCommand(CHANGE_NUMBER, {}).pipe(
|
|
131
|
+
Effect.provide(GerritApiServiceLive),
|
|
132
|
+
Effect.provide(createMockConfigLayer()),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
await Effect.runPromise(effect)
|
|
136
|
+
|
|
137
|
+
const output = capturedLogs.join('\n')
|
|
138
|
+
expect(output).toContain('Change 392385')
|
|
139
|
+
expect(output).toContain('WIP: test')
|
|
140
|
+
expect(capturedErrors.length).toBe(0)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('accepts Change-ID format', async () => {
|
|
144
|
+
const effect = showCommand(CHANGE_ID, {}).pipe(
|
|
145
|
+
Effect.provide(GerritApiServiceLive),
|
|
146
|
+
Effect.provide(createMockConfigLayer()),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
await Effect.runPromise(effect)
|
|
150
|
+
|
|
151
|
+
const output = capturedLogs.join('\n')
|
|
152
|
+
expect(output).toContain('Change 392385')
|
|
153
|
+
expect(output).toContain('WIP: test')
|
|
154
|
+
expect(capturedErrors.length).toBe(0)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('rejects invalid change identifier', async () => {
|
|
158
|
+
const effect = showCommand('invalid-id', {}).pipe(
|
|
159
|
+
Effect.provide(GerritApiServiceLive),
|
|
160
|
+
Effect.provide(createMockConfigLayer()),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
await Effect.runPromise(effect)
|
|
164
|
+
|
|
165
|
+
const output = capturedErrors.join('\n')
|
|
166
|
+
expect(output).toContain('Invalid change identifier')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('diff command', () => {
|
|
171
|
+
test('accepts numeric change number', async () => {
|
|
172
|
+
const effect = diffCommand(CHANGE_NUMBER, {}).pipe(
|
|
173
|
+
Effect.provide(GerritApiServiceLive),
|
|
174
|
+
Effect.provide(createMockConfigLayer()),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
await Effect.runPromise(effect)
|
|
178
|
+
|
|
179
|
+
const output = capturedLogs.join('\n')
|
|
180
|
+
expect(output).toContain('--- a/test.txt')
|
|
181
|
+
expect(output).toContain('+++ b/test.txt')
|
|
182
|
+
expect(capturedErrors.length).toBe(0)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('accepts Change-ID format', async () => {
|
|
186
|
+
const effect = diffCommand(CHANGE_ID, {}).pipe(
|
|
187
|
+
Effect.provide(GerritApiServiceLive),
|
|
188
|
+
Effect.provide(createMockConfigLayer()),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
await Effect.runPromise(effect)
|
|
192
|
+
|
|
193
|
+
const output = capturedLogs.join('\n')
|
|
194
|
+
expect(output).toContain('--- a/test.txt')
|
|
195
|
+
expect(output).toContain('+++ b/test.txt')
|
|
196
|
+
expect(capturedErrors.length).toBe(0)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('comment command', () => {
|
|
201
|
+
test('accepts numeric change number', async () => {
|
|
202
|
+
const effect = commentCommand(CHANGE_NUMBER, { message: 'LGTM' }).pipe(
|
|
203
|
+
Effect.provide(GerritApiServiceLive),
|
|
204
|
+
Effect.provide(createMockConfigLayer()),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
await Effect.runPromise(effect)
|
|
208
|
+
|
|
209
|
+
const output = capturedLogs.join('\n')
|
|
210
|
+
expect(output).toContain('Comment posted successfully')
|
|
211
|
+
expect(capturedErrors.length).toBe(0)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('accepts Change-ID format', async () => {
|
|
215
|
+
const effect = commentCommand(CHANGE_ID, { message: 'LGTM' }).pipe(
|
|
216
|
+
Effect.provide(GerritApiServiceLive),
|
|
217
|
+
Effect.provide(createMockConfigLayer()),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
await Effect.runPromise(effect)
|
|
221
|
+
|
|
222
|
+
const output = capturedLogs.join('\n')
|
|
223
|
+
expect(output).toContain('Comment posted successfully')
|
|
224
|
+
expect(capturedErrors.length).toBe(0)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('edge cases', () => {
|
|
229
|
+
test('trims whitespace from change identifiers', async () => {
|
|
230
|
+
const effect = showCommand(` ${CHANGE_NUMBER} `, {}).pipe(
|
|
231
|
+
Effect.provide(GerritApiServiceLive),
|
|
232
|
+
Effect.provide(createMockConfigLayer()),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
await Effect.runPromise(effect)
|
|
236
|
+
|
|
237
|
+
const output = capturedLogs.join('\n')
|
|
238
|
+
expect(output).toContain('Change 392385')
|
|
239
|
+
expect(capturedErrors.length).toBe(0)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('validates Change-ID format strictly (uppercase I)', async () => {
|
|
243
|
+
const lowercaseChangeId = 'if5a3ae8cb5a107e187447802358417f311d0c4b1'
|
|
244
|
+
const effect = showCommand(lowercaseChangeId, {}).pipe(
|
|
245
|
+
Effect.provide(GerritApiServiceLive),
|
|
246
|
+
Effect.provide(createMockConfigLayer()),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
await Effect.runPromise(effect)
|
|
250
|
+
|
|
251
|
+
const output = capturedErrors.join('\n')
|
|
252
|
+
expect(output).toContain('Invalid change identifier')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('rejects Change-ID with incorrect length', async () => {
|
|
256
|
+
const shortChangeId = 'If5a3ae8cb5a107e18744780235841'
|
|
257
|
+
const effect = showCommand(shortChangeId, {}).pipe(
|
|
258
|
+
Effect.provide(GerritApiServiceLive),
|
|
259
|
+
Effect.provide(createMockConfigLayer()),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
await Effect.runPromise(effect)
|
|
263
|
+
|
|
264
|
+
const output = capturedErrors.join('\n')
|
|
265
|
+
expect(output).toContain('Invalid change identifier')
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
})
|