@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,439 @@
|
|
|
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 { GerritApiServiceLive } from '@/api/gerrit'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { generateMockChange } from '@/test-utils/mock-generator'
|
|
9
|
+
import type { MessageInfo } from '@/schemas/gerrit'
|
|
10
|
+
|
|
11
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
12
|
+
const server = setupServer(
|
|
13
|
+
// Default handler for auth check
|
|
14
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
15
|
+
const auth = request.headers.get('Authorization')
|
|
16
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
17
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
18
|
+
}
|
|
19
|
+
return HttpResponse.json({
|
|
20
|
+
_account_id: 1000,
|
|
21
|
+
name: 'Test User',
|
|
22
|
+
email: 'test@example.com',
|
|
23
|
+
})
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Store captured output
|
|
28
|
+
let capturedLogs: string[] = []
|
|
29
|
+
let capturedErrors: string[] = []
|
|
30
|
+
|
|
31
|
+
// Mock console.log and console.error
|
|
32
|
+
const mockConsoleLog = mock((...args: any[]) => {
|
|
33
|
+
capturedLogs.push(args.join(' '))
|
|
34
|
+
})
|
|
35
|
+
const mockConsoleError = mock((...args: any[]) => {
|
|
36
|
+
capturedErrors.push(args.join(' '))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Store original console methods
|
|
40
|
+
const originalConsoleLog = console.log
|
|
41
|
+
const originalConsoleError = console.error
|
|
42
|
+
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
console.log = mockConsoleLog
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
console.error = mockConsoleError
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterAll(() => {
|
|
52
|
+
server.close()
|
|
53
|
+
console.log = originalConsoleLog
|
|
54
|
+
console.error = originalConsoleError
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
server.resetHandlers()
|
|
59
|
+
mockConsoleLog.mockClear()
|
|
60
|
+
mockConsoleError.mockClear()
|
|
61
|
+
capturedLogs = []
|
|
62
|
+
capturedErrors = []
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('show command', () => {
|
|
66
|
+
const mockChange = generateMockChange({
|
|
67
|
+
_number: 12345,
|
|
68
|
+
change_id: 'I123abc456def',
|
|
69
|
+
subject: 'Fix authentication bug',
|
|
70
|
+
status: 'NEW',
|
|
71
|
+
project: 'test-project',
|
|
72
|
+
branch: 'main',
|
|
73
|
+
created: '2024-01-15 10:00:00.000000000',
|
|
74
|
+
updated: '2024-01-15 12:00:00.000000000',
|
|
75
|
+
owner: {
|
|
76
|
+
_account_id: 1001,
|
|
77
|
+
name: 'John Doe',
|
|
78
|
+
email: 'john@example.com',
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const mockDiff = `--- a/src/auth.js
|
|
83
|
+
+++ b/src/auth.js
|
|
84
|
+
@@ -10,7 +10,8 @@ function authenticate(user) {
|
|
85
|
+
if (!user) {
|
|
86
|
+
- return false
|
|
87
|
+
+ throw new Error('User required')
|
|
88
|
+
}
|
|
89
|
+
+ // Added validation
|
|
90
|
+
return validateUser(user)
|
|
91
|
+
}`
|
|
92
|
+
|
|
93
|
+
const mockComments = {
|
|
94
|
+
'src/auth.js': [
|
|
95
|
+
{
|
|
96
|
+
id: 'comment1',
|
|
97
|
+
path: 'src/auth.js',
|
|
98
|
+
line: 12,
|
|
99
|
+
message: 'Good improvement!',
|
|
100
|
+
author: {
|
|
101
|
+
name: 'Jane Reviewer',
|
|
102
|
+
email: 'jane@example.com',
|
|
103
|
+
},
|
|
104
|
+
updated: '2024-01-15 11:30:00.000000000',
|
|
105
|
+
unresolved: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'comment2',
|
|
109
|
+
path: 'src/auth.js',
|
|
110
|
+
line: 14,
|
|
111
|
+
message: 'Consider adding JSDoc',
|
|
112
|
+
author: {
|
|
113
|
+
name: 'Bob Reviewer',
|
|
114
|
+
email: 'bob@example.com',
|
|
115
|
+
},
|
|
116
|
+
updated: '2024-01-15 11:45:00.000000000',
|
|
117
|
+
unresolved: true,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
'/COMMIT_MSG': [
|
|
121
|
+
{
|
|
122
|
+
id: 'comment3',
|
|
123
|
+
path: '/COMMIT_MSG',
|
|
124
|
+
line: 1,
|
|
125
|
+
message: 'Clear commit message',
|
|
126
|
+
author: {
|
|
127
|
+
name: 'Alice Lead',
|
|
128
|
+
email: 'alice@example.com',
|
|
129
|
+
},
|
|
130
|
+
updated: '2024-01-15 11:00:00.000000000',
|
|
131
|
+
unresolved: false,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const setupMockHandlers = () => {
|
|
137
|
+
server.use(
|
|
138
|
+
// Get change details
|
|
139
|
+
http.get('*/a/changes/:changeId', () => {
|
|
140
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
141
|
+
}),
|
|
142
|
+
// Get diff (returns base64-encoded content)
|
|
143
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
144
|
+
return HttpResponse.text(btoa(mockDiff))
|
|
145
|
+
}),
|
|
146
|
+
// Get comments
|
|
147
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
148
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
|
|
149
|
+
}),
|
|
150
|
+
// Get file diff for context (optional, may fail gracefully)
|
|
151
|
+
http.get('*/a/changes/:changeId/revisions/current/files/:fileName/diff', () => {
|
|
152
|
+
return HttpResponse.text(mockDiff)
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const createMockConfigLayer = () => Layer.succeed(ConfigService, createMockConfigService())
|
|
158
|
+
|
|
159
|
+
test('should display comprehensive change information in pretty format', async () => {
|
|
160
|
+
setupMockHandlers()
|
|
161
|
+
|
|
162
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
163
|
+
const program = showCommand('12345', {}).pipe(
|
|
164
|
+
Effect.provide(GerritApiServiceLive),
|
|
165
|
+
Effect.provide(mockConfigLayer),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
await Effect.runPromise(program)
|
|
169
|
+
|
|
170
|
+
const output = capturedLogs.join('\n')
|
|
171
|
+
|
|
172
|
+
// Check that all sections are present
|
|
173
|
+
expect(output).toContain('📋 Change 12345: Fix authentication bug')
|
|
174
|
+
expect(output).toContain('📝 Details:')
|
|
175
|
+
expect(output).toContain('Project: test-project')
|
|
176
|
+
expect(output).toContain('Branch: main')
|
|
177
|
+
expect(output).toContain('Status: NEW')
|
|
178
|
+
expect(output).toContain('Owner: John Doe')
|
|
179
|
+
expect(output).toContain('Change-Id: I123abc456def')
|
|
180
|
+
expect(output).toContain('🔍 Diff:')
|
|
181
|
+
expect(output).toContain('💬 Inline Comments:')
|
|
182
|
+
|
|
183
|
+
// Check diff content is included
|
|
184
|
+
expect(output).toContain('src/auth.js')
|
|
185
|
+
expect(output).toContain('authenticate(user)')
|
|
186
|
+
|
|
187
|
+
// Check comments are included
|
|
188
|
+
expect(output).toContain('Good improvement!')
|
|
189
|
+
expect(output).toContain('Consider adding JSDoc')
|
|
190
|
+
expect(output).toContain('Clear commit message')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('should output XML format when --xml flag is used', async () => {
|
|
194
|
+
setupMockHandlers()
|
|
195
|
+
|
|
196
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
197
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
198
|
+
Effect.provide(GerritApiServiceLive),
|
|
199
|
+
Effect.provide(mockConfigLayer),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
await Effect.runPromise(program)
|
|
203
|
+
|
|
204
|
+
const output = capturedLogs.join('\n')
|
|
205
|
+
|
|
206
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
207
|
+
expect(output).toContain('<show_result>')
|
|
208
|
+
expect(output).toContain('<status>success</status>')
|
|
209
|
+
expect(output).toContain('<change>')
|
|
210
|
+
expect(output).toContain('<id>I123abc456def</id>')
|
|
211
|
+
expect(output).toContain('<number>12345</number>')
|
|
212
|
+
expect(output).toContain('<subject><![CDATA[Fix authentication bug]]></subject>')
|
|
213
|
+
expect(output).toContain('<status>NEW</status>')
|
|
214
|
+
expect(output).toContain('<project>test-project</project>')
|
|
215
|
+
expect(output).toContain('<branch>main</branch>')
|
|
216
|
+
expect(output).toContain('<owner>')
|
|
217
|
+
expect(output).toContain('<name><![CDATA[John Doe]]></name>')
|
|
218
|
+
expect(output).toContain('<email>john@example.com</email>')
|
|
219
|
+
expect(output).toContain('<diff><![CDATA[')
|
|
220
|
+
expect(output).toContain('<comments>')
|
|
221
|
+
expect(output).toContain('<count>3</count>')
|
|
222
|
+
expect(output).toContain('</show_result>')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('should handle API errors gracefully in pretty format', async () => {
|
|
226
|
+
server.use(
|
|
227
|
+
http.get('*/a/changes/:changeId', () => {
|
|
228
|
+
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
233
|
+
const program = showCommand('12345', {}).pipe(
|
|
234
|
+
Effect.provide(GerritApiServiceLive),
|
|
235
|
+
Effect.provide(mockConfigLayer),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
await Effect.runPromise(program)
|
|
239
|
+
|
|
240
|
+
const output = capturedErrors.join('\n')
|
|
241
|
+
expect(output).toContain('✗ Failed to fetch change details')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('should handle API errors gracefully in XML format', async () => {
|
|
245
|
+
server.use(
|
|
246
|
+
http.get('*/a/changes/:changeId', () => {
|
|
247
|
+
return HttpResponse.json({ error: 'Change not found' }, { status: 404 })
|
|
248
|
+
}),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
252
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
253
|
+
Effect.provide(GerritApiServiceLive),
|
|
254
|
+
Effect.provide(mockConfigLayer),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
await Effect.runPromise(program)
|
|
258
|
+
|
|
259
|
+
const output = capturedLogs.join('\n')
|
|
260
|
+
|
|
261
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
262
|
+
expect(output).toContain('<show_result>')
|
|
263
|
+
expect(output).toContain('<status>error</status>')
|
|
264
|
+
expect(output).toContain('<error><![CDATA[')
|
|
265
|
+
expect(output).toContain('</show_result>')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('should properly escape XML special characters', async () => {
|
|
269
|
+
const changeWithSpecialChars = generateMockChange({
|
|
270
|
+
_number: 12345,
|
|
271
|
+
change_id: 'I123abc456def',
|
|
272
|
+
subject: 'Fix "quotes" & <tags> in auth',
|
|
273
|
+
project: 'test-project',
|
|
274
|
+
branch: 'feature/fix&improve',
|
|
275
|
+
owner: {
|
|
276
|
+
_account_id: 1002,
|
|
277
|
+
name: 'User <with> & "special" chars',
|
|
278
|
+
email: 'user@example.com',
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
server.use(
|
|
283
|
+
http.get('*/a/changes/:changeId', () => {
|
|
284
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithSpecialChars)}`)
|
|
285
|
+
}),
|
|
286
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
287
|
+
return HttpResponse.text('diff content')
|
|
288
|
+
}),
|
|
289
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
290
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
295
|
+
const program = showCommand('12345', { xml: true }).pipe(
|
|
296
|
+
Effect.provide(GerritApiServiceLive),
|
|
297
|
+
Effect.provide(mockConfigLayer),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
await Effect.runPromise(program)
|
|
301
|
+
|
|
302
|
+
const output = capturedLogs.join('\n')
|
|
303
|
+
|
|
304
|
+
expect(output).toContain('<subject><![CDATA[Fix "quotes" & <tags> in auth]]></subject>')
|
|
305
|
+
expect(output).toContain('<branch>feature/fix&improve</branch>')
|
|
306
|
+
expect(output).toContain('<name><![CDATA[User <with> & "special" chars]]></name>')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('should handle mixed file and commit message comments', async () => {
|
|
310
|
+
setupMockHandlers()
|
|
311
|
+
|
|
312
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
313
|
+
const program = showCommand('12345', {}).pipe(
|
|
314
|
+
Effect.provide(GerritApiServiceLive),
|
|
315
|
+
Effect.provide(mockConfigLayer),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
await Effect.runPromise(program)
|
|
319
|
+
|
|
320
|
+
const output = capturedLogs.join('\n')
|
|
321
|
+
|
|
322
|
+
// Should show comments from both files and commit message
|
|
323
|
+
expect(output).toContain('Good improvement!')
|
|
324
|
+
expect(output).toContain('Consider adding JSDoc')
|
|
325
|
+
expect(output).toContain('Clear commit message')
|
|
326
|
+
|
|
327
|
+
// Commit message path should be renamed
|
|
328
|
+
expect(output).toContain('Commit Message')
|
|
329
|
+
expect(output).not.toContain('/COMMIT_MSG')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('should handle changes with missing optional fields', async () => {
|
|
333
|
+
const minimalChange = generateMockChange({
|
|
334
|
+
_number: 12345,
|
|
335
|
+
change_id: 'I123abc456def',
|
|
336
|
+
subject: 'Minimal change',
|
|
337
|
+
status: 'NEW',
|
|
338
|
+
project: 'test-project',
|
|
339
|
+
branch: 'main',
|
|
340
|
+
owner: {
|
|
341
|
+
_account_id: 1003,
|
|
342
|
+
email: 'user@example.com',
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
server.use(
|
|
347
|
+
http.get('*/a/changes/:changeId', () => {
|
|
348
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(minimalChange)}`)
|
|
349
|
+
}),
|
|
350
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
351
|
+
return HttpResponse.text('minimal diff')
|
|
352
|
+
}),
|
|
353
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
354
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
355
|
+
}),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
359
|
+
const program = showCommand('12345', {}).pipe(
|
|
360
|
+
Effect.provide(GerritApiServiceLive),
|
|
361
|
+
Effect.provide(mockConfigLayer),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
await Effect.runPromise(program)
|
|
365
|
+
|
|
366
|
+
const output = capturedLogs.join('\n')
|
|
367
|
+
|
|
368
|
+
expect(output).toContain('📋 Change 12345: Minimal change')
|
|
369
|
+
expect(output).toContain('Owner: user@example.com') // Should fallback to email
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('should display review activity messages', async () => {
|
|
373
|
+
const mockChange = generateMockChange({
|
|
374
|
+
_number: 12345,
|
|
375
|
+
subject: 'Fix authentication bug',
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const mockMessages: MessageInfo[] = [
|
|
379
|
+
{
|
|
380
|
+
id: 'msg1',
|
|
381
|
+
message: 'Patch Set 2: Code-Review+2',
|
|
382
|
+
author: { _account_id: 1001, name: 'Jane Reviewer' },
|
|
383
|
+
date: '2024-01-15 11:30:00.000000000',
|
|
384
|
+
_revision_number: 2,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: 'msg2',
|
|
388
|
+
message: 'Patch Set 2: Verified+1\\n\\nBuild Successful',
|
|
389
|
+
author: { _account_id: 1002, name: 'Jenkins Bot' },
|
|
390
|
+
date: '2024-01-15 11:31:00.000000000',
|
|
391
|
+
_revision_number: 2,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: 'msg3',
|
|
395
|
+
message: 'Uploaded patch set 1.',
|
|
396
|
+
author: { _account_id: 1000, name: 'Author' },
|
|
397
|
+
date: '2024-01-15 11:29:00.000000000',
|
|
398
|
+
tag: 'autogenerated:gerrit:newPatchSet',
|
|
399
|
+
_revision_number: 1,
|
|
400
|
+
},
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
server.use(
|
|
404
|
+
http.get('*/a/changes/:changeId', ({ request }) => {
|
|
405
|
+
const url = new URL(request.url)
|
|
406
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
407
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: mockMessages })}`)
|
|
408
|
+
}
|
|
409
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
410
|
+
}),
|
|
411
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
412
|
+
return HttpResponse.text('diff content')
|
|
413
|
+
}),
|
|
414
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
415
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
420
|
+
const program = showCommand('12345', {}).pipe(
|
|
421
|
+
Effect.provide(GerritApiServiceLive),
|
|
422
|
+
Effect.provide(mockConfigLayer),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
await Effect.runPromise(program)
|
|
426
|
+
|
|
427
|
+
const output = capturedLogs.join('\n')
|
|
428
|
+
|
|
429
|
+
// Should display review activity section
|
|
430
|
+
expect(output).toContain('📝 Review Activity:')
|
|
431
|
+
expect(output).toContain('Jane Reviewer')
|
|
432
|
+
expect(output).toContain('Code-Review+2')
|
|
433
|
+
expect(output).toContain('Jenkins Bot')
|
|
434
|
+
expect(output).toContain('Build Successful')
|
|
435
|
+
|
|
436
|
+
// Should filter out autogenerated messages
|
|
437
|
+
expect(output).not.toContain('Uploaded patch set')
|
|
438
|
+
})
|
|
439
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Schema } from '@effect/schema'
|
|
3
|
+
import { CommentInput, GerritCredentials } from '@/schemas/gerrit'
|
|
4
|
+
|
|
5
|
+
describe('Gerrit Schemas', () => {
|
|
6
|
+
describe('GerritCredentials', () => {
|
|
7
|
+
test('should validate valid credentials', () => {
|
|
8
|
+
const validCredentials = {
|
|
9
|
+
host: 'https://gerrit.example.com',
|
|
10
|
+
username: 'testuser',
|
|
11
|
+
password: 'testpass123',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result = Schema.decodeUnknownSync(GerritCredentials)(validCredentials)
|
|
15
|
+
expect(result).toEqual(validCredentials)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('should reject invalid URL', () => {
|
|
19
|
+
const invalidCredentials = {
|
|
20
|
+
host: 'not-a-url',
|
|
21
|
+
username: 'testuser',
|
|
22
|
+
password: 'testpass123',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(() => {
|
|
26
|
+
Schema.decodeUnknownSync(GerritCredentials)(invalidCredentials)
|
|
27
|
+
}).toThrow()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('should reject empty username', () => {
|
|
31
|
+
const invalidCredentials = {
|
|
32
|
+
host: 'https://gerrit.example.com',
|
|
33
|
+
username: '',
|
|
34
|
+
password: 'testpass123',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(() => {
|
|
38
|
+
Schema.decodeUnknownSync(GerritCredentials)(invalidCredentials)
|
|
39
|
+
}).toThrow()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('should reject empty password', () => {
|
|
43
|
+
const invalidCredentials = {
|
|
44
|
+
host: 'https://gerrit.example.com',
|
|
45
|
+
username: 'testuser',
|
|
46
|
+
password: '',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
expect(() => {
|
|
50
|
+
Schema.decodeUnknownSync(GerritCredentials)(invalidCredentials)
|
|
51
|
+
}).toThrow()
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('CommentInput', () => {
|
|
56
|
+
test('should validate valid comment input', () => {
|
|
57
|
+
const validComment = {
|
|
58
|
+
message: 'This is a test comment',
|
|
59
|
+
unresolved: true,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = Schema.decodeUnknownSync(CommentInput)(validComment)
|
|
63
|
+
expect(result).toEqual(validComment)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('should validate comment without unresolved flag', () => {
|
|
67
|
+
const validComment = {
|
|
68
|
+
message: 'This is a test comment',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = Schema.decodeUnknownSync(CommentInput)(validComment)
|
|
72
|
+
expect(result).toEqual(validComment)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('should reject empty message', () => {
|
|
76
|
+
const invalidComment = {
|
|
77
|
+
message: '',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(() => {
|
|
81
|
+
Schema.decodeUnknownSync(CommentInput)(invalidComment)
|
|
82
|
+
}).toThrow()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
generateMockAccount,
|
|
4
|
+
generateMockChange,
|
|
5
|
+
generateMockFileDiff,
|
|
6
|
+
generateMockFiles,
|
|
7
|
+
} from '@/test-utils/mock-generator'
|
|
8
|
+
|
|
9
|
+
describe('Mock Generator', () => {
|
|
10
|
+
describe('generateMockChange', () => {
|
|
11
|
+
test('should generate a complete mock change object', () => {
|
|
12
|
+
const change = generateMockChange()
|
|
13
|
+
|
|
14
|
+
expect(change).toMatchObject({
|
|
15
|
+
id: 'myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940',
|
|
16
|
+
project: 'myProject',
|
|
17
|
+
branch: 'master',
|
|
18
|
+
change_id: 'I8473b95934b5732ac55d26311a706c9c2bde9940',
|
|
19
|
+
subject: 'Implementing new feature',
|
|
20
|
+
status: 'NEW',
|
|
21
|
+
created: '2023-12-01 10:00:00.000000000',
|
|
22
|
+
updated: '2023-12-01 15:30:00.000000000',
|
|
23
|
+
insertions: 25,
|
|
24
|
+
deletions: 3,
|
|
25
|
+
_number: 12345,
|
|
26
|
+
owner: {
|
|
27
|
+
_account_id: 1000096,
|
|
28
|
+
name: 'John Developer',
|
|
29
|
+
email: 'john@example.com',
|
|
30
|
+
username: 'jdeveloper',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('should apply overrides to mock change', () => {
|
|
36
|
+
const overrides = {
|
|
37
|
+
subject: 'Custom subject',
|
|
38
|
+
status: 'MERGED' as const,
|
|
39
|
+
insertions: 100,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const change = generateMockChange(overrides)
|
|
43
|
+
|
|
44
|
+
expect(change.subject).toBe('Custom subject')
|
|
45
|
+
expect(change.status).toBe('MERGED')
|
|
46
|
+
expect(change.insertions).toBe(100)
|
|
47
|
+
// Original values should remain for non-overridden fields
|
|
48
|
+
expect(change.project).toBe('myProject')
|
|
49
|
+
expect(change.deletions).toBe(3)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('should handle partial owner overrides', () => {
|
|
53
|
+
const overrides = {
|
|
54
|
+
owner: {
|
|
55
|
+
_account_id: 999,
|
|
56
|
+
name: 'Custom Developer',
|
|
57
|
+
email: 'custom@example.com',
|
|
58
|
+
username: 'customdev',
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const change = generateMockChange(overrides)
|
|
63
|
+
|
|
64
|
+
expect(change.owner).toEqual(overrides.owner)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('generateMockFiles', () => {
|
|
69
|
+
test('should generate mock file info objects', () => {
|
|
70
|
+
const files = generateMockFiles()
|
|
71
|
+
|
|
72
|
+
expect(Object.keys(files)).toContain('src/main.ts')
|
|
73
|
+
expect(Object.keys(files)).toContain('tests/main.test.ts')
|
|
74
|
+
|
|
75
|
+
expect(files['src/main.ts']).toMatchObject({
|
|
76
|
+
status: 'M',
|
|
77
|
+
lines_inserted: 15,
|
|
78
|
+
lines_deleted: 3,
|
|
79
|
+
size_delta: 120,
|
|
80
|
+
size: 1200,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(files['tests/main.test.ts']).toMatchObject({
|
|
84
|
+
status: 'A',
|
|
85
|
+
lines_inserted: 45,
|
|
86
|
+
lines_deleted: 0,
|
|
87
|
+
size_delta: 450,
|
|
88
|
+
size: 450,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('should return consistent file structure', () => {
|
|
93
|
+
const files1 = generateMockFiles()
|
|
94
|
+
const files2 = generateMockFiles()
|
|
95
|
+
|
|
96
|
+
expect(Object.keys(files1)).toEqual(Object.keys(files2))
|
|
97
|
+
expect(files1['src/main.ts']).toEqual(files2['src/main.ts'])
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('generateMockFileDiff', () => {
|
|
102
|
+
test('should generate mock file diff content', () => {
|
|
103
|
+
const diff = generateMockFileDiff()
|
|
104
|
+
|
|
105
|
+
expect(diff).toMatchObject({
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
ab: ['function main() {', ' console.log("Hello, world!")'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
a: [' return 0'],
|
|
112
|
+
b: [' return process.exit(0)'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
ab: ['}'],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
change_type: 'MODIFIED',
|
|
119
|
+
diff_header: ['--- a/src/main.ts', '+++ b/src/main.ts'],
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('should have consistent structure', () => {
|
|
124
|
+
const diff1 = generateMockFileDiff()
|
|
125
|
+
const diff2 = generateMockFileDiff()
|
|
126
|
+
|
|
127
|
+
expect(diff1.change_type).toBe('MODIFIED')
|
|
128
|
+
expect(diff2.change_type).toBe('MODIFIED')
|
|
129
|
+
expect(diff1.content.length).toBe(diff2.content.length)
|
|
130
|
+
expect(diff1.diff_header).toEqual(['--- a/src/main.ts', '+++ b/src/main.ts'])
|
|
131
|
+
expect(diff2.diff_header).toEqual(['--- a/src/main.ts', '+++ b/src/main.ts'])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('generateMockAccount', () => {
|
|
136
|
+
test('should generate mock account object', () => {
|
|
137
|
+
const account = generateMockAccount()
|
|
138
|
+
|
|
139
|
+
expect(account).toMatchObject({
|
|
140
|
+
_account_id: 1000096,
|
|
141
|
+
name: 'Test User',
|
|
142
|
+
email: 'test@example.com',
|
|
143
|
+
username: 'testuser',
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('should return consistent account data', () => {
|
|
148
|
+
const account1 = generateMockAccount()
|
|
149
|
+
const account2 = generateMockAccount()
|
|
150
|
+
|
|
151
|
+
expect(account1).toEqual(account2)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|