@aaronshaf/ger 0.1.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.
- package/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.eslintrc.js +12 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +78 -0
- package/.github/workflows/claude.yml +64 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +103 -0
- package/DEVELOPMENT.md +361 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/bin/ger +3 -0
- package/biome.json +36 -0
- package/bun.lock +688 -0
- package/bunfig.toml +8 -0
- package/oxlint.json +24 -0
- package/package.json +55 -0
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/src/api/gerrit.ts +466 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/review.ts +593 -0
- package/src/cli/commands/setup.ts +230 -0
- package/src/cli/commands/show.ts +303 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +420 -0
- package/src/prompts/default-review.md +80 -0
- package/src/prompts/system-inline-review.md +88 -0
- package/src/prompts/system-overall-review.md +152 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +75 -0
- package/src/schemas/gerrit.ts +455 -0
- package/src/services/ai-enhanced.ts +167 -0
- package/src/services/ai.ts +182 -0
- package/src/services/config.test.ts +414 -0
- package/src/services/config.ts +206 -0
- package/src/test-utils/mock-generator.ts +73 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +123 -0
- package/src/utils/url-parser.ts +91 -0
- package/tests/abandon.test.ts +163 -0
- package/tests/ai-service.test.ts +489 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +707 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +318 -0
- package/tests/mocks/fetch-mock.ts +139 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/review.test.ts +669 -0
- package/tests/setup.ts +13 -0
- package/tests/show.test.ts +439 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/prompt-helpers.test.ts +175 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test'
|
|
2
|
+
import { Effect, Layer } from 'effect'
|
|
3
|
+
import { mineCommand } from '@/cli/commands/mine'
|
|
4
|
+
import { GerritApiService, type ApiError } from '@/api/gerrit'
|
|
5
|
+
import { generateMockChange } from '@/test-utils/mock-generator'
|
|
6
|
+
import type { ChangeInfo } from '@/schemas/gerrit'
|
|
7
|
+
|
|
8
|
+
// Mock console.log to capture output
|
|
9
|
+
const mockConsole = {
|
|
10
|
+
logs: [] as string[],
|
|
11
|
+
log: function (message: string) {
|
|
12
|
+
this.logs.push(message)
|
|
13
|
+
},
|
|
14
|
+
clear: function () {
|
|
15
|
+
this.logs = []
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('mine command', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockConsole.clear()
|
|
22
|
+
// Replace console.log for tests
|
|
23
|
+
global.console.log = mockConsole.log.bind(mockConsole)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('should fetch and display my changes in pretty format', async () => {
|
|
27
|
+
const mockChanges: ChangeInfo[] = [
|
|
28
|
+
generateMockChange({
|
|
29
|
+
_number: 12345,
|
|
30
|
+
subject: 'My test change',
|
|
31
|
+
project: 'test-project',
|
|
32
|
+
branch: 'main',
|
|
33
|
+
status: 'NEW',
|
|
34
|
+
}),
|
|
35
|
+
generateMockChange({
|
|
36
|
+
_number: 12346,
|
|
37
|
+
subject: 'Another change',
|
|
38
|
+
project: 'test-project-2',
|
|
39
|
+
branch: 'develop',
|
|
40
|
+
status: 'MERGED',
|
|
41
|
+
}),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const mockApi = GerritApiService.of({
|
|
45
|
+
listChanges: (query?: string) => {
|
|
46
|
+
expect(query).toBe('owner:self status:open')
|
|
47
|
+
return Effect.succeed(mockChanges)
|
|
48
|
+
},
|
|
49
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
50
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
51
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
52
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
53
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
54
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
55
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
56
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
57
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
58
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
59
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
60
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
await Effect.runPromise(
|
|
64
|
+
mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(mockConsole.logs.length).toBeGreaterThan(0)
|
|
68
|
+
expect(mockConsole.logs.some((log) => log.includes('My test change'))).toBe(true)
|
|
69
|
+
expect(mockConsole.logs.some((log) => log.includes('Another change'))).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('should output XML format when --xml flag is used', async () => {
|
|
73
|
+
const mockChanges: ChangeInfo[] = [
|
|
74
|
+
generateMockChange({
|
|
75
|
+
_number: 12345,
|
|
76
|
+
subject: 'Test change',
|
|
77
|
+
project: 'test-project',
|
|
78
|
+
branch: 'main',
|
|
79
|
+
status: 'NEW',
|
|
80
|
+
}),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const mockApi = GerritApiService.of({
|
|
84
|
+
listChanges: (query?: string) => {
|
|
85
|
+
expect(query).toBe('owner:self status:open')
|
|
86
|
+
return Effect.succeed(mockChanges)
|
|
87
|
+
},
|
|
88
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
89
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
90
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
91
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
92
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
93
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
94
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
95
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
96
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
97
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
98
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
99
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
await Effect.runPromise(
|
|
103
|
+
mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const output = mockConsole.logs.join('\n')
|
|
107
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
108
|
+
expect(output).toContain('<changes count="1">')
|
|
109
|
+
expect(output).toContain('<change>')
|
|
110
|
+
expect(output).toContain('<number>12345</number>')
|
|
111
|
+
expect(output).toContain('<subject><![CDATA[Test change]]></subject>')
|
|
112
|
+
expect(output).toContain('<project>test-project</project>')
|
|
113
|
+
expect(output).toContain('<branch>main</branch>')
|
|
114
|
+
expect(output).toContain('<status>NEW</status>')
|
|
115
|
+
expect(output).toContain('</change>')
|
|
116
|
+
expect(output).toContain('</changes>')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('should handle no changes gracefully', async () => {
|
|
120
|
+
const mockApi = GerritApiService.of({
|
|
121
|
+
listChanges: () => Effect.succeed([]),
|
|
122
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
123
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
124
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
125
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
126
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
127
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
128
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
129
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
130
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
131
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
132
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
133
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await Effect.runPromise(
|
|
137
|
+
mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Mine command returns early for empty results, so no output is expected
|
|
141
|
+
expect(mockConsole.logs).toEqual([])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('should handle no changes gracefully in XML format', async () => {
|
|
145
|
+
const mockApi = GerritApiService.of({
|
|
146
|
+
listChanges: () => Effect.succeed([]),
|
|
147
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
148
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
149
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
150
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
151
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
152
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
153
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
154
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
155
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
156
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
157
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
158
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await Effect.runPromise(
|
|
162
|
+
mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const output = mockConsole.logs.join('\n')
|
|
166
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
167
|
+
expect(output).toContain('<changes count="0">')
|
|
168
|
+
expect(output).toContain('</changes>')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('should handle network failures gracefully', async () => {
|
|
172
|
+
const mockApi = GerritApiService.of({
|
|
173
|
+
listChanges: () => Effect.fail(new Error('Network error') as ApiError),
|
|
174
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
175
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
176
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
177
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
178
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
179
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
180
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
181
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
182
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
183
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
184
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
185
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const result = await Effect.runPromise(
|
|
189
|
+
Effect.either(
|
|
190
|
+
mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(result._tag).toBe('Left')
|
|
195
|
+
if (result._tag === 'Left') {
|
|
196
|
+
expect(result.left.message).toBe('Network error')
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('should handle network failures gracefully in XML format', async () => {
|
|
201
|
+
const mockApi = GerritApiService.of({
|
|
202
|
+
listChanges: () => Effect.fail(new Error('API error') as ApiError),
|
|
203
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
204
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
205
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
206
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
207
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
208
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
209
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
210
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
211
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
212
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
213
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
214
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const result = await Effect.runPromise(
|
|
218
|
+
Effect.either(
|
|
219
|
+
mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
expect(result._tag).toBe('Left')
|
|
224
|
+
if (result._tag === 'Left') {
|
|
225
|
+
expect(result.left.message).toBe('API error')
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('should properly escape XML special characters', async () => {
|
|
230
|
+
const mockChanges: ChangeInfo[] = [
|
|
231
|
+
generateMockChange({
|
|
232
|
+
_number: 12345,
|
|
233
|
+
subject: 'Test with <special> & "characters"',
|
|
234
|
+
project: 'test-project',
|
|
235
|
+
branch: 'feature/test&update',
|
|
236
|
+
status: 'NEW',
|
|
237
|
+
}),
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
const mockApi = GerritApiService.of({
|
|
241
|
+
listChanges: () => Effect.succeed(mockChanges),
|
|
242
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
243
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
244
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
245
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
246
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
247
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
248
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
249
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
250
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
251
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
252
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
253
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await Effect.runPromise(
|
|
257
|
+
mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const output = mockConsole.logs.join('\n')
|
|
261
|
+
// CDATA sections should preserve special characters
|
|
262
|
+
expect(output).toContain('<![CDATA[Test with <special> & "characters"]]>')
|
|
263
|
+
expect(output).toContain('<branch>feature/test&update</branch>')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('should display changes with proper grouping by project', async () => {
|
|
267
|
+
const mockChanges: ChangeInfo[] = [
|
|
268
|
+
generateMockChange({
|
|
269
|
+
_number: 12345,
|
|
270
|
+
subject: 'Change in project A',
|
|
271
|
+
project: 'project-a',
|
|
272
|
+
branch: 'main',
|
|
273
|
+
status: 'NEW',
|
|
274
|
+
}),
|
|
275
|
+
generateMockChange({
|
|
276
|
+
_number: 12346,
|
|
277
|
+
subject: 'Change in project B',
|
|
278
|
+
project: 'project-b',
|
|
279
|
+
branch: 'main',
|
|
280
|
+
status: 'NEW',
|
|
281
|
+
}),
|
|
282
|
+
generateMockChange({
|
|
283
|
+
_number: 12347,
|
|
284
|
+
subject: 'Another change in project A',
|
|
285
|
+
project: 'project-a',
|
|
286
|
+
branch: 'develop',
|
|
287
|
+
status: 'MERGED',
|
|
288
|
+
}),
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
const mockApi = GerritApiService.of({
|
|
292
|
+
listChanges: () => Effect.succeed(mockChanges),
|
|
293
|
+
getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
294
|
+
postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
295
|
+
abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
296
|
+
testConnection: Effect.fail(new Error('Not implemented') as ApiError),
|
|
297
|
+
getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
298
|
+
getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
299
|
+
getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
300
|
+
getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
301
|
+
getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
302
|
+
getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
303
|
+
getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
304
|
+
getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
await Effect.runPromise(
|
|
308
|
+
mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
const output = mockConsole.logs.join('\n')
|
|
312
|
+
expect(output).toContain('Change in project A')
|
|
313
|
+
expect(output).toContain('Change in project B')
|
|
314
|
+
expect(output).toContain('Another change in project A')
|
|
315
|
+
expect(output).toContain('project-a')
|
|
316
|
+
expect(output).toContain('project-b')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mock } from 'bun:test'
|
|
2
|
+
import { Schema } from '@effect/schema'
|
|
3
|
+
import { ChangeInfo } from '@/schemas/gerrit'
|
|
4
|
+
import {
|
|
5
|
+
generateMockAccount,
|
|
6
|
+
generateMockChange,
|
|
7
|
+
generateMockFileDiff,
|
|
8
|
+
generateMockFiles,
|
|
9
|
+
} from '@/test-utils/mock-generator'
|
|
10
|
+
|
|
11
|
+
// Generate consistent mock data using Effect Schema
|
|
12
|
+
const mockChange = generateMockChange()
|
|
13
|
+
const mockFiles = generateMockFiles()
|
|
14
|
+
const mockDiff = generateMockFileDiff()
|
|
15
|
+
const mockAccount = generateMockAccount()
|
|
16
|
+
|
|
17
|
+
// Keep the old mockChange definition for now as backup (disabled to fix unused variable)
|
|
18
|
+
// const _mockChange: Schema.Schema.Type<typeof ChangeInfo> = {
|
|
19
|
+
// id: 'myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940',
|
|
20
|
+
// project: 'myProject',
|
|
21
|
+
// branch: 'master',
|
|
22
|
+
// change_id: 'I8473b95934b5732ac55d26311a706c9c2bde9940',
|
|
23
|
+
// subject: 'Implementing new feature',
|
|
24
|
+
// status: 'NEW',
|
|
25
|
+
// created: '2023-12-01 10:00:00.000000000',
|
|
26
|
+
// updated: '2023-12-01 12:00:00.000000000',
|
|
27
|
+
// _number: 123456,
|
|
28
|
+
// owner: {
|
|
29
|
+
// _account_id: 1000000,
|
|
30
|
+
// name: 'John Doe',
|
|
31
|
+
// email: 'john.doe@example.com',
|
|
32
|
+
// },
|
|
33
|
+
// labels: {},
|
|
34
|
+
// permitted_labels: {},
|
|
35
|
+
// removable_reviewers: [],
|
|
36
|
+
// reviewers: {},
|
|
37
|
+
// requirements: [],
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
export const setupFetchMock = () => {
|
|
41
|
+
return mock(async (url: string | URL, options?: RequestInit) => {
|
|
42
|
+
const urlStr = url.toString()
|
|
43
|
+
const method = options?.method || 'GET'
|
|
44
|
+
|
|
45
|
+
// Check authentication
|
|
46
|
+
const authHeader =
|
|
47
|
+
options?.headers && 'Authorization' in options.headers
|
|
48
|
+
? (options.headers as Record<string, string>).Authorization
|
|
49
|
+
: undefined
|
|
50
|
+
|
|
51
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
52
|
+
return new Response(`)]}'\n${JSON.stringify({ message: 'Unauthorized' })}`, { status: 401 })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Authentication endpoint
|
|
56
|
+
if (urlStr.includes('/a/accounts/self')) {
|
|
57
|
+
return new Response(`)]}'\n${JSON.stringify(mockAccount)}`, { status: 200 })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// List changes endpoint (must come before get change endpoint)
|
|
61
|
+
if (urlStr.includes('/a/changes/?q=')) {
|
|
62
|
+
return new Response(`)]}'\n${JSON.stringify([mockChange])}`, { status: 200 })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get change endpoint
|
|
66
|
+
if (
|
|
67
|
+
urlStr.includes('/a/changes/') &&
|
|
68
|
+
method === 'GET' &&
|
|
69
|
+
!urlStr.includes('/files') &&
|
|
70
|
+
!urlStr.includes('/diff') &&
|
|
71
|
+
!urlStr.includes('/patch') &&
|
|
72
|
+
!urlStr.includes('/review')
|
|
73
|
+
) {
|
|
74
|
+
if (urlStr.includes('notfound')) {
|
|
75
|
+
return new Response(`)]}'\n${JSON.stringify({ message: 'Not found' })}`, { status: 404 })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Validate response against schema
|
|
79
|
+
const validated = Schema.decodeUnknownSync(ChangeInfo)(mockChange)
|
|
80
|
+
return new Response(`)]}'\n${JSON.stringify(validated)}`, { status: 200 })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get file diff endpoint - must be checked BEFORE other file endpoints
|
|
84
|
+
if (urlStr.includes('/files/') && urlStr.includes('/diff') && method === 'GET') {
|
|
85
|
+
return new Response(`)]}'\n${JSON.stringify(mockDiff)}`, { status: 200 })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get files endpoint (list of files)
|
|
89
|
+
if (
|
|
90
|
+
urlStr.includes('/files') &&
|
|
91
|
+
method === 'GET' &&
|
|
92
|
+
!urlStr.includes('/diff') &&
|
|
93
|
+
!urlStr.includes('/content')
|
|
94
|
+
) {
|
|
95
|
+
return new Response(`)]}'\n${JSON.stringify(mockFiles)}`, { status: 200 })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get file content endpoint
|
|
99
|
+
if (urlStr.includes('/content') && method === 'GET' && !urlStr.includes('/diff')) {
|
|
100
|
+
const content =
|
|
101
|
+
'function main() {\n console.log("Hello, world!")\n return process.exit(0)\n}'
|
|
102
|
+
const base64Content = btoa(content)
|
|
103
|
+
return new Response(base64Content, { status: 200 })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get patch endpoint
|
|
107
|
+
if (urlStr.includes('/patch') && method === 'GET') {
|
|
108
|
+
const patch = `--- a/src/main.ts
|
|
109
|
+
+++ b/src/main.ts
|
|
110
|
+
@@ -1,3 +1,3 @@
|
|
111
|
+
function main() {
|
|
112
|
+
console.log("Hello, world!")
|
|
113
|
+
- return 0
|
|
114
|
+
+ return process.exit(0)
|
|
115
|
+
}`
|
|
116
|
+
const base64Patch = btoa(patch)
|
|
117
|
+
return new Response(base64Patch, { status: 200 })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Post review endpoint
|
|
121
|
+
if (urlStr.includes('/review') && method === 'POST') {
|
|
122
|
+
return new Response(
|
|
123
|
+
")]}'\n" +
|
|
124
|
+
JSON.stringify({
|
|
125
|
+
labels: {},
|
|
126
|
+
ready: true,
|
|
127
|
+
}),
|
|
128
|
+
{ status: 200 },
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Default 404 for unhandled requests
|
|
133
|
+
return new Response(`)]}'\n${JSON.stringify({ message: 'Not found' })}`, { status: 404 })
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const restoreFetch: () => void = () => {
|
|
138
|
+
// Restore original fetch (Bun handles this automatically after tests)
|
|
139
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { HttpResponse, http } from 'msw'
|
|
2
|
+
import type { CommentInfo } from '@/schemas/gerrit'
|
|
3
|
+
|
|
4
|
+
export const commentHandlers = [
|
|
5
|
+
// Comments endpoint
|
|
6
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
7
|
+
const mockComments: Record<string, CommentInfo[]> = {
|
|
8
|
+
'/COMMIT_MSG': [
|
|
9
|
+
{
|
|
10
|
+
id: 'comment1',
|
|
11
|
+
message: 'Please update the commit message',
|
|
12
|
+
author: {
|
|
13
|
+
name: 'Reviewer 1',
|
|
14
|
+
email: 'reviewer1@example.com',
|
|
15
|
+
_account_id: 1001,
|
|
16
|
+
},
|
|
17
|
+
updated: '2024-01-15 10:30:00.000000000',
|
|
18
|
+
unresolved: true,
|
|
19
|
+
line: 3,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
'src/main.ts': [
|
|
23
|
+
{
|
|
24
|
+
id: 'comment2',
|
|
25
|
+
message: 'Consider using a more descriptive variable name',
|
|
26
|
+
author: {
|
|
27
|
+
name: 'Reviewer 2',
|
|
28
|
+
email: 'reviewer2@example.com',
|
|
29
|
+
_account_id: 1002,
|
|
30
|
+
},
|
|
31
|
+
updated: '2024-01-15 11:45:00.000000000',
|
|
32
|
+
unresolved: false,
|
|
33
|
+
line: 42,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'comment3',
|
|
37
|
+
message: 'This could be simplified',
|
|
38
|
+
author: {
|
|
39
|
+
name: 'Reviewer 1',
|
|
40
|
+
_account_id: 1001,
|
|
41
|
+
},
|
|
42
|
+
updated: '2024-01-15 12:00:00.000000000',
|
|
43
|
+
line: 67,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
// File diff endpoint
|
|
52
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/files/:filePath/diff', () => {
|
|
53
|
+
const mockDiff = {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
ab: ['function calculateTotal(items) {', ' let total = 0;'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
b: [
|
|
60
|
+
' // TODO: Add validation',
|
|
61
|
+
' for (const item of items) {',
|
|
62
|
+
' total += item.price * item.quantity;',
|
|
63
|
+
' }',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
ab: [' return total;', '}'],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockDiff)}`)
|
|
73
|
+
}),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
export const emptyCommentsHandlers = [
|
|
77
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
78
|
+
return HttpResponse.text(`)]}'\n{}`)
|
|
79
|
+
}),
|
|
80
|
+
]
|