@aaronshaf/ger 0.2.4 → 0.3.1
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 +87 -0
- package/package.json +1 -1
- package/src/cli/commands/build-status.ts +238 -0
- package/src/cli/index.ts +80 -0
- package/tests/abandon.test.ts +178 -111
- package/tests/build-status-watch.test.ts +347 -0
- package/tests/build-status.test.ts +640 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/mine.test.ts +130 -163
- package/tests/mocks/fetch-mock.ts +0 -142
- package/tests/setup.ts +0 -13
package/tests/abandon.test.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
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 { abandonCommand } from '@/cli/commands/abandon'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
2
9
|
import type { ChangeInfo } from '@/schemas/gerrit'
|
|
3
10
|
|
|
4
11
|
const mockChange: ChangeInfo = {
|
|
@@ -28,136 +35,196 @@ const mockChange: ChangeInfo = {
|
|
|
28
35
|
submittable: false,
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
// Create MSW server
|
|
39
|
+
const server = setupServer(
|
|
40
|
+
// Default handler for auth check
|
|
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
|
+
|
|
31
54
|
describe('abandon command', () => {
|
|
32
|
-
let
|
|
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
|
+
})
|
|
33
65
|
|
|
34
66
|
beforeEach(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 abandon a change with a message', async () => {
|
|
78
|
+
server.use(
|
|
79
|
+
http.get('*/a/changes/12345', () => {
|
|
80
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
81
|
+
}),
|
|
82
|
+
http.post('*/a/changes/12345/abandon', async ({ request }) => {
|
|
83
|
+
const body = (await request.json()) as { message?: string }
|
|
84
|
+
expect(body.message).toBe('No longer needed')
|
|
85
|
+
return HttpResponse.text(")]}'\n{}")
|
|
41
86
|
}),
|
|
42
87
|
)
|
|
43
|
-
|
|
88
|
+
|
|
89
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
90
|
+
const program = abandonCommand('12345', {
|
|
91
|
+
message: 'No longer needed',
|
|
92
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
93
|
+
|
|
94
|
+
await Effect.runPromise(program)
|
|
95
|
+
|
|
96
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
97
|
+
expect(output).toContain('Abandoned change 12345')
|
|
98
|
+
expect(output).toContain('Test change to abandon')
|
|
99
|
+
expect(output).toContain('Message: No longer needed')
|
|
44
100
|
})
|
|
45
101
|
|
|
46
|
-
it('should
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"subject": "Test change to abandon",
|
|
58
|
-
"status": "NEW",
|
|
59
|
-
"_number": 12345
|
|
60
|
-
}`,
|
|
61
|
-
})
|
|
62
|
-
.mockResolvedValueOnce({
|
|
63
|
-
ok: true,
|
|
64
|
-
text: async () => ')]}\n{}',
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// Note: This is a unit test demonstrating the API calls
|
|
68
|
-
// Actual integration would require running the full command
|
|
69
|
-
// which we avoid to prevent hitting production
|
|
70
|
-
|
|
71
|
-
// Verify the mock setup
|
|
72
|
-
const response = await mockFetch('https://test.gerrit.com/a/changes/12345')
|
|
73
|
-
const text = await response.text()
|
|
74
|
-
expect(text).toContain('Test change to abandon')
|
|
75
|
-
|
|
76
|
-
// Verify abandon endpoint would be called
|
|
77
|
-
const abandonResponse = await mockFetch('https://test.gerrit.com/a/changes/12345/abandon', {
|
|
78
|
-
method: 'POST',
|
|
79
|
-
body: JSON.stringify({ message: 'No longer needed' }),
|
|
80
|
-
})
|
|
81
|
-
expect(abandonResponse.ok).toBe(true)
|
|
102
|
+
it('should abandon a change without a message', async () => {
|
|
103
|
+
server.use(
|
|
104
|
+
http.get('*/a/changes/12345', () => {
|
|
105
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
106
|
+
}),
|
|
107
|
+
http.post('*/a/changes/12345/abandon', async ({ request }) => {
|
|
108
|
+
const body = (await request.json()) as { message?: string }
|
|
109
|
+
expect(body.message).toBeUndefined()
|
|
110
|
+
return HttpResponse.text(")]}'\n{}")
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
82
113
|
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
115
|
+
const program = abandonCommand('12345', {}).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('Abandoned change 12345')
|
|
124
|
+
expect(output).toContain('Test change to abandon')
|
|
125
|
+
expect(output).not.toContain('Message:')
|
|
85
126
|
})
|
|
86
127
|
|
|
87
|
-
it('should
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
it('should output XML format when --xml flag is used', async () => {
|
|
129
|
+
server.use(
|
|
130
|
+
http.get('*/a/changes/12345', () => {
|
|
131
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
132
|
+
}),
|
|
133
|
+
http.post('*/a/changes/12345/abandon', async ({ request }) => {
|
|
134
|
+
const body = (await request.json()) as { message?: string }
|
|
135
|
+
expect(body.message).toBe('Abandoning for testing')
|
|
136
|
+
return HttpResponse.text(")]}'\n{}")
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
92
139
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
140
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
141
|
+
const program = abandonCommand('12345', {
|
|
142
|
+
xml: true,
|
|
143
|
+
message: 'Abandoning for testing',
|
|
144
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
145
|
+
|
|
146
|
+
await Effect.runPromise(program)
|
|
147
|
+
|
|
148
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
149
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
150
|
+
expect(output).toContain('<abandon_result>')
|
|
151
|
+
expect(output).toContain('<status>success</status>')
|
|
152
|
+
expect(output).toContain('<change_number>12345</change_number>')
|
|
153
|
+
expect(output).toContain('<subject><![CDATA[Test change to abandon]]></subject>')
|
|
154
|
+
expect(output).toContain('<message><![CDATA[Abandoning for testing]]></message>')
|
|
155
|
+
expect(output).toContain('</abandon_result>')
|
|
156
|
+
})
|
|
97
157
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
it('should output XML format without message when no message provided', async () => {
|
|
159
|
+
server.use(
|
|
160
|
+
http.get('*/a/changes/12345', () => {
|
|
161
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
162
|
+
}),
|
|
163
|
+
http.post('*/a/changes/12345/abandon', async () => {
|
|
164
|
+
return HttpResponse.text(")]}'\n{}")
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
169
|
+
const program = abandonCommand('12345', { xml: true }).pipe(
|
|
170
|
+
Effect.provide(GerritApiServiceLive),
|
|
171
|
+
Effect.provide(mockConfigLayer),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await Effect.runPromise(program)
|
|
175
|
+
|
|
176
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
177
|
+
expect(output).toContain('<abandon_result>')
|
|
178
|
+
expect(output).toContain('<status>success</status>')
|
|
179
|
+
expect(output).not.toContain('<message>')
|
|
103
180
|
})
|
|
104
181
|
|
|
105
|
-
it('should handle
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
182
|
+
it('should handle not found errors gracefully', async () => {
|
|
183
|
+
server.use(
|
|
184
|
+
http.get('*/a/changes/99999', () => {
|
|
185
|
+
return HttpResponse.text('Change not found', { status: 404 })
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
111
188
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
189
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
190
|
+
const program = abandonCommand('99999', {
|
|
191
|
+
message: 'Test message',
|
|
192
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
115
193
|
|
|
116
|
-
|
|
117
|
-
expect(
|
|
194
|
+
// Should fail when change is not found
|
|
195
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
118
196
|
})
|
|
119
197
|
|
|
120
|
-
it('should
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
198
|
+
it('should show error when change ID is not provided', async () => {
|
|
199
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
200
|
+
const program = abandonCommand(undefined, {}).pipe(
|
|
201
|
+
Effect.provide(GerritApiServiceLive),
|
|
202
|
+
Effect.provide(mockConfigLayer),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
await Effect.runPromise(program)
|
|
206
|
+
|
|
207
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
208
|
+
expect(errorOutput).toContain('Change ID is required')
|
|
209
|
+
expect(errorOutput).toContain('Usage: ger abandon <change-id>')
|
|
132
210
|
})
|
|
133
211
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
'https://test.gerrit.com/a/changes/?q=owner:self+status:open',
|
|
144
|
-
)
|
|
145
|
-
const text = await response.text()
|
|
146
|
-
expect(text).toContain('Test change to abandon')
|
|
147
|
-
})
|
|
212
|
+
it('should handle abandon API failure', async () => {
|
|
213
|
+
server.use(
|
|
214
|
+
http.get('*/a/changes/12345', () => {
|
|
215
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
|
|
216
|
+
}),
|
|
217
|
+
http.post('*/a/changes/12345/abandon', () => {
|
|
218
|
+
return HttpResponse.text('Forbidden', { status: 403 })
|
|
219
|
+
}),
|
|
220
|
+
)
|
|
148
221
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'https://test.gerrit.com/a/changes/?q=owner:self+status:open',
|
|
157
|
-
)
|
|
158
|
-
const text = await response.text()
|
|
159
|
-
const parsed = JSON.parse(text.replace(")]}'\n", ''))
|
|
160
|
-
expect(parsed).toEqual([])
|
|
161
|
-
})
|
|
222
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
223
|
+
const program = abandonCommand('12345', {
|
|
224
|
+
message: 'Test',
|
|
225
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
|
|
226
|
+
|
|
227
|
+
// Should throw/fail
|
|
228
|
+
await expect(Effect.runPromise(program)).rejects.toThrow()
|
|
162
229
|
})
|
|
163
230
|
})
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test'
|
|
2
|
+
import { http, HttpResponse } from 'msw'
|
|
3
|
+
import { Effect } from 'effect'
|
|
4
|
+
import { buildStatusCommand } from '@/cli/commands/build-status'
|
|
5
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
6
|
+
import type { MessageInfo } from '@/schemas/gerrit'
|
|
7
|
+
import {
|
|
8
|
+
server,
|
|
9
|
+
capturedStdout,
|
|
10
|
+
capturedErrors,
|
|
11
|
+
mockProcessExit,
|
|
12
|
+
setupBuildStatusTests,
|
|
13
|
+
teardownBuildStatusTests,
|
|
14
|
+
resetBuildStatusMocks,
|
|
15
|
+
createMockConfigLayer,
|
|
16
|
+
} from './helpers/build-status-test-setup'
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
setupBuildStatusTests()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
teardownBuildStatusTests()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
resetBuildStatusMocks()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('build-status command - watch mode', () => {
|
|
31
|
+
test('polls until success state is reached', async () => {
|
|
32
|
+
let callCount = 0
|
|
33
|
+
|
|
34
|
+
server.use(
|
|
35
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
36
|
+
const url = new URL(request.url)
|
|
37
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
38
|
+
callCount++
|
|
39
|
+
|
|
40
|
+
let messages: MessageInfo[]
|
|
41
|
+
if (callCount === 1) {
|
|
42
|
+
// First call: pending (no build started)
|
|
43
|
+
messages = []
|
|
44
|
+
} else if (callCount === 2) {
|
|
45
|
+
// Second call: running (build started, no verification)
|
|
46
|
+
messages = [
|
|
47
|
+
{
|
|
48
|
+
id: 'msg1',
|
|
49
|
+
message: 'Build Started',
|
|
50
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
51
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
} else {
|
|
55
|
+
// Third call: success (verified +1)
|
|
56
|
+
messages = [
|
|
57
|
+
{
|
|
58
|
+
id: 'msg1',
|
|
59
|
+
message: 'Build Started',
|
|
60
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
61
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'msg2',
|
|
65
|
+
message: 'Patch Set 1: Verified+1',
|
|
66
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
67
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return HttpResponse.json(
|
|
73
|
+
{ messages },
|
|
74
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const effect = buildStatusCommand('12345', {
|
|
82
|
+
watch: true,
|
|
83
|
+
interval: 0.1, // Fast polling for tests
|
|
84
|
+
timeout: 10,
|
|
85
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
86
|
+
|
|
87
|
+
await Effect.runPromise(effect)
|
|
88
|
+
|
|
89
|
+
// Should have multiple outputs (one per poll)
|
|
90
|
+
expect(capturedStdout.length).toBeGreaterThanOrEqual(3)
|
|
91
|
+
expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'pending' })
|
|
92
|
+
expect(JSON.parse(capturedStdout[1])).toEqual({ state: 'running' })
|
|
93
|
+
expect(JSON.parse(capturedStdout[2])).toEqual({ state: 'success' })
|
|
94
|
+
|
|
95
|
+
// Should have logged progress to stderr
|
|
96
|
+
expect(capturedErrors.length).toBeGreaterThan(0)
|
|
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)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('polls until failure state is reached', async () => {
|
|
104
|
+
let callCount = 0
|
|
105
|
+
|
|
106
|
+
server.use(
|
|
107
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
108
|
+
const url = new URL(request.url)
|
|
109
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
110
|
+
callCount++
|
|
111
|
+
|
|
112
|
+
let messages: MessageInfo[]
|
|
113
|
+
if (callCount === 1) {
|
|
114
|
+
messages = [
|
|
115
|
+
{
|
|
116
|
+
id: 'msg1',
|
|
117
|
+
message: 'Build Started',
|
|
118
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
119
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
} else {
|
|
123
|
+
messages = [
|
|
124
|
+
{
|
|
125
|
+
id: 'msg1',
|
|
126
|
+
message: 'Build Started',
|
|
127
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
128
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'msg2',
|
|
132
|
+
message: 'Patch Set 1: Verified-1',
|
|
133
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
134
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
135
|
+
},
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return HttpResponse.json(
|
|
140
|
+
{ messages },
|
|
141
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const effect = buildStatusCommand('12345', {
|
|
149
|
+
watch: true,
|
|
150
|
+
interval: 0.1,
|
|
151
|
+
timeout: 10,
|
|
152
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
153
|
+
|
|
154
|
+
await Effect.runPromise(effect)
|
|
155
|
+
|
|
156
|
+
expect(capturedStdout.length).toBeGreaterThanOrEqual(2)
|
|
157
|
+
expect(JSON.parse(capturedStdout[capturedStdout.length - 1])).toEqual({ state: 'failure' })
|
|
158
|
+
expect(
|
|
159
|
+
capturedErrors.some((e: string) => e.includes('Build completed with status: failure')),
|
|
160
|
+
).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('times out after specified duration', async () => {
|
|
164
|
+
server.use(
|
|
165
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
166
|
+
const url = new URL(request.url)
|
|
167
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
168
|
+
// Always return running state
|
|
169
|
+
return HttpResponse.json(
|
|
170
|
+
{
|
|
171
|
+
messages: [
|
|
172
|
+
{
|
|
173
|
+
id: 'msg1',
|
|
174
|
+
message: 'Build Started',
|
|
175
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
176
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const effect = buildStatusCommand('12345', {
|
|
188
|
+
watch: true,
|
|
189
|
+
interval: 0.1,
|
|
190
|
+
timeout: 0.5, // Very short timeout
|
|
191
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await Effect.runPromise(effect)
|
|
195
|
+
} catch {
|
|
196
|
+
// Should exit with code 2 for timeout
|
|
197
|
+
expect(mockProcessExit).toHaveBeenCalledWith(2)
|
|
198
|
+
expect(capturedErrors.some((e: string) => e.includes('Timeout'))).toBe(true)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('exit-status flag causes exit 1 on failure', async () => {
|
|
203
|
+
server.use(
|
|
204
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
205
|
+
const url = new URL(request.url)
|
|
206
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
207
|
+
return HttpResponse.json(
|
|
208
|
+
{
|
|
209
|
+
messages: [
|
|
210
|
+
{
|
|
211
|
+
id: 'msg1',
|
|
212
|
+
message: 'Build Started',
|
|
213
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
214
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'msg2',
|
|
218
|
+
message: 'Patch Set 1: Verified-1',
|
|
219
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
220
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const effect = buildStatusCommand('12345', {
|
|
232
|
+
watch: true,
|
|
233
|
+
interval: 0.1,
|
|
234
|
+
timeout: 10,
|
|
235
|
+
exitStatus: true,
|
|
236
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await Effect.runPromise(effect)
|
|
240
|
+
} catch {
|
|
241
|
+
// Should exit with code 1 for failure when --exit-status is used
|
|
242
|
+
expect(mockProcessExit).toHaveBeenCalledWith(1)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('exit-status flag does not affect success state', async () => {
|
|
247
|
+
server.use(
|
|
248
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
249
|
+
const url = new URL(request.url)
|
|
250
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
251
|
+
return HttpResponse.json(
|
|
252
|
+
{
|
|
253
|
+
messages: [
|
|
254
|
+
{
|
|
255
|
+
id: 'msg1',
|
|
256
|
+
message: 'Build Started',
|
|
257
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
258
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'msg2',
|
|
262
|
+
message: 'Patch Set 1: Verified+1',
|
|
263
|
+
date: '2024-01-15 10:05:00.000000000',
|
|
264
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
272
|
+
}),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const effect = buildStatusCommand('12345', {
|
|
276
|
+
watch: true,
|
|
277
|
+
interval: 0.1,
|
|
278
|
+
timeout: 10,
|
|
279
|
+
exitStatus: true,
|
|
280
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
281
|
+
|
|
282
|
+
await Effect.runPromise(effect)
|
|
283
|
+
|
|
284
|
+
// Should not call process.exit for success state
|
|
285
|
+
expect(mockProcessExit).not.toHaveBeenCalled()
|
|
286
|
+
expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'success' })
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('watch mode handles not_found state', async () => {
|
|
290
|
+
server.use(
|
|
291
|
+
http.get('*/a/changes/99999', () => {
|
|
292
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const effect = buildStatusCommand('99999', {
|
|
297
|
+
watch: true,
|
|
298
|
+
interval: 0.1,
|
|
299
|
+
timeout: 10,
|
|
300
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
301
|
+
|
|
302
|
+
await Effect.runPromise(effect)
|
|
303
|
+
|
|
304
|
+
expect(capturedStdout.length).toBe(1)
|
|
305
|
+
expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'not_found' })
|
|
306
|
+
// 404 errors bypass pollBuildStatus and are handled in error handler
|
|
307
|
+
// So we get "Watching build status" but not "Build completed" message
|
|
308
|
+
expect(capturedErrors.some((e: string) => e.includes('Watching build status'))).toBe(true)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('without watch flag, behaves as single check', async () => {
|
|
312
|
+
server.use(
|
|
313
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
314
|
+
const url = new URL(request.url)
|
|
315
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
316
|
+
return HttpResponse.json(
|
|
317
|
+
{
|
|
318
|
+
messages: [
|
|
319
|
+
{
|
|
320
|
+
id: 'msg1',
|
|
321
|
+
message: 'Build Started',
|
|
322
|
+
date: '2024-01-15 10:00:00.000000000',
|
|
323
|
+
author: { _account_id: 9999, name: 'CI Bot' },
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
return HttpResponse.text('Not Found', { status: 404 })
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const effect = buildStatusCommand('12345', {
|
|
335
|
+
watch: false,
|
|
336
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
337
|
+
|
|
338
|
+
await Effect.runPromise(effect)
|
|
339
|
+
|
|
340
|
+
// Should only have one output (no polling)
|
|
341
|
+
expect(capturedStdout.length).toBe(1)
|
|
342
|
+
expect(JSON.parse(capturedStdout[0])).toEqual({ state: 'running' })
|
|
343
|
+
|
|
344
|
+
// Should not have watch mode messages in stderr
|
|
345
|
+
expect(capturedErrors.some((e: string) => e.includes('Watching build status'))).toBe(false)
|
|
346
|
+
})
|
|
347
|
+
})
|