@aaronshaf/ger 1.2.10 → 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 -180
  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,63 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { normalizeGerritHost } from '@/utils/url-parser'
3
+
4
+ describe('Setup Command', () => {
5
+ describe('URL normalization integration', () => {
6
+ test('should normalize host URL using normalizeGerritHost', () => {
7
+ // Test that the utility function is working as expected
8
+ expect(normalizeGerritHost('gerrit.example.com')).toBe('https://gerrit.example.com')
9
+ expect(normalizeGerritHost('https://gerrit.example.com/')).toBe('https://gerrit.example.com')
10
+ expect(normalizeGerritHost('gerrit.example.com:8080')).toBe('https://gerrit.example.com:8080')
11
+ })
12
+ })
13
+
14
+ describe('Configuration validation', () => {
15
+ test('should validate required fields', () => {
16
+ const config = {
17
+ host: 'https://gerrit.example.com',
18
+ username: 'testuser',
19
+ password: 'testpass',
20
+ }
21
+
22
+ expect(config.host).toBeTruthy()
23
+ expect(config.username).toBeTruthy()
24
+ expect(config.password).toBeTruthy()
25
+ })
26
+
27
+ test('should reject empty required fields', () => {
28
+ const config = {
29
+ host: '',
30
+ username: 'testuser',
31
+ password: 'testpass',
32
+ }
33
+
34
+ expect(config.host).toBeFalsy()
35
+ })
36
+ })
37
+
38
+ describe('AI tool detection', () => {
39
+ test('should check for available tools', () => {
40
+ const availableTools = ['claude', 'llm', 'chatgpt']
41
+ expect(availableTools).toContain('claude')
42
+ })
43
+
44
+ test('should handle missing tools', () => {
45
+ const availableTools: string[] = []
46
+ expect(availableTools).not.toContain('nonexistent-tool')
47
+ })
48
+ })
49
+
50
+ describe('Connection verification', () => {
51
+ test('should test connection success scenario', () => {
52
+ const mockResponse = { ok: true, status: 200 }
53
+ expect(mockResponse.ok).toBe(true)
54
+ expect(mockResponse.status).toBe(200)
55
+ })
56
+
57
+ test('should handle connection failures', () => {
58
+ const mockResponse = { ok: false, status: 401 }
59
+ expect(mockResponse.ok).toBe(false)
60
+ expect(mockResponse.status).toBe(401)
61
+ })
62
+ })
63
+ })
@@ -0,0 +1,324 @@
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach, mock, spyOn } 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 { createMockConfigService } from './helpers/config-mock'
10
+ import * as childProcess from 'node:child_process'
11
+ import { EventEmitter } from 'node:events'
12
+
13
+ /**
14
+ * Integration tests for auto-detecting Change-ID from HEAD commit
15
+ */
16
+
17
+ const mockChange = generateMockChange({
18
+ _number: 392385,
19
+ change_id: 'If5a3ae8cb5a107e187447802358417f311d0c4b1',
20
+ subject: 'WIP: test',
21
+ status: 'NEW',
22
+ project: 'my-project',
23
+ branch: 'master',
24
+ created: '2024-01-15 10:00:00.000000000',
25
+ updated: '2024-01-15 12:00:00.000000000',
26
+ owner: {
27
+ _account_id: 1001,
28
+ name: 'Test User',
29
+ email: 'test@example.com',
30
+ },
31
+ })
32
+
33
+ const mockDiff = `--- a/test.txt
34
+ +++ b/test.txt
35
+ @@ -1,1 +1,2 @@
36
+ original line
37
+ +new line`
38
+
39
+ const server = setupServer(
40
+ http.get('*/a/accounts/self', () => {
41
+ return HttpResponse.json({
42
+ _account_id: 1000,
43
+ name: 'Test User',
44
+ email: 'test@example.com',
45
+ })
46
+ }),
47
+
48
+ // Handler that matches the auto-detected Change-ID
49
+ http.get('*/a/changes/:changeId', ({ params }) => {
50
+ const { changeId } = params
51
+ if (changeId === 'If5a3ae8cb5a107e187447802358417f311d0c4b1') {
52
+ return HttpResponse.text(`)]}'
53
+ ${JSON.stringify(mockChange)}`)
54
+ }
55
+ return HttpResponse.text('Not Found', { status: 404 })
56
+ }),
57
+
58
+ http.get('*/a/changes/:changeId/revisions/current/patch', ({ params }) => {
59
+ const { changeId } = params
60
+ if (changeId === 'If5a3ae8cb5a107e187447802358417f311d0c4b1') {
61
+ return HttpResponse.text(btoa(mockDiff))
62
+ }
63
+ return HttpResponse.text('Not Found', { status: 404 })
64
+ }),
65
+
66
+ http.get('*/a/changes/:changeId/revisions/current/comments', ({ params }) => {
67
+ const { changeId } = params
68
+ if (changeId === 'If5a3ae8cb5a107e187447802358417f311d0c4b1') {
69
+ return HttpResponse.text(`)]}'
70
+ {}`)
71
+ }
72
+ return HttpResponse.text('Not Found', { status: 404 })
73
+ }),
74
+ )
75
+
76
+ let capturedLogs: string[] = []
77
+ let capturedErrors: string[] = []
78
+ let capturedStdout: string[] = []
79
+
80
+ const mockConsoleLog = mock((...args: any[]) => {
81
+ capturedLogs.push(args.join(' '))
82
+ })
83
+ const mockConsoleError = mock((...args: any[]) => {
84
+ capturedErrors.push(args.join(' '))
85
+ })
86
+
87
+ // Mock process.stdout.write to capture JSON/XML output and handle callbacks
88
+ const mockStdoutWrite = mock((chunk: any, callback?: any) => {
89
+ capturedStdout.push(String(chunk))
90
+ // Call the callback synchronously if provided
91
+ if (typeof callback === 'function') {
92
+ callback()
93
+ }
94
+ return true
95
+ })
96
+
97
+ const originalConsoleLog = console.log
98
+ const originalConsoleError = console.error
99
+ const originalStdoutWrite = process.stdout.write
100
+
101
+ let spawnSpy: ReturnType<typeof spyOn>
102
+
103
+ beforeAll(() => {
104
+ server.listen({ onUnhandledRequest: 'bypass' })
105
+ // @ts-ignore
106
+ console.log = mockConsoleLog
107
+ // @ts-ignore
108
+ console.error = mockConsoleError
109
+ // @ts-ignore
110
+ process.stdout.write = mockStdoutWrite
111
+ })
112
+
113
+ afterAll(() => {
114
+ server.close()
115
+ console.log = originalConsoleLog
116
+ console.error = originalConsoleError
117
+ // @ts-ignore
118
+ process.stdout.write = originalStdoutWrite
119
+ })
120
+
121
+ afterEach(() => {
122
+ server.resetHandlers()
123
+ mockConsoleLog.mockClear()
124
+ mockConsoleError.mockClear()
125
+ mockStdoutWrite.mockClear()
126
+ capturedLogs = []
127
+ capturedErrors = []
128
+ capturedStdout = []
129
+
130
+ if (spawnSpy) {
131
+ spawnSpy.mockRestore()
132
+ }
133
+ })
134
+
135
+ const createMockConfigLayer = (): Layer.Layer<ConfigService, never, never> =>
136
+ Layer.succeed(ConfigService, createMockConfigService())
137
+
138
+ describe('show command with auto-detection', () => {
139
+ test('auto-detects Change-ID from HEAD commit when no argument provided', async () => {
140
+ const commitMessage = `feat: add feature
141
+
142
+ Change-Id: If5a3ae8cb5a107e187447802358417f311d0c4b1`
143
+
144
+ // Mock git log command
145
+ const mockChildProcess = new EventEmitter()
146
+ // @ts-ignore
147
+ mockChildProcess.stdout = new EventEmitter()
148
+ // @ts-ignore
149
+ mockChildProcess.stderr = new EventEmitter()
150
+
151
+ spawnSpy = spyOn(childProcess, 'spawn')
152
+ spawnSpy.mockReturnValue(mockChildProcess as any)
153
+
154
+ const effect = showCommand(undefined, {}).pipe(
155
+ Effect.provide(GerritApiServiceLive),
156
+ Effect.provide(createMockConfigLayer()),
157
+ )
158
+
159
+ const resultPromise = Effect.runPromise(effect)
160
+
161
+ // Simulate git log success
162
+ setImmediate(() => {
163
+ // @ts-ignore
164
+ mockChildProcess.stdout.emit('data', Buffer.from(commitMessage))
165
+ mockChildProcess.emit('close', 0)
166
+ })
167
+
168
+ await resultPromise
169
+
170
+ const output = capturedLogs.join('\n')
171
+ expect(output).toContain('Change 392385')
172
+ expect(output).toContain('WIP: test')
173
+ expect(capturedErrors.length).toBe(0)
174
+ })
175
+
176
+ test('auto-detects Change-ID with --xml flag', async () => {
177
+ const commitMessage = `feat: add feature
178
+
179
+ Change-Id: If5a3ae8cb5a107e187447802358417f311d0c4b1`
180
+
181
+ const mockChildProcess = new EventEmitter()
182
+ // @ts-ignore
183
+ mockChildProcess.stdout = new EventEmitter()
184
+ // @ts-ignore
185
+ mockChildProcess.stderr = new EventEmitter()
186
+
187
+ spawnSpy = spyOn(childProcess, 'spawn')
188
+ spawnSpy.mockReturnValue(mockChildProcess as any)
189
+
190
+ const effect = showCommand(undefined, { xml: true }).pipe(
191
+ Effect.provide(GerritApiServiceLive),
192
+ Effect.provide(createMockConfigLayer()),
193
+ )
194
+
195
+ const resultPromise = Effect.runPromise(effect)
196
+
197
+ setImmediate(() => {
198
+ // @ts-ignore
199
+ mockChildProcess.stdout.emit('data', Buffer.from(commitMessage))
200
+ mockChildProcess.emit('close', 0)
201
+ })
202
+
203
+ await resultPromise
204
+
205
+ const output = capturedStdout.join('')
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('392385')
210
+ expect(capturedErrors.length).toBe(0)
211
+ })
212
+
213
+ test('shows error when no Change-ID in HEAD commit', async () => {
214
+ const commitMessage = `feat: add feature without Change-ID
215
+
216
+ This commit has no Change-ID footer.`
217
+
218
+ const mockChildProcess = new EventEmitter()
219
+ // @ts-ignore
220
+ mockChildProcess.stdout = new EventEmitter()
221
+ // @ts-ignore
222
+ mockChildProcess.stderr = new EventEmitter()
223
+
224
+ spawnSpy = spyOn(childProcess, 'spawn')
225
+ spawnSpy.mockReturnValue(mockChildProcess as any)
226
+
227
+ const effect = showCommand(undefined, {}).pipe(
228
+ Effect.provide(GerritApiServiceLive),
229
+ Effect.provide(createMockConfigLayer()),
230
+ )
231
+
232
+ const resultPromise = Effect.runPromise(effect)
233
+
234
+ setImmediate(() => {
235
+ // @ts-ignore
236
+ mockChildProcess.stdout.emit('data', Buffer.from(commitMessage))
237
+ mockChildProcess.emit('close', 0)
238
+ })
239
+
240
+ await resultPromise
241
+
242
+ const output = capturedErrors.join('\n')
243
+ expect(output).toContain('No Change-ID found in HEAD commit')
244
+ expect(capturedLogs.length).toBe(0)
245
+ })
246
+
247
+ test('shows error when not in git repository', async () => {
248
+ const mockChildProcess = new EventEmitter()
249
+ // @ts-ignore
250
+ mockChildProcess.stdout = new EventEmitter()
251
+ // @ts-ignore
252
+ mockChildProcess.stderr = new EventEmitter()
253
+
254
+ spawnSpy = spyOn(childProcess, 'spawn')
255
+ spawnSpy.mockReturnValue(mockChildProcess as any)
256
+
257
+ const effect = showCommand(undefined, {}).pipe(
258
+ Effect.provide(GerritApiServiceLive),
259
+ Effect.provide(createMockConfigLayer()),
260
+ )
261
+
262
+ const resultPromise = Effect.runPromise(effect)
263
+
264
+ setImmediate(() => {
265
+ // @ts-ignore
266
+ mockChildProcess.stderr.emit('data', Buffer.from('fatal: not a git repository'))
267
+ mockChildProcess.emit('close', 128)
268
+ })
269
+
270
+ await resultPromise
271
+
272
+ const output = capturedErrors.join('\n')
273
+ expect(output).toContain('fatal: not a git repository')
274
+ })
275
+
276
+ test('still works with explicit change-id argument', async () => {
277
+ // Don't mock git - should not be called when changeId is provided
278
+ const effect = showCommand('If5a3ae8cb5a107e187447802358417f311d0c4b1', {}).pipe(
279
+ Effect.provide(GerritApiServiceLive),
280
+ Effect.provide(createMockConfigLayer()),
281
+ )
282
+
283
+ await Effect.runPromise(effect)
284
+
285
+ const output = capturedLogs.join('\n')
286
+ expect(output).toContain('Change 392385')
287
+ expect(output).toContain('WIP: test')
288
+ expect(capturedErrors.length).toBe(0)
289
+ })
290
+
291
+ test('shows XML error when no Change-ID in commit with --xml flag', async () => {
292
+ const commitMessage = `feat: no change id`
293
+
294
+ const mockChildProcess = new EventEmitter()
295
+ // @ts-ignore
296
+ mockChildProcess.stdout = new EventEmitter()
297
+ // @ts-ignore
298
+ mockChildProcess.stderr = new EventEmitter()
299
+
300
+ spawnSpy = spyOn(childProcess, 'spawn')
301
+ spawnSpy.mockReturnValue(mockChildProcess as any)
302
+
303
+ const effect = showCommand(undefined, { xml: true }).pipe(
304
+ Effect.provide(GerritApiServiceLive),
305
+ Effect.provide(createMockConfigLayer()),
306
+ )
307
+
308
+ const resultPromise = Effect.runPromise(effect)
309
+
310
+ setImmediate(() => {
311
+ // @ts-ignore
312
+ mockChildProcess.stdout.emit('data', Buffer.from(commitMessage))
313
+ mockChildProcess.emit('close', 0)
314
+ })
315
+
316
+ await resultPromise
317
+
318
+ const output = capturedStdout.join('')
319
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
320
+ expect(output).toContain('<show_result>')
321
+ expect(output).toContain('<status>error</status>')
322
+ expect(output).toContain('No Change-ID found in HEAD commit')
323
+ })
324
+ })