@aaronshaf/ger 1.2.11 → 2.0.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 (180) hide show
  1. package/.ast-grep/rules/no-as-casting.yml +13 -0
  2. package/.claude-plugin/plugin.json +22 -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 +83 -0
  6. package/.github/workflows/claude.yml +50 -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 +105 -0
  16. package/DEVELOPMENT.md +361 -0
  17. package/EXAMPLES.md +457 -0
  18. package/README.md +831 -16
  19. package/bin/ger +3 -18
  20. package/biome.json +36 -0
  21. package/bun.lock +678 -0
  22. package/bunfig.toml +8 -0
  23. package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
  24. package/docs/adr/0002-use-bun-runtime.md +64 -0
  25. package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
  26. package/docs/adr/0004-use-commander-for-cli.md +76 -0
  27. package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
  28. package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
  29. package/docs/adr/0007-git-hooks-for-quality.md +94 -0
  30. package/docs/adr/0008-no-as-typecasting.md +83 -0
  31. package/docs/adr/0009-file-size-limits.md +82 -0
  32. package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
  33. package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
  34. package/docs/adr/0012-build-status-message-parsing.md +94 -0
  35. package/docs/adr/0013-git-subprocess-integration.md +98 -0
  36. package/docs/adr/0014-group-management-support.md +95 -0
  37. package/docs/adr/0015-batch-comment-processing.md +111 -0
  38. package/docs/adr/0016-flexible-change-identifiers.md +94 -0
  39. package/docs/adr/0017-git-worktree-support.md +102 -0
  40. package/docs/adr/0018-auto-install-commit-hook.md +103 -0
  41. package/docs/adr/0019-sdk-package-exports.md +95 -0
  42. package/docs/adr/0020-code-coverage-enforcement.md +105 -0
  43. package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
  44. package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
  45. package/docs/adr/README.md +30 -0
  46. package/docs/prd/README.md +12 -0
  47. package/docs/prd/architecture.md +325 -0
  48. package/docs/prd/commands.md +425 -0
  49. package/docs/prd/data-model.md +349 -0
  50. package/docs/prd/overview.md +124 -0
  51. package/index.ts +219 -0
  52. package/oxlint.json +24 -0
  53. package/package.json +82 -15
  54. package/scripts/check-coverage.ts +69 -0
  55. package/scripts/check-file-size.ts +38 -0
  56. package/scripts/fix-test-mocks.ts +55 -0
  57. package/skills/gerrit-workflow/SKILL.md +247 -0
  58. package/skills/gerrit-workflow/examples.md +572 -0
  59. package/skills/gerrit-workflow/reference.md +728 -0
  60. package/src/api/gerrit.ts +696 -0
  61. package/src/cli/commands/abandon.ts +65 -0
  62. package/src/cli/commands/add-reviewer.ts +156 -0
  63. package/src/cli/commands/build-status.ts +282 -0
  64. package/src/cli/commands/checkout.ts +422 -0
  65. package/src/cli/commands/comment.ts +460 -0
  66. package/src/cli/commands/comments.ts +85 -0
  67. package/src/cli/commands/diff.ts +71 -0
  68. package/src/cli/commands/extract-url.ts +266 -0
  69. package/src/cli/commands/groups-members.ts +104 -0
  70. package/src/cli/commands/groups-show.ts +169 -0
  71. package/src/cli/commands/groups.ts +137 -0
  72. package/src/cli/commands/incoming.ts +226 -0
  73. package/src/cli/commands/init.ts +164 -0
  74. package/src/cli/commands/mine.ts +115 -0
  75. package/src/cli/commands/open.ts +57 -0
  76. package/src/cli/commands/projects.ts +68 -0
  77. package/src/cli/commands/push.ts +430 -0
  78. package/src/cli/commands/rebase.ts +52 -0
  79. package/src/cli/commands/remove-reviewer.ts +123 -0
  80. package/src/cli/commands/restore.ts +50 -0
  81. package/src/cli/commands/review.ts +486 -0
  82. package/src/cli/commands/search.ts +162 -0
  83. package/src/cli/commands/setup.ts +286 -0
  84. package/src/cli/commands/show.ts +491 -0
  85. package/src/cli/commands/status.ts +35 -0
  86. package/src/cli/commands/submit.ts +108 -0
  87. package/src/cli/commands/vote.ts +119 -0
  88. package/src/cli/commands/workspace.ts +200 -0
  89. package/src/cli/index.ts +53 -0
  90. package/src/cli/register-commands.ts +659 -0
  91. package/src/cli/register-group-commands.ts +88 -0
  92. package/src/cli/register-reviewer-commands.ts +97 -0
  93. package/src/prompts/default-review.md +86 -0
  94. package/src/prompts/system-inline-review.md +135 -0
  95. package/src/prompts/system-overall-review.md +206 -0
  96. package/src/schemas/config.test.ts +245 -0
  97. package/src/schemas/config.ts +84 -0
  98. package/src/schemas/gerrit.ts +681 -0
  99. package/src/services/commit-hook.ts +314 -0
  100. package/src/services/config.test.ts +150 -0
  101. package/src/services/config.ts +250 -0
  102. package/src/services/git-worktree.ts +342 -0
  103. package/src/services/review-strategy.ts +292 -0
  104. package/src/test-utils/mock-generator.ts +138 -0
  105. package/src/utils/change-id.test.ts +98 -0
  106. package/src/utils/change-id.ts +63 -0
  107. package/src/utils/comment-formatters.ts +153 -0
  108. package/src/utils/diff-context.ts +103 -0
  109. package/src/utils/diff-formatters.ts +141 -0
  110. package/src/utils/formatters.ts +85 -0
  111. package/src/utils/git-commit.test.ts +277 -0
  112. package/src/utils/git-commit.ts +122 -0
  113. package/src/utils/index.ts +55 -0
  114. package/src/utils/message-filters.ts +26 -0
  115. package/src/utils/review-formatters.ts +89 -0
  116. package/src/utils/review-prompt-builder.ts +110 -0
  117. package/src/utils/shell-safety.ts +117 -0
  118. package/src/utils/status-indicators.ts +100 -0
  119. package/src/utils/url-parser.test.ts +271 -0
  120. package/src/utils/url-parser.ts +118 -0
  121. package/tests/abandon.test.ts +230 -0
  122. package/tests/add-reviewer.test.ts +579 -0
  123. package/tests/build-status-watch.test.ts +344 -0
  124. package/tests/build-status.test.ts +789 -0
  125. package/tests/change-id-formats.test.ts +268 -0
  126. package/tests/checkout/integration.test.ts +653 -0
  127. package/tests/checkout/parse-input.test.ts +55 -0
  128. package/tests/checkout/validation.test.ts +178 -0
  129. package/tests/comment-batch-advanced.test.ts +431 -0
  130. package/tests/comment-gerrit-api-compliance.test.ts +414 -0
  131. package/tests/comment.test.ts +708 -0
  132. package/tests/comments.test.ts +323 -0
  133. package/tests/config-service-simple.test.ts +100 -0
  134. package/tests/diff.test.ts +419 -0
  135. package/tests/extract-url.test.ts +517 -0
  136. package/tests/groups-members.test.ts +256 -0
  137. package/tests/groups-show.test.ts +323 -0
  138. package/tests/groups.test.ts +334 -0
  139. package/tests/helpers/build-status-test-setup.ts +83 -0
  140. package/tests/helpers/config-mock.ts +27 -0
  141. package/tests/incoming.test.ts +357 -0
  142. package/tests/init.test.ts +70 -0
  143. package/tests/integration/commit-hook.test.ts +246 -0
  144. package/tests/interactive-incoming.test.ts +173 -0
  145. package/tests/mine.test.ts +285 -0
  146. package/tests/mocks/msw-handlers.ts +80 -0
  147. package/tests/open.test.ts +233 -0
  148. package/tests/projects.test.ts +259 -0
  149. package/tests/rebase.test.ts +271 -0
  150. package/tests/remove-reviewer.test.ts +357 -0
  151. package/tests/restore.test.ts +237 -0
  152. package/tests/review.test.ts +135 -0
  153. package/tests/search.test.ts +712 -0
  154. package/tests/setup.test.ts +63 -0
  155. package/tests/show-auto-detect.test.ts +324 -0
  156. package/tests/show.test.ts +813 -0
  157. package/tests/status.test.ts +145 -0
  158. package/tests/submit.test.ts +316 -0
  159. package/tests/unit/commands/push.test.ts +194 -0
  160. package/tests/unit/git-branch-detection.test.ts +82 -0
  161. package/tests/unit/git-worktree.test.ts +55 -0
  162. package/tests/unit/patterns/push-patterns.test.ts +148 -0
  163. package/tests/unit/schemas/gerrit.test.ts +85 -0
  164. package/tests/unit/services/commit-hook.test.ts +132 -0
  165. package/tests/unit/services/review-strategy.test.ts +349 -0
  166. package/tests/unit/test-utils/mock-generator.test.ts +154 -0
  167. package/tests/unit/utils/comment-formatters.test.ts +415 -0
  168. package/tests/unit/utils/diff-context.test.ts +171 -0
  169. package/tests/unit/utils/diff-formatters.test.ts +165 -0
  170. package/tests/unit/utils/formatters.test.ts +411 -0
  171. package/tests/unit/utils/message-filters.test.ts +227 -0
  172. package/tests/unit/utils/shell-safety.test.ts +230 -0
  173. package/tests/unit/utils/status-indicators.test.ts +137 -0
  174. package/tests/vote.test.ts +317 -0
  175. package/tests/workspace.test.ts +295 -0
  176. package/tsconfig.json +36 -5
  177. package/src/commands/branch.ts +0 -196
  178. package/src/ger.ts +0 -22
  179. package/src/types.d.ts +0 -35
  180. package/src/utils.ts +0 -130
