@aaronshaf/ger 1.2.11 → 2.0.1

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 +433 -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 +71 -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 +368 -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,368 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test'
2
+ import { Effect, Layer } from 'effect'
3
+ import { HttpResponse, http } from 'msw'
4
+ import { setupServer } from 'msw/node'
5
+ import { GerritApiServiceLive } from '@/api/gerrit'
6
+ import { rebaseCommand } from '@/cli/commands/rebase'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './helpers/config-mock'
9
+ import type { ChangeInfo } from '@/schemas/gerrit'
10
+
11
+ const mockChange: ChangeInfo = {
12
+ id: 'test-project~master~I123',
13
+ _number: 12345,
14
+ change_id: 'I123',
15
+ project: 'test-project',
16
+ branch: 'master',
17
+ subject: 'Test change to rebase',
18
+ status: 'NEW',
19
+ created: '2024-01-01 10:00:00.000000000',
20
+ updated: '2024-01-01 12:00:00.000000000',
21
+ owner: {
22
+ _account_id: 1000,
23
+ name: 'Test User',
24
+ email: 'test@example.com',
25
+ },
26
+ labels: {
27
+ 'Code-Review': {
28
+ value: 0,
29
+ },
30
+ Verified: {
31
+ value: 0,
32
+ },
33
+ },
34
+ work_in_progress: false,
35
+ submittable: false,
36
+ }
37
+
38
+ // Create MSW server
39
+ const server = setupServer(
40
+ // Default handler for auth check
41
+ http.get('*/a/accounts/self', ({ request }) => {
42
+ const auth = request.headers.get('Authorization')
43
+ if (!auth || !auth.startsWith('Basic ')) {
44
+ return HttpResponse.text('Unauthorized', { status: 401 })
45
+ }
46
+ return HttpResponse.json({
47
+ _account_id: 1000,
48
+ name: 'Test User',
49
+ email: 'test@example.com',
50
+ })
51
+ }),
52
+ )
53
+
54
+ describe('rebase command', () => {
55
+ let mockConsoleLog: ReturnType<typeof mock>
56
+ let mockConsoleError: ReturnType<typeof mock>
57
+
58
+ beforeAll(() => {
59
+ server.listen({ onUnhandledRequest: 'bypass' })
60
+ })
61
+
62
+ afterAll(() => {
63
+ server.close()
64
+ })
65
+
66
+ beforeEach(() => {
67
+ mockConsoleLog = mock(() => {})
68
+ mockConsoleError = mock(() => {})
69
+ console.log = mockConsoleLog
70
+ console.error = mockConsoleError
71
+ })
72
+
73
+ afterEach(() => {
74
+ server.resetHandlers()
75
+ })
76
+
77
+ it('should rebase a change without a base', async () => {
78
+ server.use(
79
+ http.post('*/a/changes/12345/revisions/current/rebase', async ({ request }) => {
80
+ const body = (await request.json()) as { base?: string }
81
+ expect(body.base).toBeUndefined()
82
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
83
+ }),
84
+ )
85
+
86
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
87
+ const program = rebaseCommand('12345', {}).pipe(
88
+ Effect.provide(GerritApiServiceLive),
89
+ Effect.provide(mockConfigLayer),
90
+ )
91
+
92
+ await Effect.runPromise(program)
93
+
94
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
95
+ expect(output).toContain('Rebased change 12345')
96
+ expect(output).toContain('Test change to rebase')
97
+ expect(output).toContain('Branch: master')
98
+ expect(output).not.toContain('Base:')
99
+ })
100
+
101
+ it('should rebase a change with a specified base', async () => {
102
+ server.use(
103
+ http.post('*/a/changes/12345/revisions/current/rebase', async ({ request }) => {
104
+ const body = (await request.json()) as { base?: string }
105
+ expect(body.base).toBe('refs/heads/main')
106
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
107
+ }),
108
+ )
109
+
110
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
111
+ const program = rebaseCommand('12345', {
112
+ base: 'refs/heads/main',
113
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
114
+
115
+ await Effect.runPromise(program)
116
+
117
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
118
+ expect(output).toContain('Rebased change 12345')
119
+ expect(output).toContain('Test change to rebase')
120
+ expect(output).toContain('Branch: master')
121
+ expect(output).toContain('Base: refs/heads/main')
122
+ })
123
+
124
+ it('should output XML format when --xml flag is used', async () => {
125
+ server.use(
126
+ http.post('*/a/changes/12345/revisions/current/rebase', async () => {
127
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
128
+ }),
129
+ )
130
+
131
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
132
+ const program = rebaseCommand('12345', { xml: true }).pipe(
133
+ Effect.provide(GerritApiServiceLive),
134
+ Effect.provide(mockConfigLayer),
135
+ )
136
+
137
+ await Effect.runPromise(program)
138
+
139
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
140
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
141
+ expect(output).toContain('<rebase_result>')
142
+ expect(output).toContain('<status>success</status>')
143
+ expect(output).toContain('<change_number>12345</change_number>')
144
+ expect(output).toContain('<subject><![CDATA[Test change to rebase]]></subject>')
145
+ expect(output).toContain('<branch>master</branch>')
146
+ expect(output).toContain('</rebase_result>')
147
+ })
148
+
149
+ it('should output XML format with base when --base is provided', async () => {
150
+ server.use(
151
+ http.post('*/a/changes/12345/revisions/current/rebase', async ({ request }) => {
152
+ const body = (await request.json()) as { base?: string }
153
+ expect(body.base).toBe('refs/heads/develop')
154
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
155
+ }),
156
+ )
157
+
158
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
159
+ const program = rebaseCommand('12345', {
160
+ xml: true,
161
+ base: 'refs/heads/develop',
162
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
163
+
164
+ await Effect.runPromise(program)
165
+
166
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
167
+ expect(output).toContain('<rebase_result>')
168
+ expect(output).toContain('<status>success</status>')
169
+ expect(output).toContain('<base><![CDATA[refs/heads/develop]]></base>')
170
+ expect(output).toContain('</rebase_result>')
171
+ })
172
+
173
+ it('should handle not found errors gracefully with pretty output', async () => {
174
+ server.use(
175
+ http.post('*/a/changes/99999/revisions/current/rebase', () => {
176
+ return HttpResponse.text('Change not found', { status: 404 })
177
+ }),
178
+ )
179
+
180
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
181
+ const program = rebaseCommand('99999', {}).pipe(
182
+ Effect.provide(GerritApiServiceLive),
183
+ Effect.provide(mockConfigLayer),
184
+ )
185
+
186
+ // Error boundary catches and outputs to console.error
187
+ await Effect.runPromise(program)
188
+
189
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
190
+ expect(errorOutput).toContain('Error:')
191
+ })
192
+
193
+ it('should handle not found errors with XML output when --xml flag is used', async () => {
194
+ server.use(
195
+ http.post('*/a/changes/99999/revisions/current/rebase', () => {
196
+ return HttpResponse.text('Change not found', { status: 404 })
197
+ }),
198
+ )
199
+
200
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
201
+ const program = rebaseCommand('99999', { xml: true }).pipe(
202
+ Effect.provide(GerritApiServiceLive),
203
+ Effect.provide(mockConfigLayer),
204
+ )
205
+
206
+ // Error boundary catches and outputs XML error
207
+ await Effect.runPromise(program)
208
+
209
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
210
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
211
+ expect(output).toContain('<rebase_result>')
212
+ expect(output).toContain('<status>error</status>')
213
+ expect(output).toContain('<error><![CDATA[')
214
+ expect(output).toContain('</rebase_result>')
215
+ })
216
+
217
+ it('should output error to console.error when no change ID and HEAD has no Change-Id', async () => {
218
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
219
+ const program = rebaseCommand(undefined, {}).pipe(
220
+ Effect.provide(GerritApiServiceLive),
221
+ Effect.provide(mockConfigLayer),
222
+ )
223
+
224
+ // Error boundary catches NoChangeIdError and outputs to console.error
225
+ await Effect.runPromise(program)
226
+
227
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
228
+ expect(errorOutput).toContain('Error:')
229
+ expect(errorOutput).toContain('No Change-ID found in HEAD commit')
230
+ })
231
+
232
+ it('should output XML error when no change ID and HEAD has no Change-Id with --xml flag', async () => {
233
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
234
+ const program = rebaseCommand(undefined, { xml: true }).pipe(
235
+ Effect.provide(GerritApiServiceLive),
236
+ Effect.provide(mockConfigLayer),
237
+ )
238
+
239
+ // Error boundary catches NoChangeIdError and outputs XML error
240
+ await Effect.runPromise(program)
241
+
242
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
243
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
244
+ expect(output).toContain('<rebase_result>')
245
+ expect(output).toContain('<status>error</status>')
246
+ expect(output).toContain('No Change-ID found in HEAD commit')
247
+ expect(output).toContain('</rebase_result>')
248
+ })
249
+
250
+ it('should treat empty string as missing change ID and auto-detect', async () => {
251
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
252
+ const program = rebaseCommand('', {}).pipe(
253
+ Effect.provide(GerritApiServiceLive),
254
+ Effect.provide(mockConfigLayer),
255
+ )
256
+
257
+ // Empty string triggers auto-detection, which fails with NoChangeIdError
258
+ await Effect.runPromise(program)
259
+
260
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
261
+ expect(errorOutput).toContain('Error:')
262
+ expect(errorOutput).toContain('No Change-ID found in HEAD commit')
263
+ })
264
+
265
+ it('should handle rebase conflicts gracefully', async () => {
266
+ server.use(
267
+ http.post('*/a/changes/12345/revisions/current/rebase', () => {
268
+ return HttpResponse.text('Rebase conflict detected', { status: 409 })
269
+ }),
270
+ )
271
+
272
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
273
+ const program = rebaseCommand('12345', {}).pipe(
274
+ Effect.provide(GerritApiServiceLive),
275
+ Effect.provide(mockConfigLayer),
276
+ )
277
+
278
+ // Error boundary catches and outputs to console.error
279
+ await Effect.runPromise(program)
280
+
281
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
282
+ expect(errorOutput).toContain('Error:')
283
+ })
284
+
285
+ it('should handle API errors gracefully', async () => {
286
+ server.use(
287
+ http.post('*/a/changes/12345/revisions/current/rebase', () => {
288
+ return HttpResponse.text('Forbidden', { status: 403 })
289
+ }),
290
+ )
291
+
292
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
293
+ const program = rebaseCommand('12345', {}).pipe(
294
+ Effect.provide(GerritApiServiceLive),
295
+ Effect.provide(mockConfigLayer),
296
+ )
297
+
298
+ // Error boundary catches and outputs to console.error
299
+ await Effect.runPromise(program)
300
+
301
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
302
+ expect(errorOutput).toContain('Error:')
303
+ })
304
+
305
+ it('should handle changes that are already up to date', async () => {
306
+ server.use(
307
+ http.post('*/a/changes/12345/revisions/current/rebase', () => {
308
+ return HttpResponse.text('Change is already up to date', { status: 409 })
309
+ }),
310
+ )
311
+
312
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
313
+ const program = rebaseCommand('12345', {}).pipe(
314
+ Effect.provide(GerritApiServiceLive),
315
+ Effect.provide(mockConfigLayer),
316
+ )
317
+
318
+ // Error boundary catches and outputs to console.error
319
+ await Effect.runPromise(program)
320
+
321
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
322
+ expect(errorOutput).toContain('Error:')
323
+ })
324
+
325
+ it('should handle network errors gracefully', async () => {
326
+ server.use(
327
+ http.post('*/a/changes/12345/revisions/current/rebase', () => {
328
+ return HttpResponse.error()
329
+ }),
330
+ )
331
+
332
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
333
+ const program = rebaseCommand('12345', {}).pipe(
334
+ Effect.provide(GerritApiServiceLive),
335
+ Effect.provide(mockConfigLayer),
336
+ )
337
+
338
+ // Error boundary catches and outputs to console.error
339
+ await Effect.runPromise(program)
340
+
341
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
342
+ expect(errorOutput).toContain('Error:')
343
+ })
344
+
345
+ it('should handle network errors with XML output', async () => {
346
+ server.use(
347
+ http.post('*/a/changes/12345/revisions/current/rebase', () => {
348
+ return HttpResponse.error()
349
+ }),
350
+ )
351
+
352
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
353
+ const program = rebaseCommand('12345', { xml: true }).pipe(
354
+ Effect.provide(GerritApiServiceLive),
355
+ Effect.provide(mockConfigLayer),
356
+ )
357
+
358
+ // Error boundary catches and outputs XML error
359
+ await Effect.runPromise(program)
360
+
361
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
362
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
363
+ expect(output).toContain('<rebase_result>')
364
+ expect(output).toContain('<status>error</status>')
365
+ expect(output).toContain('<error><![CDATA[')
366
+ expect(output).toContain('</rebase_result>')
367
+ })
368
+ })