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