@aaronshaf/ger 0.3.1 → 0.3.3
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.
- package/EXAMPLES.md +48 -0
- package/README.md +87 -0
- package/package.json +1 -1
- package/src/api/gerrit.ts +28 -0
- package/src/cli/commands/add-reviewer.ts +135 -0
- package/src/cli/commands/build-status.ts +69 -25
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/show.ts +20 -0
- package/src/cli/index.ts +115 -74
- package/src/schemas/gerrit.ts +43 -0
- package/src/services/commit-hook.ts +314 -0
- package/tests/add-reviewer.test.ts +393 -0
- package/tests/build-status-watch.test.ts +8 -11
- package/tests/build-status.test.ts +149 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/search.test.ts +712 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
|
@@ -0,0 +1,393 @@
|
|
|
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 { addReviewerCommand } from '@/cli/commands/add-reviewer'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
9
|
+
|
|
10
|
+
// Create MSW server
|
|
11
|
+
const server = setupServer(
|
|
12
|
+
// Default handler for auth check
|
|
13
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
14
|
+
const auth = request.headers.get('Authorization')
|
|
15
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
16
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
17
|
+
}
|
|
18
|
+
return HttpResponse.json({
|
|
19
|
+
_account_id: 1000,
|
|
20
|
+
name: 'Test User',
|
|
21
|
+
email: 'test@example.com',
|
|
22
|
+
})
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
describe('add-reviewer command', () => {
|
|
27
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
28
|
+
let mockConsoleError: ReturnType<typeof mock>
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
server.close()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
mockConsoleLog = mock(() => {})
|
|
40
|
+
mockConsoleError = mock(() => {})
|
|
41
|
+
console.log = mockConsoleLog
|
|
42
|
+
console.error = mockConsoleError
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
server.resetHandlers()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should add a single reviewer successfully', async () => {
|
|
50
|
+
server.use(
|
|
51
|
+
http.post('*/a/changes/12345/reviewers', async ({ request }) => {
|
|
52
|
+
const body = (await request.json()) as { reviewer: string; state?: string }
|
|
53
|
+
expect(body.reviewer).toBe('reviewer@example.com')
|
|
54
|
+
expect(body.state).toBe('REVIEWER')
|
|
55
|
+
return HttpResponse.text(
|
|
56
|
+
`)]}'\n${JSON.stringify({
|
|
57
|
+
input: 'reviewer@example.com',
|
|
58
|
+
reviewers: [
|
|
59
|
+
{
|
|
60
|
+
_account_id: 2000,
|
|
61
|
+
name: 'Reviewer User',
|
|
62
|
+
email: 'reviewer@example.com',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
})}`,
|
|
66
|
+
)
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
71
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
72
|
+
change: '12345',
|
|
73
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
74
|
+
|
|
75
|
+
await Effect.runPromise(program)
|
|
76
|
+
|
|
77
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
78
|
+
expect(output).toContain('Added Reviewer User as reviewer')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should add multiple reviewers successfully', async () => {
|
|
82
|
+
let callCount = 0
|
|
83
|
+
server.use(
|
|
84
|
+
http.post('*/a/changes/12345/reviewers', async ({ request }) => {
|
|
85
|
+
const body = (await request.json()) as { reviewer: string }
|
|
86
|
+
callCount++
|
|
87
|
+
const reviewerName = callCount === 1 ? 'User One' : 'User Two'
|
|
88
|
+
return HttpResponse.text(
|
|
89
|
+
`)]}'\n${JSON.stringify({
|
|
90
|
+
input: body.reviewer,
|
|
91
|
+
reviewers: [
|
|
92
|
+
{
|
|
93
|
+
_account_id: 2000 + callCount,
|
|
94
|
+
name: reviewerName,
|
|
95
|
+
email: body.reviewer,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
})}`,
|
|
99
|
+
)
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
104
|
+
const program = addReviewerCommand(['user1@example.com', 'user2@example.com'], {
|
|
105
|
+
change: '12345',
|
|
106
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
107
|
+
|
|
108
|
+
await Effect.runPromise(program)
|
|
109
|
+
|
|
110
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
111
|
+
expect(output).toContain('Added User One as reviewer')
|
|
112
|
+
expect(output).toContain('Added User Two as reviewer')
|
|
113
|
+
expect(callCount).toBe(2)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should add as CC when --cc flag is used', async () => {
|
|
117
|
+
server.use(
|
|
118
|
+
http.post('*/a/changes/12345/reviewers', async ({ request }) => {
|
|
119
|
+
const body = (await request.json()) as { reviewer: string; state?: string }
|
|
120
|
+
expect(body.reviewer).toBe('cc@example.com')
|
|
121
|
+
expect(body.state).toBe('CC')
|
|
122
|
+
return HttpResponse.text(
|
|
123
|
+
`)]}'\n${JSON.stringify({
|
|
124
|
+
input: 'cc@example.com',
|
|
125
|
+
ccs: [
|
|
126
|
+
{
|
|
127
|
+
_account_id: 2000,
|
|
128
|
+
name: 'CC User',
|
|
129
|
+
email: 'cc@example.com',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
})}`,
|
|
133
|
+
)
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
138
|
+
const program = addReviewerCommand(['cc@example.com'], {
|
|
139
|
+
change: '12345',
|
|
140
|
+
cc: true,
|
|
141
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
142
|
+
|
|
143
|
+
await Effect.runPromise(program)
|
|
144
|
+
|
|
145
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
146
|
+
expect(output).toContain('Added CC User as cc')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should pass notify option to API', async () => {
|
|
150
|
+
server.use(
|
|
151
|
+
http.post('*/a/changes/12345/reviewers', async ({ request }) => {
|
|
152
|
+
const body = (await request.json()) as { reviewer: string; notify?: string }
|
|
153
|
+
expect(body.notify).toBe('NONE')
|
|
154
|
+
return HttpResponse.text(
|
|
155
|
+
`)]}'\n${JSON.stringify({
|
|
156
|
+
input: 'reviewer@example.com',
|
|
157
|
+
reviewers: [
|
|
158
|
+
{
|
|
159
|
+
_account_id: 2000,
|
|
160
|
+
name: 'Reviewer',
|
|
161
|
+
email: 'reviewer@example.com',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
})}`,
|
|
165
|
+
)
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
170
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
171
|
+
change: '12345',
|
|
172
|
+
notify: 'none',
|
|
173
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
174
|
+
|
|
175
|
+
await Effect.runPromise(program)
|
|
176
|
+
|
|
177
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
178
|
+
expect(output).toContain('Added Reviewer as reviewer')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should handle API error in result', async () => {
|
|
182
|
+
server.use(
|
|
183
|
+
http.post('*/a/changes/12345/reviewers', async () => {
|
|
184
|
+
return HttpResponse.text(
|
|
185
|
+
`)]}'\n${JSON.stringify({
|
|
186
|
+
input: 'nonexistent@example.com',
|
|
187
|
+
error: 'Account not found: nonexistent@example.com',
|
|
188
|
+
})}`,
|
|
189
|
+
)
|
|
190
|
+
}),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
194
|
+
const program = addReviewerCommand(['nonexistent@example.com'], {
|
|
195
|
+
change: '12345',
|
|
196
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
197
|
+
|
|
198
|
+
await Effect.runPromise(program)
|
|
199
|
+
|
|
200
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
201
|
+
expect(errorOutput).toContain('Failed to add nonexistent@example.com')
|
|
202
|
+
expect(errorOutput).toContain('Account not found')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should show error when change ID is not provided', async () => {
|
|
206
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
207
|
+
const program = addReviewerCommand(['reviewer@example.com'], {}).pipe(
|
|
208
|
+
Effect.provide(GerritApiServiceLive),
|
|
209
|
+
Effect.provide(mockConfigLayer),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const result = await Effect.runPromiseExit(program)
|
|
213
|
+
expect(result._tag).toBe('Failure')
|
|
214
|
+
|
|
215
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
216
|
+
expect(errorOutput).toContain('Change ID is required')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should show error when no reviewers are provided', async () => {
|
|
220
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
221
|
+
const program = addReviewerCommand([], {
|
|
222
|
+
change: '12345',
|
|
223
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
224
|
+
|
|
225
|
+
const result = await Effect.runPromiseExit(program)
|
|
226
|
+
expect(result._tag).toBe('Failure')
|
|
227
|
+
|
|
228
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
229
|
+
expect(errorOutput).toContain('At least one reviewer is required')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should output XML format when --xml flag is used', async () => {
|
|
233
|
+
server.use(
|
|
234
|
+
http.post('*/a/changes/12345/reviewers', async () => {
|
|
235
|
+
return HttpResponse.text(
|
|
236
|
+
`)]}'\n${JSON.stringify({
|
|
237
|
+
input: 'reviewer@example.com',
|
|
238
|
+
reviewers: [
|
|
239
|
+
{
|
|
240
|
+
_account_id: 2000,
|
|
241
|
+
name: 'Reviewer User',
|
|
242
|
+
email: 'reviewer@example.com',
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
})}`,
|
|
246
|
+
)
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
251
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
252
|
+
change: '12345',
|
|
253
|
+
xml: true,
|
|
254
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
255
|
+
|
|
256
|
+
await Effect.runPromise(program)
|
|
257
|
+
|
|
258
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
259
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
260
|
+
expect(output).toContain('<add_reviewer_result>')
|
|
261
|
+
expect(output).toContain('<change_id>12345</change_id>')
|
|
262
|
+
expect(output).toContain('<state>reviewer</state>')
|
|
263
|
+
expect(output).toContain('<reviewer status="added">')
|
|
264
|
+
expect(output).toContain('<input>reviewer@example.com</input>')
|
|
265
|
+
expect(output).toContain('<name><![CDATA[Reviewer User]]></name>')
|
|
266
|
+
expect(output).toContain('<status>success</status>')
|
|
267
|
+
expect(output).toContain('</add_reviewer_result>')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should output XML format for errors when --xml flag is used', async () => {
|
|
271
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
272
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
273
|
+
xml: true,
|
|
274
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
275
|
+
|
|
276
|
+
const result = await Effect.runPromiseExit(program)
|
|
277
|
+
expect(result._tag).toBe('Failure')
|
|
278
|
+
|
|
279
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
280
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
281
|
+
expect(output).toContain('<add_reviewer_result>')
|
|
282
|
+
expect(output).toContain('<status>error</status>')
|
|
283
|
+
expect(output).toContain('<error><![CDATA[Change ID is required')
|
|
284
|
+
expect(output).toContain('</add_reviewer_result>')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('should handle network errors gracefully', async () => {
|
|
288
|
+
server.use(
|
|
289
|
+
http.post('*/a/changes/12345/reviewers', () => {
|
|
290
|
+
return HttpResponse.text('Internal Server Error', { status: 500 })
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
295
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
296
|
+
change: '12345',
|
|
297
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
298
|
+
|
|
299
|
+
await Effect.runPromise(program)
|
|
300
|
+
|
|
301
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
302
|
+
expect(errorOutput).toContain('Failed to add reviewer@example.com')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should handle partial success with multiple reviewers', async () => {
|
|
306
|
+
let callCount = 0
|
|
307
|
+
server.use(
|
|
308
|
+
http.post('*/a/changes/12345/reviewers', async ({ request }) => {
|
|
309
|
+
const body = (await request.json()) as { reviewer: string }
|
|
310
|
+
callCount++
|
|
311
|
+
if (callCount === 1) {
|
|
312
|
+
return HttpResponse.text(
|
|
313
|
+
`)]}'\n${JSON.stringify({
|
|
314
|
+
input: body.reviewer,
|
|
315
|
+
reviewers: [
|
|
316
|
+
{
|
|
317
|
+
_account_id: 2001,
|
|
318
|
+
name: 'Valid User',
|
|
319
|
+
email: body.reviewer,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
})}`,
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
return HttpResponse.text(
|
|
326
|
+
`)]}'\n${JSON.stringify({
|
|
327
|
+
input: body.reviewer,
|
|
328
|
+
error: 'Account not found',
|
|
329
|
+
})}`,
|
|
330
|
+
)
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
335
|
+
const program = addReviewerCommand(['valid@example.com', 'invalid@example.com'], {
|
|
336
|
+
change: '12345',
|
|
337
|
+
xml: true,
|
|
338
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
339
|
+
|
|
340
|
+
await Effect.runPromise(program)
|
|
341
|
+
|
|
342
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
343
|
+
expect(output).toContain('<status>partial_failure</status>')
|
|
344
|
+
expect(output).toContain('<reviewer status="added">')
|
|
345
|
+
expect(output).toContain('<reviewer status="failed">')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('should reject invalid notify option', async () => {
|
|
349
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
350
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
351
|
+
change: '12345',
|
|
352
|
+
notify: 'invalid',
|
|
353
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
354
|
+
|
|
355
|
+
const result = await Effect.runPromiseExit(program)
|
|
356
|
+
expect(result._tag).toBe('Failure')
|
|
357
|
+
|
|
358
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
359
|
+
expect(errorOutput).toContain('Invalid notify level: invalid')
|
|
360
|
+
expect(errorOutput).toContain('Valid values: none, owner, owner_reviewers, all')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('should pass REVIEWER state by default (not CC)', async () => {
|
|
364
|
+
let receivedState: string | undefined
|
|
365
|
+
server.use(
|
|
366
|
+
http.post('*/a/changes/12345/reviewers', async ({ request }) => {
|
|
367
|
+
const body = (await request.json()) as { reviewer: string; state?: string }
|
|
368
|
+
receivedState = body.state
|
|
369
|
+
return HttpResponse.text(
|
|
370
|
+
`)]}'\n${JSON.stringify({
|
|
371
|
+
input: body.reviewer,
|
|
372
|
+
reviewers: [
|
|
373
|
+
{
|
|
374
|
+
_account_id: 2000,
|
|
375
|
+
name: 'Reviewer',
|
|
376
|
+
email: body.reviewer,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
})}`,
|
|
380
|
+
)
|
|
381
|
+
}),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
385
|
+
const program = addReviewerCommand(['reviewer@example.com'], {
|
|
386
|
+
change: '12345',
|
|
387
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
388
|
+
|
|
389
|
+
await Effect.runPromise(program)
|
|
390
|
+
|
|
391
|
+
expect(receivedState).toBe('REVIEWER')
|
|
392
|
+
})
|
|
393
|
+
})
|
|
@@ -92,12 +92,8 @@ describe('build-status command - watch mode', () => {
|
|
|
92
92
|
expect(JSON.parse(capturedStdout[1])).toEqual({ state: 'running' })
|
|
93
93
|
expect(JSON.parse(capturedStdout[2])).toEqual({ state: 'success' })
|
|
94
94
|
|
|
95
|
-
//
|
|
96
|
-
expect(capturedErrors.length).
|
|
97
|
-
expect(capturedErrors.some((e: string) => e.includes('Watching build status'))).toBe(true)
|
|
98
|
-
expect(
|
|
99
|
-
capturedErrors.some((e: string) => e.includes('Build completed with status: success')),
|
|
100
|
-
).toBe(true)
|
|
95
|
+
// Minimalistic output: no stderr messages except on timeout/error
|
|
96
|
+
expect(capturedErrors.length).toBe(0)
|
|
101
97
|
})
|
|
102
98
|
|
|
103
99
|
test('polls until failure state is reached', async () => {
|
|
@@ -155,9 +151,9 @@ describe('build-status command - watch mode', () => {
|
|
|
155
151
|
|
|
156
152
|
expect(capturedStdout.length).toBeGreaterThanOrEqual(2)
|
|
157
153
|
expect(JSON.parse(capturedStdout[capturedStdout.length - 1])).toEqual({ state: 'failure' })
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
).toBe(
|
|
154
|
+
|
|
155
|
+
// Minimalistic output: no stderr messages except on timeout/error
|
|
156
|
+
expect(capturedErrors.length).toBe(0)
|
|
161
157
|
})
|
|
162
158
|
|
|
163
159
|
test('times out after specified duration', async () => {
|
|
@@ -303,9 +299,10 @@ describe('build-status command - watch mode', () => {
|
|
|
303
299
|
|
|
304
300
|
expect(capturedStdout.length).toBe(1)
|
|
305
301
|
expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'not_found' })
|
|
302
|
+
|
|
306
303
|
// 404 errors bypass pollBuildStatus and are handled in error handler
|
|
307
|
-
//
|
|
308
|
-
expect(capturedErrors.
|
|
304
|
+
// Minimalistic output: no stderr messages for not_found state
|
|
305
|
+
expect(capturedErrors.length).toBe(0)
|
|
309
306
|
})
|
|
310
307
|
|
|
311
308
|
test('without watch flag, behaves as single check', async () => {
|
|
@@ -637,4 +637,153 @@ describe('build-status command', () => {
|
|
|
637
637
|
// Regex should handle extra whitespace
|
|
638
638
|
expect(output).toEqual({ state: 'running' })
|
|
639
639
|
})
|
|
640
|
+
|
|
641
|
+
test('ignores verification from older patchset when newer patchset build is running', async () => {
|
|
642
|
+
// This test replicates the bug scenario:
|
|
643
|
+
// - PS 3 build started, then PS 4 build started
|
|
644
|
+
// - PS 3 verification (-1) comes AFTER PS 4 build started
|
|
645
|
+
// - Should return "running" because PS 4 has no verification yet
|
|
646
|
+
const messages: MessageInfo[] = [
|
|
647
|
+
{
|
|
648
|
+
id: 'msg1',
|
|
649
|
+
message: 'Build Started https://jenkins.example.com/job/123/',
|
|
650
|
+
date: '2024-01-15 11:12:00.000000000',
|
|
651
|
+
_revision_number: 2,
|
|
652
|
+
author: {
|
|
653
|
+
_account_id: 9999,
|
|
654
|
+
name: 'Service Cloud Jenkins',
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: 'msg2',
|
|
659
|
+
message: 'Patch Set 2: Verified -1\n\nBuild Failed',
|
|
660
|
+
date: '2024-01-15 11:23:00.000000000',
|
|
661
|
+
_revision_number: 2,
|
|
662
|
+
author: {
|
|
663
|
+
_account_id: 9999,
|
|
664
|
+
name: 'Service Cloud Jenkins',
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
id: 'msg3',
|
|
669
|
+
message: 'Build Started https://jenkins.example.com/job/456/',
|
|
670
|
+
date: '2024-01-15 13:57:00.000000000',
|
|
671
|
+
_revision_number: 3,
|
|
672
|
+
author: {
|
|
673
|
+
_account_id: 9999,
|
|
674
|
+
name: 'Service Cloud Jenkins',
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: 'msg4',
|
|
679
|
+
message: 'Build Started https://jenkins.example.com/job/789/',
|
|
680
|
+
date: '2024-01-15 14:02:00.000000000',
|
|
681
|
+
_revision_number: 4,
|
|
682
|
+
author: {
|
|
683
|
+
_account_id: 9999,
|
|
684
|
+
name: 'Service Cloud Jenkins',
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
id: 'msg5',
|
|
689
|
+
message: 'Patch Set 3: Verified -1\n\nBuild Failed : ABORTED',
|
|
690
|
+
date: '2024-01-15 14:03:00.000000000',
|
|
691
|
+
_revision_number: 3,
|
|
692
|
+
author: {
|
|
693
|
+
_account_id: 9999,
|
|
694
|
+
name: 'Service Cloud Jenkins',
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
]
|
|
698
|
+
|
|
699
|
+
server.use(
|
|
700
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
701
|
+
const url = new URL(request.url)
|
|
702
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
703
|
+
return HttpResponse.json(
|
|
704
|
+
{ messages },
|
|
705
|
+
{
|
|
706
|
+
headers: { 'Content-Type': 'application/json' },
|
|
707
|
+
},
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
711
|
+
}),
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
715
|
+
Effect.provide(GerritApiServiceLive),
|
|
716
|
+
Effect.provide(createMockConfigLayer()),
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
await Effect.runPromise(effect)
|
|
720
|
+
|
|
721
|
+
expect(capturedStdout.length).toBe(1)
|
|
722
|
+
const output = JSON.parse(capturedStdout[0])
|
|
723
|
+
// PS 4 build started at 14:02, PS 3 verification at 14:03 should be IGNORED
|
|
724
|
+
// because it's for a different revision. PS 4 build is still running.
|
|
725
|
+
expect(output).toEqual({ state: 'running' })
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
test('returns success when verification matches the latest patchset', async () => {
|
|
729
|
+
const messages: MessageInfo[] = [
|
|
730
|
+
{
|
|
731
|
+
id: 'msg1',
|
|
732
|
+
message: 'Build Started',
|
|
733
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
734
|
+
_revision_number: 1,
|
|
735
|
+
author: {
|
|
736
|
+
_account_id: 9999,
|
|
737
|
+
name: 'CI Bot',
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: 'msg2',
|
|
742
|
+
message: 'Build Started',
|
|
743
|
+
date: '2024-01-15 11:00:00.000000000',
|
|
744
|
+
_revision_number: 2,
|
|
745
|
+
author: {
|
|
746
|
+
_account_id: 9999,
|
|
747
|
+
name: 'CI Bot',
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
id: 'msg3',
|
|
752
|
+
message: 'Patch Set 2: Verified+1',
|
|
753
|
+
date: '2024-01-15 11:15:00.000000000',
|
|
754
|
+
_revision_number: 2,
|
|
755
|
+
author: {
|
|
756
|
+
_account_id: 9999,
|
|
757
|
+
name: 'CI Bot',
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
]
|
|
761
|
+
|
|
762
|
+
server.use(
|
|
763
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
764
|
+
const url = new URL(request.url)
|
|
765
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
766
|
+
return HttpResponse.json(
|
|
767
|
+
{ messages },
|
|
768
|
+
{
|
|
769
|
+
headers: { 'Content-Type': 'application/json' },
|
|
770
|
+
},
|
|
771
|
+
)
|
|
772
|
+
}
|
|
773
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
774
|
+
}),
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
778
|
+
Effect.provide(GerritApiServiceLive),
|
|
779
|
+
Effect.provide(createMockConfigLayer()),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
await Effect.runPromise(effect)
|
|
783
|
+
|
|
784
|
+
expect(capturedStdout.length).toBe(1)
|
|
785
|
+
const output = JSON.parse(capturedStdout[0])
|
|
786
|
+
// PS 2 build started at 11:00, PS 2 verification at 11:15 - same revision, success
|
|
787
|
+
expect(output).toEqual({ state: 'success' })
|
|
788
|
+
})
|
|
640
789
|
})
|