@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,334 @@
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 { groupsCommand } from '@/cli/commands/groups'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './helpers/config-mock'
9
+
10
+ const mockGroups = {
11
+ administrators: {
12
+ id: 'administrators',
13
+ name: 'Administrators',
14
+ description: 'Site administrators with full access',
15
+ owner: 'Administrators',
16
+ owner_id: 'administrators',
17
+ group_id: 1,
18
+ options: { visible_to_all: true },
19
+ created_on: '2024-01-01 10:00:00.000000000',
20
+ },
21
+ 'project-reviewers': {
22
+ id: 'project-reviewers',
23
+ name: 'Project Reviewers',
24
+ description: 'Code reviewers for the project',
25
+ owner: 'Project Owners',
26
+ owner_id: 'project-owners',
27
+ group_id: 2,
28
+ options: { visible_to_all: false },
29
+ },
30
+ developers: {
31
+ id: 'developers',
32
+ name: 'Developers',
33
+ description: 'Development team members',
34
+ owner: 'Administrators',
35
+ owner_id: 'administrators',
36
+ group_id: 3,
37
+ },
38
+ }
39
+
40
+ const server = setupServer(
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('groups 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 list all groups', async () => {
78
+ server.use(
79
+ http.get('*/a/groups/', () => {
80
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
81
+ }),
82
+ )
83
+
84
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
85
+ const program = groupsCommand({}).pipe(
86
+ Effect.provide(GerritApiServiceLive),
87
+ Effect.provide(mockConfigLayer),
88
+ )
89
+
90
+ await Effect.runPromise(program)
91
+
92
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
93
+ expect(output).toContain('Available groups (3)')
94
+ expect(output).toContain('Administrators')
95
+ expect(output).toContain('Site administrators with full access')
96
+ expect(output).toContain('Project Reviewers')
97
+ expect(output).toContain('Developers')
98
+ })
99
+
100
+ it('should list groups with pattern filter', async () => {
101
+ server.use(
102
+ http.get('*/a/groups/', ({ request }) => {
103
+ const url = new URL(request.url)
104
+ const pattern = url.searchParams.get('r')
105
+ expect(pattern).toBe('project-.*')
106
+
107
+ const filtered = {
108
+ 'project-reviewers': mockGroups['project-reviewers'],
109
+ }
110
+ return HttpResponse.text(`)]}'\n${JSON.stringify(filtered)}`)
111
+ }),
112
+ )
113
+
114
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
115
+ const program = groupsCommand({ pattern: 'project-.*' }).pipe(
116
+ Effect.provide(GerritApiServiceLive),
117
+ Effect.provide(mockConfigLayer),
118
+ )
119
+
120
+ await Effect.runPromise(program)
121
+
122
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
123
+ expect(output).toContain('Project Reviewers')
124
+ expect(output).not.toContain('Administrators')
125
+ expect(output).not.toContain('Developers')
126
+ })
127
+
128
+ it('should list owned groups only', async () => {
129
+ server.use(
130
+ http.get('*/a/groups/', ({ request }) => {
131
+ const url = new URL(request.url)
132
+ expect(url.searchParams.has('owned')).toBe(true)
133
+
134
+ const filtered = {
135
+ administrators: mockGroups['administrators'],
136
+ }
137
+ return HttpResponse.text(`)]}'\n${JSON.stringify(filtered)}`)
138
+ }),
139
+ )
140
+
141
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
142
+ const program = groupsCommand({ owned: true }).pipe(
143
+ Effect.provide(GerritApiServiceLive),
144
+ Effect.provide(mockConfigLayer),
145
+ )
146
+
147
+ await Effect.runPromise(program)
148
+
149
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
150
+ expect(output).toContain('Administrators')
151
+ })
152
+
153
+ it('should list groups for specific project', async () => {
154
+ server.use(
155
+ http.get('*/a/groups/', ({ request }) => {
156
+ const url = new URL(request.url)
157
+ const project = url.searchParams.get('p')
158
+ expect(project).toBe('my-project')
159
+
160
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
161
+ }),
162
+ )
163
+
164
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
165
+ const program = groupsCommand({ project: 'my-project' }).pipe(
166
+ Effect.provide(GerritApiServiceLive),
167
+ Effect.provide(mockConfigLayer),
168
+ )
169
+
170
+ await Effect.runPromise(program)
171
+
172
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
173
+ expect(output).toContain('Available groups')
174
+ })
175
+
176
+ it('should output XML format when --xml flag is used', async () => {
177
+ server.use(
178
+ http.get('*/a/groups/', () => {
179
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
180
+ }),
181
+ )
182
+
183
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
184
+ const program = groupsCommand({ xml: true }).pipe(
185
+ Effect.provide(GerritApiServiceLive),
186
+ Effect.provide(mockConfigLayer),
187
+ )
188
+
189
+ await Effect.runPromise(program)
190
+
191
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
192
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
193
+ expect(output).toContain('<groups_result>')
194
+ expect(output).toContain('<status>success</status>')
195
+ expect(output).toContain('<count>3</count>')
196
+ expect(output).toContain('<groups>')
197
+ expect(output).toContain('<group>')
198
+ expect(output).toContain('<id><![CDATA[administrators]]></id>')
199
+ expect(output).toContain('<name><![CDATA[Administrators]]></name>')
200
+ expect(output).toContain(
201
+ '<description><![CDATA[Site administrators with full access]]></description>',
202
+ )
203
+ expect(output).toContain('<owner><![CDATA[Administrators]]></owner>')
204
+ expect(output).toContain('<visible_to_all>true</visible_to_all>')
205
+ expect(output).toContain('</group>')
206
+ expect(output).toContain('</groups>')
207
+ expect(output).toContain('</groups_result>')
208
+ })
209
+
210
+ it('should handle empty results', async () => {
211
+ server.use(
212
+ http.get('*/a/groups/', () => {
213
+ return HttpResponse.text(`)]}'\n${JSON.stringify({})}`)
214
+ }),
215
+ )
216
+
217
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
218
+ const program = groupsCommand({}).pipe(
219
+ Effect.provide(GerritApiServiceLive),
220
+ Effect.provide(mockConfigLayer),
221
+ )
222
+
223
+ await Effect.runPromise(program)
224
+
225
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
226
+ expect(output).toContain('No groups found')
227
+ })
228
+
229
+ it('should handle empty results with XML format', async () => {
230
+ server.use(
231
+ http.get('*/a/groups/', () => {
232
+ return HttpResponse.text(`)]}'\n${JSON.stringify({})}`)
233
+ }),
234
+ )
235
+
236
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
237
+ const program = groupsCommand({ xml: true }).pipe(
238
+ Effect.provide(GerritApiServiceLive),
239
+ Effect.provide(mockConfigLayer),
240
+ )
241
+
242
+ await Effect.runPromise(program)
243
+
244
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
245
+ expect(output).toContain('<groups_result>')
246
+ expect(output).toContain('<status>success</status>')
247
+ expect(output).toContain('<count>0</count>')
248
+ expect(output).toContain('<groups />')
249
+ })
250
+
251
+ it('should handle network errors gracefully', async () => {
252
+ server.use(
253
+ http.get('*/a/groups/', () => {
254
+ return HttpResponse.error()
255
+ }),
256
+ )
257
+
258
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
259
+ const program = groupsCommand({}).pipe(
260
+ Effect.provide(GerritApiServiceLive),
261
+ Effect.provide(mockConfigLayer),
262
+ )
263
+
264
+ await expect(Effect.runPromise(program)).rejects.toThrow()
265
+ })
266
+
267
+ it('should handle API errors (403)', async () => {
268
+ server.use(
269
+ http.get('*/a/groups/', () => {
270
+ return HttpResponse.text('Forbidden', { status: 403 })
271
+ }),
272
+ )
273
+
274
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
275
+ const program = groupsCommand({}).pipe(
276
+ Effect.provide(GerritApiServiceLive),
277
+ Effect.provide(mockConfigLayer),
278
+ )
279
+
280
+ await expect(Effect.runPromise(program)).rejects.toThrow()
281
+ })
282
+
283
+ it('should handle groups without optional fields', async () => {
284
+ const minimalGroups = {
285
+ 'minimal-group': {
286
+ id: 'minimal-group',
287
+ name: 'Minimal Group',
288
+ },
289
+ }
290
+
291
+ server.use(
292
+ http.get('*/a/groups/', () => {
293
+ return HttpResponse.text(`)]}'\n${JSON.stringify(minimalGroups)}`)
294
+ }),
295
+ )
296
+
297
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
298
+ const program = groupsCommand({ xml: true }).pipe(
299
+ Effect.provide(GerritApiServiceLive),
300
+ Effect.provide(mockConfigLayer),
301
+ )
302
+
303
+ await Effect.runPromise(program)
304
+
305
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
306
+ expect(output).toContain('<id><![CDATA[minimal-group]]></id>')
307
+ expect(output).toContain('<name><![CDATA[Minimal Group]]></name>')
308
+ expect(output).not.toContain('<description>')
309
+ expect(output).not.toContain('<owner>')
310
+ })
311
+
312
+ it('should respect limit parameter', async () => {
313
+ server.use(
314
+ http.get('*/a/groups/', ({ request }) => {
315
+ const url = new URL(request.url)
316
+ const limit = url.searchParams.get('n')
317
+ expect(limit).toBe('10')
318
+
319
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
320
+ }),
321
+ )
322
+
323
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
324
+ const program = groupsCommand({ limit: '10' }).pipe(
325
+ Effect.provide(GerritApiServiceLive),
326
+ Effect.provide(mockConfigLayer),
327
+ )
328
+
329
+ await Effect.runPromise(program)
330
+
331
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
332
+ expect(output).toContain('Available groups')
333
+ })
334
+ })
@@ -0,0 +1,83 @@
1
+ import type { Mock } from 'bun:test'
2
+ import { mock } from 'bun:test'
3
+ import type { SetupServer } from 'msw/node'
4
+ import { setupServer } from 'msw/node'
5
+ import { http, HttpResponse } from 'msw'
6
+ import { Layer } from 'effect'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './config-mock'
9
+
10
+ export const server: SetupServer = setupServer(
11
+ // Default handler for auth check
12
+ http.get('*/a/accounts/self', ({ request }) => {
13
+ const auth = request.headers.get('Authorization')
14
+ if (!auth || !auth.startsWith('Basic ')) {
15
+ return HttpResponse.text('Unauthorized', { status: 401 })
16
+ }
17
+ return HttpResponse.json({
18
+ _account_id: 1000,
19
+ name: 'Test User',
20
+ email: 'test@example.com',
21
+ })
22
+ }),
23
+ )
24
+
25
+ // Store captured output
26
+ export let capturedStdout: string[] = []
27
+ export let capturedErrors: string[] = []
28
+
29
+ // Mock process.stdout.write to capture JSON output
30
+ export const mockStdoutWrite: Mock<(chunk: unknown) => boolean> = mock(
31
+ (chunk: unknown): boolean => {
32
+ capturedStdout.push(String(chunk))
33
+ return true
34
+ },
35
+ )
36
+
37
+ // Mock console.error to capture errors
38
+ export const mockConsoleError: Mock<(...args: unknown[]) => void> = mock(
39
+ (...args: unknown[]): void => {
40
+ capturedErrors.push(args.join(' '))
41
+ },
42
+ )
43
+
44
+ // Mock process.exit to prevent test termination
45
+ export const mockProcessExit: Mock<(code?: number) => never> = mock((_code?: number): never => {
46
+ throw new Error('Process exited')
47
+ })
48
+
49
+ // Store original methods
50
+ export const originalStdoutWrite: typeof process.stdout.write = process.stdout.write
51
+ export const originalConsoleError: typeof console.error = console.error
52
+ export const originalProcessExit: typeof process.exit = process.exit
53
+
54
+ export const setupBuildStatusTests = (): void => {
55
+ server.listen({ onUnhandledRequest: 'bypass' })
56
+ // @ts-ignore - Mocking stdout
57
+ process.stdout.write = mockStdoutWrite
58
+ // @ts-ignore - Mocking console
59
+ console.error = mockConsoleError
60
+ // @ts-ignore - Mocking process.exit
61
+ process.exit = mockProcessExit
62
+ }
63
+
64
+ export const teardownBuildStatusTests = (): void => {
65
+ server.close()
66
+ // @ts-ignore - Restoring stdout
67
+ process.stdout.write = originalStdoutWrite
68
+ console.error = originalConsoleError
69
+ // @ts-ignore - Restoring process.exit
70
+ process.exit = originalProcessExit
71
+ }
72
+
73
+ export const resetBuildStatusMocks = (): void => {
74
+ server.resetHandlers()
75
+ mockStdoutWrite.mockClear()
76
+ mockConsoleError.mockClear()
77
+ mockProcessExit.mockClear()
78
+ capturedStdout = []
79
+ capturedErrors = []
80
+ }
81
+
82
+ export const createMockConfigLayer = (): Layer.Layer<ConfigService> =>
83
+ Layer.succeed(ConfigService, createMockConfigService())
@@ -0,0 +1,27 @@
1
+ import { Effect } from 'effect'
2
+ import type { ConfigServiceImpl } from '@/services/config'
3
+ import type { GerritCredentials } from '@/schemas/gerrit'
4
+ import type { AiConfig, AppConfig } from '@/schemas/config'
5
+
6
+ export const createMockConfigService = (
7
+ credentials: GerritCredentials = {
8
+ host: 'https://test.gerrit.com',
9
+ username: 'testuser',
10
+ password: 'testpass',
11
+ },
12
+ aiConfig: AiConfig = { autoDetect: true },
13
+ ): ConfigServiceImpl => ({
14
+ getCredentials: Effect.succeed(credentials),
15
+ saveCredentials: () => Effect.succeed(undefined as void),
16
+ deleteCredentials: Effect.succeed(undefined as void),
17
+ getAiConfig: Effect.succeed(aiConfig),
18
+ saveAiConfig: () => Effect.succeed(undefined as void),
19
+ getFullConfig: Effect.succeed({
20
+ host: credentials.host,
21
+ username: credentials.username,
22
+ password: credentials.password,
23
+ aiTool: aiConfig.tool,
24
+ aiAutoDetect: aiConfig.autoDetect ?? true,
25
+ } as AppConfig),
26
+ saveFullConfig: () => Effect.succeed(undefined as void),
27
+ })