@aaronshaf/ger 3.0.2 → 4.0.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.
@@ -0,0 +1,220 @@
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 { listCommand } from '@/cli/commands/list'
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: 'Fix the important thing',
18
+ status: 'NEW',
19
+ created: '2025-01-15 10:00:00.000000000',
20
+ updated: '2025-01-15 12:00:00.000000000',
21
+ owner: { _account_id: 1, name: 'Alice', email: 'alice@x.com' },
22
+ ...overrides,
23
+ })
24
+
25
+ const mockChanges: ChangeInfo[] = [
26
+ makeChange({ _number: 1, subject: 'First change' }),
27
+ makeChange({
28
+ _number: 2,
29
+ subject: 'Second change with Code-Review',
30
+ labels: { 'Code-Review': { approved: { _account_id: 1 }, value: 2 } },
31
+ }),
32
+ makeChange({
33
+ _number: 3,
34
+ subject: 'Third change rejected',
35
+ labels: { 'Code-Review': { rejected: { _account_id: 2 }, value: -2 } },
36
+ }),
37
+ ]
38
+
39
+ const server = setupServer(
40
+ http.get('*/a/accounts/self', () =>
41
+ HttpResponse.json({ _account_id: 1, name: 'Alice', email: 'alice@x.com' }),
42
+ ),
43
+ http.get('*/a/changes/', () => HttpResponse.json(mockChanges)),
44
+ )
45
+
46
+ beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
47
+ afterAll(() => server.close())
48
+ afterEach(() => server.resetHandlers())
49
+
50
+ const mockConfig = createMockConfigService()
51
+
52
+ describe('list command', () => {
53
+ test('renders table with header and rows', async () => {
54
+ const logs: string[] = []
55
+ const origLog = console.log
56
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
57
+
58
+ try {
59
+ await Effect.runPromise(
60
+ listCommand({}).pipe(
61
+ Effect.provide(GerritApiServiceLive),
62
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
63
+ ),
64
+ )
65
+ } finally {
66
+ console.log = origLog
67
+ }
68
+
69
+ const output = logs.join('\n')
70
+ expect(output).toContain('Change')
71
+ expect(output).toContain('Subject')
72
+ expect(output).toContain('CR')
73
+ expect(output).toContain('Verified')
74
+ expect(output).toContain('1')
75
+ expect(output).toContain('First change')
76
+ })
77
+
78
+ test('outputs JSON', async () => {
79
+ const logs: string[] = []
80
+ const origLog = console.log
81
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
82
+
83
+ try {
84
+ await Effect.runPromise(
85
+ listCommand({ json: true }).pipe(
86
+ Effect.provide(GerritApiServiceLive),
87
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
88
+ ),
89
+ )
90
+ } finally {
91
+ console.log = origLog
92
+ }
93
+
94
+ const parsed = JSON.parse(logs.join('')) as { status: string; count: number }
95
+ expect(parsed.status).toBe('success')
96
+ expect(parsed.count).toBe(3)
97
+ })
98
+
99
+ test('outputs XML', async () => {
100
+ const logs: string[] = []
101
+ const origLog = console.log
102
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
103
+
104
+ try {
105
+ await Effect.runPromise(
106
+ listCommand({ xml: true }).pipe(
107
+ Effect.provide(GerritApiServiceLive),
108
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
109
+ ),
110
+ )
111
+ } finally {
112
+ console.log = origLog
113
+ }
114
+
115
+ const output = logs.join('\n')
116
+ expect(output).toContain('<changes count="3">')
117
+ expect(output).toContain('<number>1</number>')
118
+ })
119
+
120
+ test('--detailed shows per-change info', async () => {
121
+ const logs: string[] = []
122
+ const origLog = console.log
123
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
124
+
125
+ try {
126
+ await Effect.runPromise(
127
+ listCommand({ detailed: true }).pipe(
128
+ Effect.provide(GerritApiServiceLive),
129
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
130
+ ),
131
+ )
132
+ } finally {
133
+ console.log = origLog
134
+ }
135
+
136
+ const output = logs.join('\n')
137
+ expect(output).toContain('Change:')
138
+ expect(output).toContain('Subject:')
139
+ expect(output).toContain('Project:')
140
+ })
141
+
142
+ test('--limit caps results', async () => {
143
+ const logs: string[] = []
144
+ const origLog = console.log
145
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
146
+
147
+ try {
148
+ await Effect.runPromise(
149
+ listCommand({ limit: 1, json: true }).pipe(
150
+ Effect.provide(GerritApiServiceLive),
151
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
152
+ ),
153
+ )
154
+ } finally {
155
+ console.log = origLog
156
+ }
157
+
158
+ const parsed = JSON.parse(logs.join('')) as { count: number }
159
+ expect(parsed.count).toBe(1)
160
+ })
161
+
162
+ test('--status passes query to API', async () => {
163
+ let capturedUrl = ''
164
+ server.use(
165
+ http.get('*/a/changes/', ({ request }) => {
166
+ capturedUrl = decodeURIComponent(request.url)
167
+ return HttpResponse.json([])
168
+ }),
169
+ )
170
+
171
+ await Effect.runPromise(
172
+ listCommand({ status: 'merged' }).pipe(
173
+ Effect.provide(GerritApiServiceLive),
174
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
175
+ ),
176
+ )
177
+
178
+ expect(capturedUrl).toContain('status:merged')
179
+ })
180
+
181
+ test('--reviewer uses reviewer query', async () => {
182
+ let capturedUrl = ''
183
+ server.use(
184
+ http.get('*/a/changes/', ({ request }) => {
185
+ capturedUrl = decodeURIComponent(request.url)
186
+ return HttpResponse.json([])
187
+ }),
188
+ )
189
+
190
+ await Effect.runPromise(
191
+ listCommand({ reviewer: true }).pipe(
192
+ Effect.provide(GerritApiServiceLive),
193
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
194
+ ),
195
+ )
196
+
197
+ expect(capturedUrl).toContain('reviewer:')
198
+ })
199
+
200
+ test('shows empty message when no changes', async () => {
201
+ server.use(http.get('*/a/changes/', () => HttpResponse.json([])))
202
+
203
+ const logs: string[] = []
204
+ const origLog = console.log
205
+ console.log = (...args: unknown[]) => logs.push(String(args[0]))
206
+
207
+ try {
208
+ await Effect.runPromise(
209
+ listCommand({}).pipe(
210
+ Effect.provide(GerritApiServiceLive),
211
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
212
+ ),
213
+ )
214
+ } finally {
215
+ console.log = origLog
216
+ }
217
+
218
+ expect(logs.join('\n')).toContain('No changes found')
219
+ })
220
+ })
@@ -0,0 +1,159 @@
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 { ConfigService } from '@/services/config'
7
+ import { retriggerCommand } from '@/cli/commands/retrigger'
8
+ import { createMockConfigService } from './helpers/config-mock'
9
+
10
+ // Mock @inquirer/prompts so tests don't block on stdin
11
+ const mockInput = mock(async () => 'trigger-build')
12
+ mock.module('@inquirer/prompts', () => ({ input: mockInput }))
13
+
14
+ const server = setupServer(
15
+ http.get('*/a/accounts/self', () =>
16
+ HttpResponse.json({ _account_id: 1, name: 'User', email: 'u@example.com' }),
17
+ ),
18
+ http.post('*/a/changes/:changeId/revisions/current/review', () => HttpResponse.json({})),
19
+ )
20
+
21
+ beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
22
+ afterAll(() => server.close())
23
+
24
+ describe('retrigger command', () => {
25
+ let mockConsoleLog: ReturnType<typeof mock>
26
+ let mockConsoleError: ReturnType<typeof mock>
27
+
28
+ beforeEach(() => {
29
+ mockConsoleLog = mock()
30
+ mockConsoleError = mock()
31
+ console.log = mockConsoleLog
32
+ console.error = mockConsoleError
33
+ mockInput.mockReset()
34
+ mockInput.mockResolvedValue('trigger-build')
35
+ server.resetHandlers()
36
+ })
37
+
38
+ afterEach(() => {
39
+ server.resetHandlers()
40
+ })
41
+
42
+ it('posts the retrigger comment when change-id is explicit and comment is configured', async () => {
43
+ const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
44
+
45
+ await Effect.runPromise(
46
+ retriggerCommand('12345', {}).pipe(
47
+ Effect.provide(GerritApiServiceLive),
48
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
49
+ ),
50
+ )
51
+
52
+ // Should print success
53
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('✓'))
54
+ })
55
+
56
+ it('posts to the given change-id', async () => {
57
+ const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
58
+
59
+ let postedChangeId = ''
60
+ server.use(
61
+ http.post('*/a/changes/:changeId/revisions/current/review', ({ params }) => {
62
+ postedChangeId = params.changeId as string
63
+ return HttpResponse.json({})
64
+ }),
65
+ )
66
+
67
+ await Effect.runPromise(
68
+ retriggerCommand('67890', {}).pipe(
69
+ Effect.provide(GerritApiServiceLive),
70
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
71
+ ),
72
+ )
73
+
74
+ expect(postedChangeId).toBe('67890')
75
+ })
76
+
77
+ it('prompts for retrigger comment when not configured, then saves it', async () => {
78
+ let savedComment = ''
79
+ const mockConfig: ReturnType<typeof createMockConfigService> = {
80
+ ...createMockConfigService(),
81
+ getRetriggerComment: Effect.succeed(undefined),
82
+ saveRetriggerComment: (comment: string) => {
83
+ savedComment = comment
84
+ return Effect.succeed(undefined as void)
85
+ },
86
+ }
87
+
88
+ mockInput.mockResolvedValue('my-trigger-comment')
89
+
90
+ await Effect.runPromise(
91
+ retriggerCommand('12345', {}).pipe(
92
+ Effect.provide(GerritApiServiceLive),
93
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
94
+ ),
95
+ )
96
+
97
+ expect(mockInput).toHaveBeenCalled()
98
+ expect(savedComment).toBe('my-trigger-comment')
99
+ })
100
+
101
+ it('throws when prompted comment is empty', async () => {
102
+ const mockConfig: ReturnType<typeof createMockConfigService> = {
103
+ ...createMockConfigService(),
104
+ getRetriggerComment: Effect.succeed(undefined),
105
+ saveRetriggerComment: () => Effect.succeed(undefined as void),
106
+ }
107
+
108
+ mockInput.mockResolvedValue(' ')
109
+
110
+ let threw = false
111
+ try {
112
+ await Effect.runPromise(
113
+ retriggerCommand('12345', {}).pipe(
114
+ Effect.provide(GerritApiServiceLive),
115
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
116
+ ),
117
+ )
118
+ } catch (e) {
119
+ threw = true
120
+ expect(String(e)).toContain('cannot be empty')
121
+ }
122
+ expect(threw).toBe(true)
123
+ })
124
+
125
+ it('outputs JSON on success', async () => {
126
+ const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
127
+
128
+ const logs: string[] = []
129
+ console.log = (msg: string) => logs.push(msg)
130
+
131
+ await Effect.runPromise(
132
+ retriggerCommand('12345', { json: true }).pipe(
133
+ Effect.provide(GerritApiServiceLive),
134
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
135
+ ),
136
+ )
137
+
138
+ const parsed = JSON.parse(logs[0]) as { status: string; change_id: string }
139
+ expect(parsed.status).toBe('success')
140
+ expect(parsed.change_id).toBe('12345')
141
+ })
142
+
143
+ it('outputs XML on success', async () => {
144
+ const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
145
+
146
+ const logs: string[] = []
147
+ console.log = (msg: string) => logs.push(msg)
148
+
149
+ await Effect.runPromise(
150
+ retriggerCommand('12345', { xml: true }).pipe(
151
+ Effect.provide(GerritApiServiceLive),
152
+ Effect.provide(Layer.succeed(ConfigService, mockConfig)),
153
+ ),
154
+ )
155
+
156
+ expect(logs.join('\n')).toContain('<retrigger>')
157
+ expect(logs.join('\n')).toContain('<status>success</status>')
158
+ })
159
+ })