@@ -0,0 +1,349 @@
1
+ import { describe, it, expect, beforeEach, mock } from 'bun:test'
2
+ import { Effect } from 'effect'
3
+
4
+ // Create testable versions of the strategies by injecting dependencies
5
+ interface MockDeps {
6
+ execAsync: (cmd: string) => Promise<{ stdout: string; stderr: string }>
7
+ spawn: (command: string, options: any) => any
8
+ }
9
+
10
+ // Test implementation that mirrors the real strategy structure
11
+ const createTestStrategy = (name: string, command: string, flags: string[], deps: MockDeps) => ({
12
+ name,
13
+ isAvailable: () =>
14
+ Effect.gen(function* () {
15
+ try {
16
+ const result = yield* Effect.tryPromise({
17
+ try: () => deps.execAsync(`which ${command.split(' ')[0]}`),
18
+ catch: () => null,
19
+ }).pipe(Effect.orElseSucceed(() => null))
20
+
21
+ return Boolean(result && result.stdout.trim())
22
+ } catch {
23
+ return false
24
+ }
25
+ }),
26
+ executeReview: (prompt: string, options: { cwd?: string } = {}) =>
27
+ Effect.gen(function* () {
28
+ const result = yield* Effect.tryPromise({
29
+ try: async () => {
30
+ const child = deps.spawn(`${command} ${flags.join(' ')}`, {
31
+ shell: true,
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ cwd: options.cwd || process.cwd(),
34
+ })
35
+
36
+ child.stdin.write(prompt)
37
+ child.stdin.end()
38
+
39
+ let stdout = ''
40
+ let stderr = ''
41
+
42
+ child.stdout.on('data', (data: Buffer) => {
43
+ stdout += data.toString()
44
+ })
45
+
46
+ child.stderr.on('data', (data: Buffer) => {
47
+ stderr += data.toString()
48
+ })
49
+
50
+ return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
51
+ child.on('close', (code: number) => {
52
+ if (code !== 0) {
53
+ reject(new Error(`${name} exited with code ${code}: ${stderr}`))
54
+ } else {
55
+ resolve({ stdout, stderr })
56
+ }
57
+ })
58
+
59
+ child.on('error', reject)
60
+ })
61
+ },
62
+ catch: (error) =>
63
+ new Error(`${name} failed: ${error instanceof Error ? error.message : String(error)}`),
64
+ })
65
+
66
+ // Extract response from <response> tags or use full output
67
+ const responseMatch = result.stdout.match(/<response>([\s\S]*?)<\/response>/i)
68
+ return responseMatch ? responseMatch[1].trim() : result.stdout.trim()
69
+ }),
70
+ })
71
+
72
+ describe('Review Strategy', () => {
73
+ let mockExecAsync: any
74
+ let mockSpawn: any
75
+ let mockChildProcess: any
76
+
77
+ beforeEach(() => {
78
+ mockChildProcess = {
79
+ stdin: {
80
+ write: mock(() => {}),
81
+ end: mock(() => {}),
82
+ },
83
+ stdout: {
84
+ on: mock(() => {}),
85
+ },
86
+ stderr: {
87
+ on: mock(() => {}),
88
+ },
89
+ on: mock(() => {}),
90
+ }
91
+
92
+ mockExecAsync = mock()
93
+ mockSpawn = mock(() => mockChildProcess)
94
+ })
95
+
96
+ const setupSuccessfulExecution = (output = 'AI response') => {
97
+ mockChildProcess.stdout.on.mockImplementation((event: string, callback: Function) => {
98
+ if (event === 'data') {
99
+ process.nextTick(() => callback(Buffer.from(output)))
100
+ }
101
+ })
102
+
103
+ mockChildProcess.stderr.on.mockImplementation((_event: string, _callback: Function) => {
104
+ // No stderr for success
105
+ })
106
+
107
+ mockChildProcess.on.mockImplementation((event: string, callback: Function) => {
108
+ if (event === 'close') {
109
+ process.nextTick(() => callback(0))
110
+ }
111
+ })
112
+ }
113
+
114
+ const setupFailedExecution = (exitCode = 1, stderr = 'Command failed') => {
115
+ mockChildProcess.stdout.on.mockImplementation((_event: string, _callback: Function) => {
116
+ // No stdout for failure
117
+ })
118
+
119
+ mockChildProcess.stderr.on.mockImplementation((event: string, callback: Function) => {
120
+ if (event === 'data') {
121
+ process.nextTick(() => callback(Buffer.from(stderr)))
122
+ }
123
+ })
124
+
125
+ mockChildProcess.on.mockImplementation((event: string, callback: Function) => {
126
+ if (event === 'close') {
127
+ process.nextTick(() => callback(exitCode))
128
+ }
129
+ })
130
+ }
131
+
132
+ describe('Claude CLI Strategy', () => {
133
+ let claudeStrategy: any
134
+
135
+ beforeEach(() => {
136
+ claudeStrategy = createTestStrategy('Claude CLI', 'claude', ['-p'], {
137
+ execAsync: mockExecAsync,
138
+ spawn: mockSpawn,
139
+ })
140
+ })
141
+
142
+ it('should check availability when claude is installed', async () => {
143
+ mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/claude', stderr: '' })
144
+
145
+ const available = await Effect.runPromise(claudeStrategy.isAvailable())
146
+
147
+ expect(available).toBe(true)
148
+ expect(mockExecAsync).toHaveBeenCalledWith('which claude')
149
+ })
150
+
151
+ it('should check availability when claude is not installed', async () => {
152
+ mockExecAsync.mockRejectedValueOnce(new Error('Command not found'))
153
+
154
+ const available = await Effect.runPromise(claudeStrategy.isAvailable())
155
+
156
+ expect(available).toBe(false)
157
+ })
158
+
159
+ it('should execute review successfully', async () => {
160
+ setupSuccessfulExecution('Claude AI response')
161
+
162
+ const response = await Effect.runPromise(
163
+ claudeStrategy.executeReview('Test prompt', { cwd: '/tmp' }),
164
+ )
165
+
166
+ expect(response).toBe('Claude AI response')
167
+ expect(mockSpawn).toHaveBeenCalledWith('claude -p', {
168
+ shell: true,
169
+ stdio: ['pipe', 'pipe', 'pipe'],
170
+ cwd: '/tmp',
171
+ })
172
+ expect(mockChildProcess.stdin.write).toHaveBeenCalledWith('Test prompt')
173
+ expect(mockChildProcess.stdin.end).toHaveBeenCalled()
174
+ })
175
+
176
+ it('should extract response from tags', async () => {
177
+ setupSuccessfulExecution('<response>Tagged content</response>')
178
+
179
+ const response = await Effect.runPromise(claudeStrategy.executeReview('Test prompt'))
180
+
181
+ expect(response).toBe('Tagged content')
182
+ })
183
+
184
+ it('should handle command failures', async () => {
185
+ setupFailedExecution(1, 'Claude CLI error')
186
+
187
+ try {
188
+ await Effect.runPromise(claudeStrategy.executeReview('Test prompt'))
189
+ expect(false).toBe(true) // Should not reach here
190
+ } catch (error: any) {
191
+ expect(error.message).toContain('Claude CLI failed')
192
+ }
193
+ })
194
+ })
195
+
196
+ describe('Gemini CLI Strategy', () => {
197
+ let geminiStrategy: any
198
+
199
+ beforeEach(() => {
200
+ geminiStrategy = createTestStrategy('Gemini CLI', 'gemini', ['-p'], {
201
+ execAsync: mockExecAsync,
202
+ spawn: mockSpawn,
203
+ })
204
+ })
205
+
206
+ it('should check availability', async () => {
207
+ mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/gemini', stderr: '' })
208
+
209
+ const available = await Effect.runPromise(geminiStrategy.isAvailable())
210
+
211
+ expect(available).toBe(true)
212
+ expect(mockExecAsync).toHaveBeenCalledWith('which gemini')
213
+ })
214
+
215
+ it('should use -p flag', async () => {
216
+ setupSuccessfulExecution('Gemini response')
217
+
218
+ const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
219
+
220
+ expect(response).toBe('Gemini response')
221
+ expect(mockSpawn).toHaveBeenCalledWith('gemini -p', expect.any(Object))
222
+ })
223
+
224
+ it('should extract response from tags', async () => {
225
+ setupSuccessfulExecution('<response>Gemini tagged content</response>')
226
+
227
+ const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
228
+
229
+ expect(response).toBe('Gemini tagged content')
230
+ })
231
+ })
232
+
233
+ describe('OpenCode CLI Strategy', () => {
234
+ let opencodeStrategy: any
235
+
236
+ beforeEach(() => {
237
+ opencodeStrategy = createTestStrategy('OpenCode CLI', 'opencode', ['-p'], {
238
+ execAsync: mockExecAsync,
239
+ spawn: mockSpawn,
240
+ })
241
+ })
242
+
243
+ it('should check availability', async () => {
244
+ mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/opencode', stderr: '' })
245
+
246
+ const available = await Effect.runPromise(opencodeStrategy.isAvailable())
247
+
248
+ expect(available).toBe(true)
249
+ expect(mockExecAsync).toHaveBeenCalledWith('which opencode')
250
+ })
251
+
252
+ it('should use -p flag', async () => {
253
+ setupSuccessfulExecution('OpenCode response')
254
+
255
+ const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
256
+
257
+ expect(response).toBe('OpenCode response')
258
+ expect(mockSpawn).toHaveBeenCalledWith('opencode -p', expect.any(Object))
259
+ })
260
+
261
+ it('should extract response from tags', async () => {
262
+ setupSuccessfulExecution('<response>OpenCode tagged content</response>')
263
+
264
+ const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
265
+
266
+ expect(response).toBe('OpenCode tagged content')
267
+ })
268
+ })
269
+
270
+ describe('Integration with actual service patterns', () => {
271
+ it('should demonstrate proper Effect patterns', async () => {
272
+ const mockStrategy = createTestStrategy('Mock CLI', 'mock', [], {
273
+ execAsync: mockExecAsync,
274
+ spawn: mockSpawn,
275
+ })
276
+
277
+ setupSuccessfulExecution('Integration test response')
278
+
279
+ // Test using Effect.gen patterns like the real service
280
+ const result = await Effect.runPromise(
281
+ Effect.gen(function* () {
282
+ // Test availability check
283
+ mockExecAsync.mockResolvedValueOnce({ stdout: '/usr/local/bin/mock', stderr: '' })
284
+ const available = yield* mockStrategy.isAvailable()
285
+
286
+ if (!available) {
287
+ return yield* Effect.fail(new Error('Strategy not available'))
288
+ }
289
+
290
+ // Test execution
291
+ const response = yield* mockStrategy.executeReview('Test prompt', { cwd: '/tmp' })
292
+ return response
293
+ }),
294
+ )
295
+
296
+ expect(result).toBe('Integration test response')
297
+ })
298
+
299
+ it('should handle error propagation correctly', async () => {
300
+ const mockStrategy = createTestStrategy('Failing CLI', 'failing', [], {
301
+ execAsync: mockExecAsync,
302
+ spawn: mockSpawn,
303
+ })
304
+
305
+ setupFailedExecution(1, 'Mock failure')
306
+
307
+ try {
308
+ await Effect.runPromise(
309
+ Effect.gen(function* () {
310
+ return yield* mockStrategy.executeReview('Test prompt')
311
+ }),
312
+ )
313
+ expect(false).toBe(true) // Should not reach here
314
+ } catch (error: any) {
315
+ expect(error.message).toContain('Failing CLI failed')
316
+ }
317
+ })
318
+
319
+ it('should test multiple strategy selection logic', async () => {
320
+ const strategies = [
321
+ createTestStrategy('Strategy A', 'a', [], { execAsync: mockExecAsync, spawn: mockSpawn }),
322
+ createTestStrategy('Strategy B', 'b', [], { execAsync: mockExecAsync, spawn: mockSpawn }),
323
+ createTestStrategy('Strategy C', 'c', [], { execAsync: mockExecAsync, spawn: mockSpawn }),
324
+ ]
325
+
326
+ // Mock availability checks: A fails, B succeeds, C succeeds
327
+ mockExecAsync
328
+ .mockRejectedValueOnce(new Error('Command not found')) // A not available
329
+ .mockResolvedValueOnce({ stdout: '/usr/local/bin/b', stderr: '' }) // B available
330
+ .mockResolvedValueOnce({ stdout: '/usr/local/bin/c', stderr: '' }) // C available
331
+
332
+ const available = await Effect.runPromise(
333
+ Effect.gen(function* () {
334
+ const availableStrategies = []
335
+ for (const strategy of strategies) {
336
+ const isAvailable = yield* strategy.isAvailable()
337
+ if (isAvailable) {
338
+ availableStrategies.push(strategy)
339
+ }
340
+ }
341
+ return availableStrategies
342
+ }),
343
+ )
344
+
345
+ expect(available.length).toBe(2)
346
+ expect(available.map((s) => s.name)).toEqual(['Strategy B', 'Strategy C'])
347
+ })
348
+ })
349
+ })
@@ -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
+ })