@aaronshaf/ger 0.2.2 → 0.2.5
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/CLAUDE.md +3 -3
- package/README.md +49 -0
- package/package.json +1 -1
- package/src/cli/commands/build-status.ts +116 -0
- package/src/cli/commands/show.ts +138 -64
- package/src/cli/index.ts +58 -0
- package/tests/abandon.test.ts +178 -111
- package/tests/build-status.test.ts +691 -0
- package/tests/mine.test.ts +130 -163
- package/tests/show-auto-detect.test.ts +20 -2
- package/tests/show.test.ts +226 -8
- package/tests/mocks/fetch-mock.ts +0 -142
- package/tests/setup.ts +0 -13
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach, mock } from 'bun:test'
|
|
2
|
+
import { setupServer } from 'msw/node'
|
|
3
|
+
import { http, HttpResponse } from 'msw'
|
|
4
|
+
import { Effect, Layer } from 'effect'
|
|
5
|
+
import { buildStatusCommand } from '@/cli/commands/build-status'
|
|
6
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import type { MessageInfo } from '@/schemas/gerrit'
|
|
9
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
10
|
+
|
|
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
|
+
// Store captured output
|
|
27
|
+
let capturedStdout: string[] = []
|
|
28
|
+
let capturedErrors: string[] = []
|
|
29
|
+
|
|
30
|
+
// Mock process.stdout.write to capture JSON output
|
|
31
|
+
const mockStdoutWrite = mock((chunk: any) => {
|
|
32
|
+
capturedStdout.push(String(chunk))
|
|
33
|
+
return true
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Mock console.error to capture errors
|
|
37
|
+
const mockConsoleError = mock((...args: any[]) => {
|
|
38
|
+
capturedErrors.push(args.join(' '))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Mock process.exit to prevent test termination
|
|
42
|
+
const mockProcessExit = mock((_code?: number) => {
|
|
43
|
+
throw new Error('Process exited')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Store original methods
|
|
47
|
+
const originalStdoutWrite = process.stdout.write
|
|
48
|
+
const originalConsoleError = console.error
|
|
49
|
+
const originalProcessExit = process.exit
|
|
50
|
+
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
53
|
+
// @ts-ignore - Mocking stdout
|
|
54
|
+
process.stdout.write = mockStdoutWrite
|
|
55
|
+
// @ts-ignore - Mocking console
|
|
56
|
+
console.error = mockConsoleError
|
|
57
|
+
// @ts-ignore - Mocking process.exit
|
|
58
|
+
process.exit = mockProcessExit
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
server.close()
|
|
63
|
+
// @ts-ignore - Restoring stdout
|
|
64
|
+
process.stdout.write = originalStdoutWrite
|
|
65
|
+
console.error = originalConsoleError
|
|
66
|
+
// @ts-ignore - Restoring process.exit
|
|
67
|
+
process.exit = originalProcessExit
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
server.resetHandlers()
|
|
72
|
+
mockStdoutWrite.mockClear()
|
|
73
|
+
mockConsoleError.mockClear()
|
|
74
|
+
mockProcessExit.mockClear()
|
|
75
|
+
capturedStdout = []
|
|
76
|
+
capturedErrors = []
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('build-status command', () => {
|
|
80
|
+
const createMockConfigLayer = () => Layer.succeed(ConfigService, createMockConfigService())
|
|
81
|
+
|
|
82
|
+
test('returns pending when no Build Started message found', async () => {
|
|
83
|
+
const messages: MessageInfo[] = [
|
|
84
|
+
{
|
|
85
|
+
id: 'msg1',
|
|
86
|
+
message: 'Patch Set 1',
|
|
87
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
88
|
+
author: {
|
|
89
|
+
_account_id: 1001,
|
|
90
|
+
name: 'Test User',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'msg2',
|
|
95
|
+
message: 'Review comment',
|
|
96
|
+
date: '2024-01-15 10:30:00.000000000',
|
|
97
|
+
author: {
|
|
98
|
+
_account_id: 1002,
|
|
99
|
+
name: 'Reviewer',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
server.use(
|
|
105
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
106
|
+
const url = new URL(request.url)
|
|
107
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
108
|
+
return HttpResponse.json(
|
|
109
|
+
{ messages },
|
|
110
|
+
{
|
|
111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
120
|
+
Effect.provide(GerritApiServiceLive),
|
|
121
|
+
Effect.provide(createMockConfigLayer()),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
await Effect.runPromise(effect)
|
|
125
|
+
|
|
126
|
+
expect(capturedStdout.length).toBe(1)
|
|
127
|
+
const output = JSON.parse(capturedStdout[0])
|
|
128
|
+
expect(output).toEqual({ state: 'pending' })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('returns running when Build Started but no verification', async () => {
|
|
132
|
+
const messages: MessageInfo[] = [
|
|
133
|
+
{
|
|
134
|
+
id: 'msg1',
|
|
135
|
+
message: 'Patch Set 1',
|
|
136
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
137
|
+
author: {
|
|
138
|
+
_account_id: 1001,
|
|
139
|
+
name: 'Test User',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'msg2',
|
|
144
|
+
message: 'Build Started',
|
|
145
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
146
|
+
author: {
|
|
147
|
+
_account_id: 9999,
|
|
148
|
+
name: 'CI Bot',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'msg3',
|
|
153
|
+
message: 'Some other message',
|
|
154
|
+
date: '2024-01-15 10:10:00.000000000',
|
|
155
|
+
author: {
|
|
156
|
+
_account_id: 1002,
|
|
157
|
+
name: 'Reviewer',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
server.use(
|
|
163
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
164
|
+
const url = new URL(request.url)
|
|
165
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
166
|
+
return HttpResponse.json(
|
|
167
|
+
{ messages },
|
|
168
|
+
{
|
|
169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
178
|
+
Effect.provide(GerritApiServiceLive),
|
|
179
|
+
Effect.provide(createMockConfigLayer()),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
await Effect.runPromise(effect)
|
|
183
|
+
|
|
184
|
+
expect(capturedStdout.length).toBe(1)
|
|
185
|
+
const output = JSON.parse(capturedStdout[0])
|
|
186
|
+
expect(output).toEqual({ state: 'running' })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('returns success when Verified+1 after Build Started', async () => {
|
|
190
|
+
const messages: MessageInfo[] = [
|
|
191
|
+
{
|
|
192
|
+
id: 'msg1',
|
|
193
|
+
message: 'Patch Set 1',
|
|
194
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
195
|
+
author: {
|
|
196
|
+
_account_id: 1001,
|
|
197
|
+
name: 'Test User',
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'msg2',
|
|
202
|
+
message: 'Build Started',
|
|
203
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
204
|
+
author: {
|
|
205
|
+
_account_id: 9999,
|
|
206
|
+
name: 'CI Bot',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'msg3',
|
|
211
|
+
message: 'Patch Set 1: Verified+1',
|
|
212
|
+
date: '2024-01-15 10:15:00.000000000',
|
|
213
|
+
author: {
|
|
214
|
+
_account_id: 9999,
|
|
215
|
+
name: 'CI Bot',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
server.use(
|
|
221
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
222
|
+
const url = new URL(request.url)
|
|
223
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
224
|
+
return HttpResponse.json(
|
|
225
|
+
{ messages },
|
|
226
|
+
{
|
|
227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
232
|
+
}),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
236
|
+
Effect.provide(GerritApiServiceLive),
|
|
237
|
+
Effect.provide(createMockConfigLayer()),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
await Effect.runPromise(effect)
|
|
241
|
+
|
|
242
|
+
expect(capturedStdout.length).toBe(1)
|
|
243
|
+
const output = JSON.parse(capturedStdout[0])
|
|
244
|
+
expect(output).toEqual({ state: 'success' })
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('returns failure when Verified-1 after Build Started', async () => {
|
|
248
|
+
const messages: MessageInfo[] = [
|
|
249
|
+
{
|
|
250
|
+
id: 'msg1',
|
|
251
|
+
message: 'Patch Set 1',
|
|
252
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
253
|
+
author: {
|
|
254
|
+
_account_id: 1001,
|
|
255
|
+
name: 'Test User',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: 'msg2',
|
|
260
|
+
message: 'Build Started',
|
|
261
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
262
|
+
author: {
|
|
263
|
+
_account_id: 9999,
|
|
264
|
+
name: 'CI Bot',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'msg3',
|
|
269
|
+
message: 'Patch Set 1: Verified-1\n\nBuild Failed',
|
|
270
|
+
date: '2024-01-15 10:20:00.000000000',
|
|
271
|
+
author: {
|
|
272
|
+
_account_id: 9999,
|
|
273
|
+
name: 'CI Bot',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
server.use(
|
|
279
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
280
|
+
const url = new URL(request.url)
|
|
281
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
282
|
+
return HttpResponse.json(
|
|
283
|
+
{ messages },
|
|
284
|
+
{
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
290
|
+
}),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
294
|
+
Effect.provide(GerritApiServiceLive),
|
|
295
|
+
Effect.provide(createMockConfigLayer()),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
await Effect.runPromise(effect)
|
|
299
|
+
|
|
300
|
+
expect(capturedStdout.length).toBe(1)
|
|
301
|
+
const output = JSON.parse(capturedStdout[0])
|
|
302
|
+
expect(output).toEqual({ state: 'failure' })
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('ignores Verified messages before Build Started', async () => {
|
|
306
|
+
const messages: MessageInfo[] = [
|
|
307
|
+
{
|
|
308
|
+
id: 'msg1',
|
|
309
|
+
message: 'Patch Set 1: Verified+1',
|
|
310
|
+
date: '2024-01-15 09:00:00.000000000',
|
|
311
|
+
author: {
|
|
312
|
+
_account_id: 9999,
|
|
313
|
+
name: 'CI Bot',
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: 'msg2',
|
|
318
|
+
message: 'Build Started',
|
|
319
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
320
|
+
author: {
|
|
321
|
+
_account_id: 9999,
|
|
322
|
+
name: 'CI Bot',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
server.use(
|
|
328
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
329
|
+
const url = new URL(request.url)
|
|
330
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
331
|
+
return HttpResponse.json(
|
|
332
|
+
{ messages },
|
|
333
|
+
{
|
|
334
|
+
headers: { 'Content-Type': 'application/json' },
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
339
|
+
}),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
343
|
+
Effect.provide(GerritApiServiceLive),
|
|
344
|
+
Effect.provide(createMockConfigLayer()),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
await Effect.runPromise(effect)
|
|
348
|
+
|
|
349
|
+
expect(capturedStdout.length).toBe(1)
|
|
350
|
+
const output = JSON.parse(capturedStdout[0])
|
|
351
|
+
expect(output).toEqual({ state: 'running' })
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('uses most recent Build Started message', async () => {
|
|
355
|
+
const messages: MessageInfo[] = [
|
|
356
|
+
{
|
|
357
|
+
id: 'msg1',
|
|
358
|
+
message: 'Build Started',
|
|
359
|
+
date: '2024-01-15 09:00:00.000000000',
|
|
360
|
+
author: {
|
|
361
|
+
_account_id: 9999,
|
|
362
|
+
name: 'CI Bot',
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: 'msg2',
|
|
367
|
+
message: 'Patch Set 1: Verified-1',
|
|
368
|
+
date: '2024-01-15 09:30:00.000000000',
|
|
369
|
+
author: {
|
|
370
|
+
_account_id: 9999,
|
|
371
|
+
name: 'CI Bot',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'msg3',
|
|
376
|
+
message: 'Build Started',
|
|
377
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
378
|
+
author: {
|
|
379
|
+
_account_id: 9999,
|
|
380
|
+
name: 'CI Bot',
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
server.use(
|
|
386
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
387
|
+
const url = new URL(request.url)
|
|
388
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
389
|
+
return HttpResponse.json(
|
|
390
|
+
{ messages },
|
|
391
|
+
{
|
|
392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
401
|
+
Effect.provide(GerritApiServiceLive),
|
|
402
|
+
Effect.provide(createMockConfigLayer()),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
await Effect.runPromise(effect)
|
|
406
|
+
|
|
407
|
+
expect(capturedStdout.length).toBe(1)
|
|
408
|
+
const output = JSON.parse(capturedStdout[0])
|
|
409
|
+
// Should be running because the most recent Build Started has no verification after it
|
|
410
|
+
expect(output).toEqual({ state: 'running' })
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('returns not_found when change does not exist', async () => {
|
|
414
|
+
server.use(
|
|
415
|
+
http.get('*/a/changes/99999', () => {
|
|
416
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
417
|
+
}),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
const effect = buildStatusCommand('99999').pipe(
|
|
421
|
+
Effect.provide(GerritApiServiceLive),
|
|
422
|
+
Effect.provide(createMockConfigLayer()),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
await Effect.runPromise(effect)
|
|
426
|
+
|
|
427
|
+
expect(capturedStdout.length).toBe(1)
|
|
428
|
+
const output = JSON.parse(capturedStdout[0])
|
|
429
|
+
expect(output).toEqual({ state: 'not_found' })
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test('handles empty message list', async () => {
|
|
433
|
+
server.use(
|
|
434
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
435
|
+
const url = new URL(request.url)
|
|
436
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
437
|
+
return HttpResponse.json(
|
|
438
|
+
{ messages: [] },
|
|
439
|
+
{
|
|
440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
445
|
+
}),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
449
|
+
Effect.provide(GerritApiServiceLive),
|
|
450
|
+
Effect.provide(createMockConfigLayer()),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
await Effect.runPromise(effect)
|
|
454
|
+
|
|
455
|
+
expect(capturedStdout.length).toBe(1)
|
|
456
|
+
const output = JSON.parse(capturedStdout[0])
|
|
457
|
+
// Empty messages means change exists but has no activity - returns pending
|
|
458
|
+
expect(output).toEqual({ state: 'pending' })
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
test('returns first match when both Verified+1 and Verified-1 after Build Started', async () => {
|
|
462
|
+
const messages: MessageInfo[] = [
|
|
463
|
+
{
|
|
464
|
+
id: 'msg1',
|
|
465
|
+
message: 'Build Started',
|
|
466
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
467
|
+
author: {
|
|
468
|
+
_account_id: 9999,
|
|
469
|
+
name: 'CI Bot',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'msg2',
|
|
474
|
+
message: 'Patch Set 1: Verified-1',
|
|
475
|
+
date: '2024-01-15 10:15:00.000000000',
|
|
476
|
+
author: {
|
|
477
|
+
_account_id: 9999,
|
|
478
|
+
name: 'CI Bot',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: 'msg3',
|
|
483
|
+
message: 'Patch Set 2: Verified+1',
|
|
484
|
+
date: '2024-01-15 10:30:00.000000000',
|
|
485
|
+
author: {
|
|
486
|
+
_account_id: 9999,
|
|
487
|
+
name: 'CI Bot',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
server.use(
|
|
493
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
494
|
+
const url = new URL(request.url)
|
|
495
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
496
|
+
return HttpResponse.json(
|
|
497
|
+
{ messages },
|
|
498
|
+
{
|
|
499
|
+
headers: { 'Content-Type': 'application/json' },
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
504
|
+
}),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
508
|
+
Effect.provide(GerritApiServiceLive),
|
|
509
|
+
Effect.provide(createMockConfigLayer()),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
await Effect.runPromise(effect)
|
|
513
|
+
|
|
514
|
+
expect(capturedStdout.length).toBe(1)
|
|
515
|
+
const output = JSON.parse(capturedStdout[0])
|
|
516
|
+
// Should return first verification result (failure)
|
|
517
|
+
expect(output).toEqual({ state: 'failure' })
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
test('does not match malformed verification messages', async () => {
|
|
521
|
+
const messages: MessageInfo[] = [
|
|
522
|
+
{
|
|
523
|
+
id: 'msg1',
|
|
524
|
+
message: 'Build Started',
|
|
525
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
526
|
+
author: {
|
|
527
|
+
_account_id: 9999,
|
|
528
|
+
name: 'CI Bot',
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: 'msg2',
|
|
533
|
+
message: 'Please verify this +1 thanks',
|
|
534
|
+
date: '2024-01-15 10:15:00.000000000',
|
|
535
|
+
author: {
|
|
536
|
+
_account_id: 1001,
|
|
537
|
+
name: 'Reviewer',
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
id: 'msg3',
|
|
542
|
+
message: 'We are not verified -1 yet',
|
|
543
|
+
date: '2024-01-15 10:20:00.000000000',
|
|
544
|
+
author: {
|
|
545
|
+
_account_id: 1002,
|
|
546
|
+
name: 'Reviewer',
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
server.use(
|
|
552
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
553
|
+
const url = new URL(request.url)
|
|
554
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
555
|
+
return HttpResponse.json(
|
|
556
|
+
{ messages },
|
|
557
|
+
{
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
},
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
563
|
+
}),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
567
|
+
Effect.provide(GerritApiServiceLive),
|
|
568
|
+
Effect.provide(createMockConfigLayer()),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
await Effect.runPromise(effect)
|
|
572
|
+
|
|
573
|
+
expect(capturedStdout.length).toBe(1)
|
|
574
|
+
const output = JSON.parse(capturedStdout[0])
|
|
575
|
+
// Malformed messages should not match, so build is still running
|
|
576
|
+
expect(output).toEqual({ state: 'running' })
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
test('handles network error (500)', async () => {
|
|
580
|
+
server.use(
|
|
581
|
+
http.get('*/a/changes/12345', () => {
|
|
582
|
+
return HttpResponse.text('Internal Server Error', { status: 500 })
|
|
583
|
+
}),
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
587
|
+
Effect.provide(GerritApiServiceLive),
|
|
588
|
+
Effect.provide(createMockConfigLayer()),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
await Effect.runPromise(effect)
|
|
593
|
+
} catch (_error) {
|
|
594
|
+
// Should throw error and call process.exit
|
|
595
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1)
|
|
596
|
+
expect(capturedErrors.length).toBeGreaterThan(0)
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
test('handles same timestamp for Build Started and Verified', async () => {
|
|
601
|
+
const sameTimestamp = '2024-01-15 10:00:00.000000000'
|
|
602
|
+
const messages: MessageInfo[] = [
|
|
603
|
+
{
|
|
604
|
+
id: 'msg1',
|
|
605
|
+
message: 'Build Started',
|
|
606
|
+
date: sameTimestamp,
|
|
607
|
+
author: {
|
|
608
|
+
_account_id: 9999,
|
|
609
|
+
name: 'CI Bot',
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
id: 'msg2',
|
|
614
|
+
message: 'Patch Set 1: Verified+1',
|
|
615
|
+
date: sameTimestamp,
|
|
616
|
+
author: {
|
|
617
|
+
_account_id: 9999,
|
|
618
|
+
name: 'CI Bot',
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
]
|
|
622
|
+
|
|
623
|
+
server.use(
|
|
624
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
625
|
+
const url = new URL(request.url)
|
|
626
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
627
|
+
return HttpResponse.json(
|
|
628
|
+
{ messages },
|
|
629
|
+
{
|
|
630
|
+
headers: { 'Content-Type': 'application/json' },
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
635
|
+
}),
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
639
|
+
Effect.provide(GerritApiServiceLive),
|
|
640
|
+
Effect.provide(createMockConfigLayer()),
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
await Effect.runPromise(effect)
|
|
644
|
+
|
|
645
|
+
expect(capturedStdout.length).toBe(1)
|
|
646
|
+
const output = JSON.parse(capturedStdout[0])
|
|
647
|
+
// Same timestamp means Verified is not after Build Started, so running
|
|
648
|
+
expect(output).toEqual({ state: 'running' })
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
test('matches Build Started with different spacing', async () => {
|
|
652
|
+
const messages: MessageInfo[] = [
|
|
653
|
+
{
|
|
654
|
+
id: 'msg1',
|
|
655
|
+
message: 'Build Started', // Extra space
|
|
656
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
657
|
+
author: {
|
|
658
|
+
_account_id: 9999,
|
|
659
|
+
name: 'CI Bot',
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
]
|
|
663
|
+
|
|
664
|
+
server.use(
|
|
665
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
666
|
+
const url = new URL(request.url)
|
|
667
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
668
|
+
return HttpResponse.json(
|
|
669
|
+
{ messages },
|
|
670
|
+
{
|
|
671
|
+
headers: { 'Content-Type': 'application/json' },
|
|
672
|
+
},
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
676
|
+
}),
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
const effect = buildStatusCommand('12345').pipe(
|
|
680
|
+
Effect.provide(GerritApiServiceLive),
|
|
681
|
+
Effect.provide(createMockConfigLayer()),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
await Effect.runPromise(effect)
|
|
685
|
+
|
|
686
|
+
expect(capturedStdout.length).toBe(1)
|
|
687
|
+
const output = JSON.parse(capturedStdout[0])
|
|
688
|
+
// Regex should handle extra whitespace
|
|
689
|
+
expect(output).toEqual({ state: 'running' })
|
|
690
|
+
})
|
|
691
|
+
})
|