@aaronshaf/ger 3.0.2 → 4.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.
@@ -0,0 +1,197 @@
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test'
2
+ import { HttpResponse, http } from 'msw'
3
+ import { setupServer } from 'msw/node'
4
+ import { Effect, Layer } from 'effect'
5
+ import { analyzeCommand } from '@/cli/commands/analyze'
6
+ import { GerritApiServiceLive } from '@/api/gerrit'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './helpers/config-mock'
9
+ import type { ChangeInfo } from '@/schemas/gerrit'
10
+
11
+ const makeChange = (overrides: Partial<ChangeInfo> = {}): ChangeInfo => ({
12
+ id: 'project~main~I123',
13
+ _number: 12345,
14
+ project: 'my-project',
15
+ branch: 'main',
16
+ change_id: 'I123',
17
+ subject: 'Test change',
18
+ status: 'MERGED',
19
+ created: '2025-01-15 10:00:00.000000000',
20
+ updated: '2025-01-15 12:00:00.000000000',
21
+ submitted: '2025-01-15 12:00:00.000000000',
22
+ owner: { _account_id: 1001, name: 'Alice Smith', email: 'alice@example.com' },
23
+ ...overrides,
24
+ })
25
+
26
+ const mockChanges: ChangeInfo[] = [
27
+ makeChange({
28
+ _number: 1,
29
+ project: 'repo-a',
30
+ owner: { _account_id: 1, name: 'Alice', email: 'alice@x.com' },
31
+ }),
32
+ makeChange({
33
+ _number: 2,
34
+ project: 'repo-a',
35
+ owner: { _account_id: 1, name: 'Alice', email: 'alice@x.com' },
36
+ }),
37
+ makeChange({
38
+ _number: 3,
39
+ project: 'repo-b',
40
+ owner: { _account_id: 2, name: 'Bob', email: 'bob@x.com' },
41
+ submitted: '2025-02-10 10:00:00.000000000',
42
+ }),
43
+ ]
44
+
45
+ const server = setupServer(
46
+ http.get('*/a/accounts/self', () =>
47
+ HttpResponse.json({ _account_id: 1, name: 'User', email: 'u@example.com' }),
48
+ ),
49
+ http.get('*/a/changes/', () => HttpResponse.json(mockChanges)),
50
+ )
51
+
52
+ beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
53
+ afterAll(() => server.close())
54
+ afterEach(() => server.resetHandlers())
55
+
56
+ const mockConfig = createMockConfigService()
57
+
58
+ describe('analyze command', () => {
59
+ test('runs without error and outputs terminal UI', async () => {
60
+ const logs: string[] = []
61
+ const origLog = console.log
62
+ console.log = (...args: unknown[]) => logs.push(args.join(' '))
63
+
64
+ try {
65
+ await Effect.runPromise(
66
+ analyzeCommand({}).pipe(
67
+ Effect.provide(GerritApiServiceLive),
68
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
69
+ ),
70
+ )
71
+ } finally {
72
+ console.log = origLog
73
+ }
74
+
75
+ const output = logs.join('\n')
76
+ expect(output).toContain('repo-a')
77
+ expect(output).toContain('Alice')
78
+ })
79
+
80
+ test('outputs JSON', async () => {
81
+ const logs: string[] = []
82
+ const origLog = console.log
83
+ console.log = (...args: unknown[]) => logs.push(args.join(' '))
84
+
85
+ try {
86
+ await Effect.runPromise(
87
+ analyzeCommand({ json: true }).pipe(
88
+ Effect.provide(GerritApiServiceLive),
89
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
90
+ ),
91
+ )
92
+ } finally {
93
+ console.log = origLog
94
+ }
95
+
96
+ const jsonStr = logs.find((l) => l.startsWith('{'))
97
+ expect(jsonStr).toBeDefined()
98
+ const parsed = JSON.parse(jsonStr as string) as { totalMerged: number }
99
+ expect(parsed.totalMerged).toBe(3)
100
+ })
101
+
102
+ test('outputs XML', async () => {
103
+ const logs: string[] = []
104
+ const origLog = console.log
105
+ console.log = (...args: unknown[]) => logs.push(args.join(' '))
106
+
107
+ try {
108
+ await Effect.runPromise(
109
+ analyzeCommand({ xml: true }).pipe(
110
+ Effect.provide(GerritApiServiceLive),
111
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
112
+ ),
113
+ )
114
+ } finally {
115
+ console.log = origLog
116
+ }
117
+
118
+ const output = logs.join('\n')
119
+ expect(output).toContain('<analytics>')
120
+ expect(output).toContain('<total_merged>3</total_merged>')
121
+ })
122
+
123
+ test('outputs markdown', async () => {
124
+ const logs: string[] = []
125
+ const origLog = console.log
126
+ console.log = (...args: unknown[]) => logs.push(args.join(' '))
127
+
128
+ try {
129
+ await Effect.runPromise(
130
+ analyzeCommand({ markdown: true }).pipe(
131
+ Effect.provide(GerritApiServiceLive),
132
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
133
+ ),
134
+ )
135
+ } finally {
136
+ console.log = origLog
137
+ }
138
+
139
+ const output = logs.join('\n')
140
+ expect(output).toContain('# Contribution Analytics')
141
+ expect(output).toContain('| Repository | Count |')
142
+ })
143
+
144
+ test('outputs CSV', async () => {
145
+ const logs: string[] = []
146
+ const origLog = console.log
147
+ console.log = (...args: unknown[]) => logs.push(args.join(' '))
148
+
149
+ try {
150
+ await Effect.runPromise(
151
+ analyzeCommand({ csv: true }).pipe(
152
+ Effect.provide(GerritApiServiceLive),
153
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
154
+ ),
155
+ )
156
+ } finally {
157
+ console.log = origLog
158
+ }
159
+
160
+ const output = logs.join('\n')
161
+ expect(output).toContain('section,key,count')
162
+ expect(output).toContain('repo,"repo-a",2')
163
+ })
164
+
165
+ test('filters by repo via query', async () => {
166
+ let capturedUrl = ''
167
+ server.use(
168
+ http.get('*/a/changes/', ({ request }) => {
169
+ capturedUrl = request.url
170
+ return HttpResponse.json([mockChanges[0]])
171
+ }),
172
+ )
173
+
174
+ await Effect.runPromise(
175
+ analyzeCommand({ repo: 'my-repo', json: true }).pipe(
176
+ Effect.provide(GerritApiServiceLive),
177
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
178
+ ),
179
+ )
180
+
181
+ expect(capturedUrl).toContain('project%3Amy-repo')
182
+ })
183
+
184
+ test('fails gracefully when API returns error', async () => {
185
+ server.use(http.get('*/a/changes/', () => HttpResponse.json({}, { status: 500 })))
186
+
187
+ const result = await Effect.runPromise(
188
+ analyzeCommand({}).pipe(
189
+ Effect.provide(GerritApiServiceLive),
190
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
191
+ Effect.either,
192
+ ),
193
+ )
194
+
195
+ expect(result._tag).toBe('Left')
196
+ })
197
+ })
@@ -0,0 +1,208 @@
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach, mock, spyOn } from 'bun:test'
2
+ import { HttpResponse, http } from 'msw'
3
+ import { setupServer } from 'msw/node'
4
+ import { Effect, Layer } from 'effect'
5
+ import { cherryCommand } from '@/cli/commands/cherry'
6
+ import { GerritApiServiceLive } from '@/api/gerrit'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './helpers/config-mock'
9
+ import type { ChangeInfo, RevisionInfo } from '@/schemas/gerrit'
10
+ import * as childProcess from 'node:child_process'
11
+ import type { SpawnSyncReturns } from 'node:child_process'
12
+
13
+ const mockChange: ChangeInfo = {
14
+ id: 'test-project~main~I123',
15
+ _number: 12345,
16
+ project: 'test-project',
17
+ branch: 'main',
18
+ change_id: 'I123',
19
+ subject: 'Test cherry-pick change',
20
+ status: 'NEW',
21
+ created: '2024-01-15 10:00:00.000000000',
22
+ updated: '2024-01-15 10:00:00.000000000',
23
+ }
24
+
25
+ const mockRevision: RevisionInfo = {
26
+ _number: 1,
27
+ ref: 'refs/changes/45/12345/1',
28
+ created: '2024-01-15 10:00:00.000000000',
29
+ uploader: { _account_id: 1000, name: 'Test User', email: 'test@example.com' },
30
+ }
31
+
32
+ const server = setupServer(
33
+ http.get('*/a/accounts/self', ({ request }) => {
34
+ const auth = request.headers.get('Authorization')
35
+ if (!auth?.startsWith('Basic ')) return HttpResponse.text('Unauthorized', { status: 401 })
36
+ return HttpResponse.json({ _account_id: 1000, name: 'Test User', email: 'test@example.com' })
37
+ }),
38
+ http.get('*/a/changes/12345', () => HttpResponse.json(mockChange)),
39
+ http.get('*/a/changes/12345/revisions/current', () => HttpResponse.json(mockRevision)),
40
+ )
41
+
42
+ beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
43
+ afterAll(() => server.close())
44
+
45
+ const mockConfig = createMockConfigService({
46
+ host: 'https://test.gerrit.com',
47
+ username: 'testuser',
48
+ password: 'testpass',
49
+ })
50
+
51
+ describe('cherry command', () => {
52
+ let mockExecSync: ReturnType<typeof spyOn>
53
+ let mockSpawnSync: ReturnType<typeof spyOn>
54
+
55
+ afterEach(() => {
56
+ server.resetHandlers()
57
+ mockExecSync?.mockRestore()
58
+ mockSpawnSync?.mockRestore()
59
+ })
60
+
61
+ const setupGitMocks = (spawnOverrides: { failFetch?: boolean; failCherry?: boolean } = {}) => {
62
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((command: string) => {
63
+ if (command.includes('rev-parse --git-dir')) return Buffer.from('.git')
64
+ if (command.includes('remote -v'))
65
+ return Buffer.from('origin\thttps://test.gerrit.com/project\t(fetch)\n')
66
+ return Buffer.from('')
67
+ }) as typeof childProcess.execSync)
68
+
69
+ mockSpawnSync = spyOn(childProcess, 'spawnSync').mockImplementation(((
70
+ _cmd: string,
71
+ args: string[],
72
+ ) => {
73
+ const isCherry = args.includes('cherry-pick')
74
+ if (isCherry && spawnOverrides.failCherry) {
75
+ return {
76
+ status: 1,
77
+ stderr: Buffer.from('conflict during cherry-pick'),
78
+ } as unknown as SpawnSyncReturns<Buffer>
79
+ }
80
+ if (!isCherry && spawnOverrides.failFetch) {
81
+ return {
82
+ status: 1,
83
+ stderr: Buffer.from('fetch failed'),
84
+ } as unknown as SpawnSyncReturns<Buffer>
85
+ }
86
+ return { status: 0, stderr: Buffer.from('') } as unknown as SpawnSyncReturns<Buffer>
87
+ }) as typeof childProcess.spawnSync)
88
+ }
89
+
90
+ test('cherry-picks a change successfully', async () => {
91
+ setupGitMocks()
92
+
93
+ await Effect.runPromise(
94
+ cherryCommand('12345', {}).pipe(
95
+ Effect.provide(GerritApiServiceLive),
96
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
97
+ ),
98
+ )
99
+
100
+ const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
101
+ expect(spawnCalls.some(([, args]) => args.includes('fetch'))).toBe(true)
102
+ expect(
103
+ spawnCalls.some(([, args]) => args.includes('cherry-pick') && args.includes('FETCH_HEAD')),
104
+ ).toBe(true)
105
+ expect(spawnCalls.some(([, args]) => args.includes('cherry-pick') && args.includes('-n'))).toBe(
106
+ false,
107
+ )
108
+ })
109
+
110
+ test('cherry-picks with --no-commit flag', async () => {
111
+ setupGitMocks()
112
+
113
+ await Effect.runPromise(
114
+ cherryCommand('12345', { noCommit: true }).pipe(
115
+ Effect.provide(GerritApiServiceLive),
116
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
117
+ ),
118
+ )
119
+
120
+ const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
121
+ expect(spawnCalls.some(([, args]) => args.includes('cherry-pick') && args.includes('-n'))).toBe(
122
+ true,
123
+ )
124
+ })
125
+
126
+ test('parses 12345/3 patchset syntax', async () => {
127
+ setupGitMocks()
128
+
129
+ server.use(
130
+ http.get('*/a/changes/12345/revisions/3', () =>
131
+ HttpResponse.json({ ...mockRevision, _number: 3, ref: 'refs/changes/45/12345/3' }),
132
+ ),
133
+ )
134
+
135
+ await Effect.runPromise(
136
+ cherryCommand('12345/3', {}).pipe(
137
+ Effect.provide(GerritApiServiceLive),
138
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
139
+ ),
140
+ )
141
+
142
+ const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
143
+ expect(spawnCalls.some(([, args]) => args.includes('refs/changes/45/12345/3'))).toBe(true)
144
+ })
145
+
146
+ test('fails when not in a git repo', async () => {
147
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation((() => {
148
+ throw new Error('not a git repo')
149
+ }) as typeof childProcess.execSync)
150
+
151
+ const result = await Effect.runPromise(
152
+ cherryCommand('12345', {}).pipe(
153
+ Effect.provide(GerritApiServiceLive),
154
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
155
+ Effect.either,
156
+ ),
157
+ )
158
+ expect(result._tag).toBe('Left')
159
+ })
160
+
161
+ test('fails when change not found', async () => {
162
+ setupGitMocks()
163
+ server.use(http.get('*/a/changes/99999', () => HttpResponse.json({}, { status: 404 })))
164
+
165
+ const result = await Effect.runPromise(
166
+ cherryCommand('99999', {}).pipe(
167
+ Effect.provide(GerritApiServiceLive),
168
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
169
+ Effect.either,
170
+ ),
171
+ )
172
+ expect(result._tag).toBe('Left')
173
+ })
174
+
175
+ test('fails when git cherry-pick fails', async () => {
176
+ setupGitMocks({ failCherry: true })
177
+
178
+ let threw = false
179
+ try {
180
+ await Effect.runPromise(
181
+ cherryCommand('12345', {}).pipe(
182
+ Effect.provide(GerritApiServiceLive),
183
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
184
+ ),
185
+ )
186
+ } catch (e) {
187
+ threw = true
188
+ expect(String(e)).toContain('Cherry-pick failed')
189
+ }
190
+ expect(threw).toBe(true)
191
+ })
192
+
193
+ test('uses --remote option when provided', async () => {
194
+ setupGitMocks()
195
+
196
+ await Effect.runPromise(
197
+ cherryCommand('12345', { remote: 'upstream' }).pipe(
198
+ Effect.provide(GerritApiServiceLive),
199
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
200
+ ),
201
+ )
202
+
203
+ const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
204
+ expect(spawnCalls.some(([, args]) => args.includes('fetch') && args.includes('upstream'))).toBe(
205
+ true,
206
+ )
207
+ })
208
+ })
@@ -0,0 +1,212 @@
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test'
2
+ import { HttpResponse, http } from 'msw'
3
+ import { setupServer } from 'msw/node'
4
+ import { Effect, Layer } from 'effect'
5
+ import { failuresCommand } from '@/cli/commands/failures'
6
+ import { GerritApiServiceLive } from '@/api/gerrit'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './helpers/config-mock'
9
+
10
+ const JENKINS_URL = 'https://jenkins.inst-ci.net/job/Canvas/job/main/123//build-summary-report/'
11
+
12
+ const makeMessage = (id: string, message: string, authorName = 'Test User') => ({
13
+ id,
14
+ message,
15
+ date: '2025-01-15 10:00:00.000000000',
16
+ author: { _account_id: 1, name: authorName, email: 'test@example.com' },
17
+ })
18
+
19
+ const makeMessagesResponse = (messages: ReturnType<typeof makeMessage>[]) => ({
20
+ messages,
21
+ })
22
+
23
+ const defaultMessages = [
24
+ makeMessage('m1', 'Build started', 'Service Cloud Jenkins'),
25
+ makeMessage(
26
+ 'm2',
27
+ `Patch Set 1: Verified-1\n\nBuild failed. See ${JENKINS_URL}`,
28
+ 'Service Cloud Jenkins',
29
+ ),
30
+ ]
31
+
32
+ const server = setupServer(
33
+ http.get('*/a/accounts/self', () =>
34
+ HttpResponse.json({ _account_id: 1, name: 'User', email: 'u@example.com' }),
35
+ ),
36
+ http.get('*/a/changes/12345', () => HttpResponse.json(makeMessagesResponse(defaultMessages))),
37
+ )
38
+
39
+ beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
40
+ afterAll(() => server.close())
41
+ afterEach(() => server.resetHandlers())
42
+
43
+ const mockConfig = createMockConfigService()
44
+
45
+ describe('failures command', () => {
46
+ test('outputs the Jenkins failure URL', async () => {
47
+ const logs: string[] = []
48
+ const origLog = console.log
49
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
50
+
51
+ try {
52
+ await Effect.runPromise(
53
+ failuresCommand('12345', {}).pipe(
54
+ Effect.provide(GerritApiServiceLive),
55
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
56
+ ),
57
+ )
58
+ } finally {
59
+ console.log = origLog
60
+ }
61
+
62
+ expect(logs.join('\n')).toContain(JENKINS_URL)
63
+ })
64
+
65
+ test('outputs JSON with url field', async () => {
66
+ const logs: string[] = []
67
+ const origLog = console.log
68
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
69
+
70
+ try {
71
+ await Effect.runPromise(
72
+ failuresCommand('12345', { json: true }).pipe(
73
+ Effect.provide(GerritApiServiceLive),
74
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
75
+ ),
76
+ )
77
+ } finally {
78
+ console.log = origLog
79
+ }
80
+
81
+ const parsed = JSON.parse(logs.join('')) as { status: string; url: string }
82
+ expect(parsed.status).toBe('found')
83
+ expect(parsed.url).toBe(JENKINS_URL)
84
+ })
85
+
86
+ test('outputs XML with url element', async () => {
87
+ const logs: string[] = []
88
+ const origLog = console.log
89
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
90
+
91
+ try {
92
+ await Effect.runPromise(
93
+ failuresCommand('12345', { xml: true }).pipe(
94
+ Effect.provide(GerritApiServiceLive),
95
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
96
+ ),
97
+ )
98
+ } finally {
99
+ console.log = origLog
100
+ }
101
+
102
+ const output = logs.join('\n')
103
+ expect(output).toContain('<failures>')
104
+ expect(output).toContain(`<url>${JENKINS_URL}</url>`)
105
+ })
106
+
107
+ test('ignores messages not from Service Cloud Jenkins', async () => {
108
+ server.use(
109
+ http.get('*/a/changes/12345', () =>
110
+ HttpResponse.json(
111
+ makeMessagesResponse([
112
+ makeMessage('m1', `Verified-1\n\nFailed: ${JENKINS_URL}`, 'Some Other Bot'),
113
+ ]),
114
+ ),
115
+ ),
116
+ )
117
+
118
+ const logs: string[] = []
119
+ const origLog = console.log
120
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
121
+
122
+ try {
123
+ await Effect.runPromise(
124
+ failuresCommand('12345', {}).pipe(
125
+ Effect.provide(GerritApiServiceLive),
126
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
127
+ ),
128
+ )
129
+ } finally {
130
+ console.log = origLog
131
+ }
132
+
133
+ expect(logs.join('\n')).not.toContain(JENKINS_URL)
134
+ expect(logs.join('\n')).toContain('No build failure links found')
135
+ })
136
+
137
+ test('ignores Service Cloud Jenkins messages without Verified-1', async () => {
138
+ server.use(
139
+ http.get('*/a/changes/12345', () =>
140
+ HttpResponse.json(
141
+ makeMessagesResponse([
142
+ makeMessage('m1', `Build started: ${JENKINS_URL}`, 'Service Cloud Jenkins'),
143
+ ]),
144
+ ),
145
+ ),
146
+ )
147
+
148
+ const logs: string[] = []
149
+ const origLog = console.log
150
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
151
+
152
+ try {
153
+ await Effect.runPromise(
154
+ failuresCommand('12345', {}).pipe(
155
+ Effect.provide(GerritApiServiceLive),
156
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
157
+ ),
158
+ )
159
+ } finally {
160
+ console.log = origLog
161
+ }
162
+
163
+ expect(logs.join('\n')).not.toContain(JENKINS_URL)
164
+ expect(logs.join('\n')).toContain('No build failure links found')
165
+ })
166
+
167
+ test('returns most recent failure when multiple exist', async () => {
168
+ const NEWER_URL = 'https://jenkins.inst-ci.net/job/Canvas/job/main/456//build-summary-report/'
169
+ server.use(
170
+ http.get('*/a/changes/12345', () =>
171
+ HttpResponse.json(
172
+ makeMessagesResponse([
173
+ makeMessage('m1', `Verified-1\n\nFailed: ${JENKINS_URL}`, 'Service Cloud Jenkins'),
174
+ makeMessage('m2', `Verified-1\n\nFailed: ${NEWER_URL}`, 'Service Cloud Jenkins'),
175
+ ]),
176
+ ),
177
+ ),
178
+ )
179
+
180
+ const logs: string[] = []
181
+ const origLog = console.log
182
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
183
+
184
+ try {
185
+ await Effect.runPromise(
186
+ failuresCommand('12345', {}).pipe(
187
+ Effect.provide(GerritApiServiceLive),
188
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
189
+ ),
190
+ )
191
+ } finally {
192
+ console.log = origLog
193
+ }
194
+
195
+ expect(logs.join('\n')).toContain(NEWER_URL)
196
+ expect(logs.join('\n')).not.toContain(JENKINS_URL)
197
+ })
198
+
199
+ test('fails when change not found', async () => {
200
+ server.use(http.get('*/a/changes/99999', () => HttpResponse.json({}, { status: 404 })))
201
+
202
+ const result = await Effect.runPromise(
203
+ failuresCommand('99999', {}).pipe(
204
+ Effect.provide(GerritApiServiceLive),
205
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
206
+ Effect.either,
207
+ ),
208
+ )
209
+
210
+ expect(result._tag).toBe('Left')
211
+ })
212
+ })
@@ -10,6 +10,7 @@ export const createMockConfigService = (
10
10
  password: 'testpass',
11
11
  },
12
12
  aiConfig: AiConfig = { autoDetect: true },
13
+ retriggerComment?: string,
13
14
  ): ConfigServiceImpl => ({
14
15
  getCredentials: Effect.succeed(credentials),
15
16
  saveCredentials: () => Effect.succeed(undefined as void),
@@ -22,6 +23,9 @@ export const createMockConfigService = (
22
23
  password: credentials.password,
23
24
  aiTool: aiConfig.tool,
24
25
  aiAutoDetect: aiConfig.autoDetect ?? true,
26
+ retriggerComment,
25
27
  } as AppConfig),
26
28
  saveFullConfig: () => Effect.succeed(undefined as void),
29
+ getRetriggerComment: Effect.succeed(retriggerComment),
30
+ saveRetriggerComment: () => Effect.succeed(undefined as void),
27
31
  })