@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,419 @@
|
|
|
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 { diffCommand } from '@/cli/commands/diff'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
|
|
9
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
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('diff 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
|
+
server.resetHandlers()
|
|
40
|
+
|
|
41
|
+
mockConsoleLog = mock(() => {})
|
|
42
|
+
mockConsoleError = mock(() => {})
|
|
43
|
+
console.log = mockConsoleLog
|
|
44
|
+
console.error = mockConsoleError
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
server.resetHandlers()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const createMockConfigLayer = () => Layer.succeed(ConfigService, createMockConfigService())
|
|
52
|
+
|
|
53
|
+
describe('unified diff output', () => {
|
|
54
|
+
it('should fetch and display unified diff by default', async () => {
|
|
55
|
+
const mockDiff = `ZGlmZiAtLWdpdCBhL3NyYy9tYWluLmpzIGIvc3JjL21haW4uanMKaW5kZXggMTIzNDU2Ny4uYWJjZGVmZyAxMDA2NDQKLS0tIGEvc3JjL21haW4uanMKKysrIGIvc3JjL21haW4uanMKQEAgLTEwLDYgKzEwLDcgQEAgZXhwb3J0IGZ1bmN0aW9uIG1haW4oKSB7CiAgIGNvbnNvbGUubG9nKCdTdGFydGluZyBhcHBsaWNhdGlvbicpCisgIGNvbnNvbGUubG9nKCdEZWJ1ZyBpbmZvJykKICAgcmV0dXJuICdzdWNjZXNzJwogfQ==`
|
|
56
|
+
|
|
57
|
+
server.use(
|
|
58
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
59
|
+
return HttpResponse.text(mockDiff)
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const program = diffCommand('12345', {}).pipe(
|
|
64
|
+
Effect.provide(GerritApiServiceLive),
|
|
65
|
+
Effect.provide(createMockConfigLayer()),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
await Effect.runPromise(program)
|
|
69
|
+
|
|
70
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
71
|
+
expect(output).toContain('diff --git a/src/main.js b/src/main.js')
|
|
72
|
+
expect(output).toContain("+ console.log('Debug info')")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should fetch diff for specific file', async () => {
|
|
76
|
+
const mockDiff = {
|
|
77
|
+
meta_a: {
|
|
78
|
+
name: 'src/utils.js',
|
|
79
|
+
},
|
|
80
|
+
meta_b: {
|
|
81
|
+
name: 'src/utils.js',
|
|
82
|
+
},
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
ab: ['export function helper() {', ' return true'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
b: [' // Added comment'],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
ab: ['}'],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
server.use(
|
|
97
|
+
http.get('*/a/changes/:changeId/revisions/current/files/*/diff', () => {
|
|
98
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockDiff)}`)
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const program = diffCommand('12345', { file: 'src/utils.js' }).pipe(
|
|
103
|
+
Effect.provide(GerritApiServiceLive),
|
|
104
|
+
Effect.provide(createMockConfigLayer()),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await Effect.runPromise(program)
|
|
108
|
+
|
|
109
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
110
|
+
expect(output).toContain('--- a/src/utils.js')
|
|
111
|
+
expect(output).toContain('+++ b/src/utils.js')
|
|
112
|
+
expect(output).toContain('+ // Added comment')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should use specified format', async () => {
|
|
116
|
+
const mockFiles = {
|
|
117
|
+
'/COMMIT_MSG': { status: 'A' },
|
|
118
|
+
'src/main.js': { status: 'M' },
|
|
119
|
+
'src/utils.js': { status: 'A' },
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
server.use(
|
|
123
|
+
http.get('*/a/changes/:changeId/revisions/current/files', () => {
|
|
124
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const program = diffCommand('12345', { format: 'json' }).pipe(
|
|
129
|
+
Effect.provide(GerritApiServiceLive),
|
|
130
|
+
Effect.provide(createMockConfigLayer()),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await Effect.runPromise(program)
|
|
134
|
+
|
|
135
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
136
|
+
expect(output).toContain('"src/main.js"')
|
|
137
|
+
expect(output).toContain('"status": "M"')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('files-only output', () => {
|
|
142
|
+
it('should fetch and display files list when filesOnly is true', async () => {
|
|
143
|
+
const mockFiles = {
|
|
144
|
+
'/COMMIT_MSG': { status: 'A' },
|
|
145
|
+
'src/main.js': { status: 'M' },
|
|
146
|
+
'src/utils.js': { status: 'A' },
|
|
147
|
+
'README.md': { status: 'M' },
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
server.use(
|
|
151
|
+
http.get('*/a/changes/:changeId/revisions/current/files', () => {
|
|
152
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const program = diffCommand('12345', { filesOnly: true }).pipe(
|
|
157
|
+
Effect.provide(GerritApiServiceLive),
|
|
158
|
+
Effect.provide(createMockConfigLayer()),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
await Effect.runPromise(program)
|
|
162
|
+
|
|
163
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
164
|
+
expect(output).toContain('Changed files')
|
|
165
|
+
expect(output).toContain('src/main.js')
|
|
166
|
+
expect(output).toContain('src/utils.js')
|
|
167
|
+
expect(output).toContain('README.md')
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('XML output', () => {
|
|
172
|
+
it('should output XML format for unified diff', async () => {
|
|
173
|
+
const mockDiff = `Y29uc29sZS5sb2coJ3Rlc3QnKQorY29uc29sZS5sb2coJ25ldyBsaW5lJyk=`
|
|
174
|
+
|
|
175
|
+
server.use(
|
|
176
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
177
|
+
return HttpResponse.text(mockDiff)
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const program = diffCommand('12345', { xml: true }).pipe(
|
|
182
|
+
Effect.provide(GerritApiServiceLive),
|
|
183
|
+
Effect.provide(createMockConfigLayer()),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await Effect.runPromise(program)
|
|
187
|
+
|
|
188
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
189
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
190
|
+
expect(output).toContain('<diff_result>')
|
|
191
|
+
expect(output).toContain('<status>success</status>')
|
|
192
|
+
expect(output).toContain('<change_id>12345</change_id>')
|
|
193
|
+
expect(output).toContain('<content><![CDATA[')
|
|
194
|
+
expect(output).toContain('console.log')
|
|
195
|
+
expect(output).toContain(']]></content>')
|
|
196
|
+
expect(output).toContain('</diff_result>')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should output XML format for files list', async () => {
|
|
200
|
+
const mockFiles = {
|
|
201
|
+
'/COMMIT_MSG': { status: 'A' },
|
|
202
|
+
'src/main.js': { status: 'M' },
|
|
203
|
+
'test.js': { status: 'A' },
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
server.use(
|
|
207
|
+
http.get('*/a/changes/:changeId/revisions/current/files', () => {
|
|
208
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const program = diffCommand('12345', { xml: true, filesOnly: true }).pipe(
|
|
213
|
+
Effect.provide(GerritApiServiceLive),
|
|
214
|
+
Effect.provide(createMockConfigLayer()),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
await Effect.runPromise(program)
|
|
218
|
+
|
|
219
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
220
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
221
|
+
expect(output).toContain('<diff_result>')
|
|
222
|
+
expect(output).toContain('<files>')
|
|
223
|
+
expect(output).toContain('<file>src/main.js</file>')
|
|
224
|
+
expect(output).toContain('<file>test.js</file>')
|
|
225
|
+
expect(output).toContain('</files>')
|
|
226
|
+
expect(output).toContain('</diff_result>')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should output XML format for JSON data', async () => {
|
|
230
|
+
const mockData = {
|
|
231
|
+
'/COMMIT_MSG': { status: 'A' },
|
|
232
|
+
'src/main.js': { status: 'M' },
|
|
233
|
+
'test.js': { status: 'A' },
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
server.use(
|
|
237
|
+
http.get('*/a/changes/:changeId/revisions/current/files', () => {
|
|
238
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockData)}`)
|
|
239
|
+
}),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const program = diffCommand('12345', { xml: true, format: 'json' }).pipe(
|
|
243
|
+
Effect.provide(GerritApiServiceLive),
|
|
244
|
+
Effect.provide(createMockConfigLayer()),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
await Effect.runPromise(program)
|
|
248
|
+
|
|
249
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
250
|
+
expect(output).toContain('<content><![CDATA[')
|
|
251
|
+
expect(output).toContain('"src/main.js"')
|
|
252
|
+
expect(output).toContain('"status": "M"')
|
|
253
|
+
expect(output).toContain(']]></content>')
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('error handling', () => {
|
|
258
|
+
it('should handle 404 change not found', async () => {
|
|
259
|
+
server.use(
|
|
260
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
261
|
+
return HttpResponse.text('Change not found', { status: 404 })
|
|
262
|
+
}),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const program = diffCommand('12345', {}).pipe(
|
|
266
|
+
Effect.provide(GerritApiServiceLive),
|
|
267
|
+
Effect.provide(createMockConfigLayer()),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get diff')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should handle 403 access denied', async () => {
|
|
274
|
+
server.use(
|
|
275
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
276
|
+
return HttpResponse.text('Access denied', { status: 403 })
|
|
277
|
+
}),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const program = diffCommand('12345', {}).pipe(
|
|
281
|
+
Effect.provide(GerritApiServiceLive),
|
|
282
|
+
Effect.provide(createMockConfigLayer()),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get diff')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should handle network errors', async () => {
|
|
289
|
+
server.use(
|
|
290
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
291
|
+
return HttpResponse.error()
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
const program = diffCommand('12345', {}).pipe(
|
|
296
|
+
Effect.provide(GerritApiServiceLive),
|
|
297
|
+
Effect.provide(createMockConfigLayer()),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get diff')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('should handle invalid options schema', async () => {
|
|
304
|
+
const program = diffCommand('12345', { format: 'invalid' } as never).pipe(
|
|
305
|
+
Effect.provide(GerritApiServiceLive),
|
|
306
|
+
Effect.provide(createMockConfigLayer()),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Invalid diff command options')
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('output formatting', () => {
|
|
314
|
+
it('should apply pretty formatting to unified diff by default', async () => {
|
|
315
|
+
const mockDiff = `ZGlmZiAtLWdpdCBhL3NyYy9tYWluLmpzIGIvc3JjL21haW4uanMKaW5kZXggMTIzNDU2Ny4uYWJjZGVmZyAxMDA2NDQKLS0tIGEvc3JjL21haW4uanMKKysrIGIvc3JjL21haW4uanMKQEAgLTEwLDYgKzEwLDcgQEAgZXhwb3J0IGZ1bmN0aW9uIG1haW4oKSB7CiAgIGNvbnNvbGUubG9nKCdTdGFydGluZyBhcHBsaWNhdGlvbicpCisgIGNvbnNvbGUubG9nKCdEZWJ1ZyBpbmZvJykKICAgcmV0dXJuICdzdWNjZXNzJwogfQ==`
|
|
316
|
+
|
|
317
|
+
server.use(
|
|
318
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
319
|
+
return HttpResponse.text(mockDiff)
|
|
320
|
+
}),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const program = diffCommand('12345', {}).pipe(
|
|
324
|
+
Effect.provide(GerritApiServiceLive),
|
|
325
|
+
Effect.provide(createMockConfigLayer()),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
await Effect.runPromise(program)
|
|
329
|
+
|
|
330
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
331
|
+
|
|
332
|
+
// Check that pretty formatting is applied (colors would be removed in test output,
|
|
333
|
+
// but structure should be preserved)
|
|
334
|
+
expect(output).toContain('diff --git')
|
|
335
|
+
expect(output).toContain('index 1234567..abcdefg')
|
|
336
|
+
expect(output).toContain('--- a/src/main.js')
|
|
337
|
+
expect(output).toContain('+++ b/src/main.js')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should format files list prettily', async () => {
|
|
341
|
+
const mockFiles = {
|
|
342
|
+
'/COMMIT_MSG': { status: 'A' },
|
|
343
|
+
'src/main.js': { status: 'M' },
|
|
344
|
+
'src/utils.js': { status: 'A' },
|
|
345
|
+
'test/test.js': { status: 'M' },
|
|
346
|
+
'README.md': { status: 'M' },
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
server.use(
|
|
350
|
+
http.get('*/a/changes/:changeId/revisions/current/files', () => {
|
|
351
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const program = diffCommand('12345', { filesOnly: true }).pipe(
|
|
356
|
+
Effect.provide(GerritApiServiceLive),
|
|
357
|
+
Effect.provide(createMockConfigLayer()),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
await Effect.runPromise(program)
|
|
361
|
+
|
|
362
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
363
|
+
expect(output).toContain('Changed files')
|
|
364
|
+
expect(output).toContain('src/main.js')
|
|
365
|
+
expect(output).toContain('src/utils.js')
|
|
366
|
+
expect(output).toContain('test/test.js')
|
|
367
|
+
expect(output).toContain('README.md')
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('option validation', () => {
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
// Default mock handlers for validation tests
|
|
374
|
+
server.use(
|
|
375
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
376
|
+
return HttpResponse.text('bW9jayBkaWZmIGNvbnRlbnQ=') // base64 for "mock diff content"
|
|
377
|
+
}),
|
|
378
|
+
http.get('*/a/changes/:changeId/revisions/current/files', () => {
|
|
379
|
+
return HttpResponse.text(`)]}'\n{"src/test.js": {"status": "M"}}`)
|
|
380
|
+
}),
|
|
381
|
+
http.get('*/a/changes/:changeId/revisions/current/files/*/diff', () => {
|
|
382
|
+
return HttpResponse.text(`)]}'\n{"content": [{"ab": ["test content"]}]}`)
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should accept valid format values', async () => {
|
|
388
|
+
// Test each valid format
|
|
389
|
+
for (const format of ['unified', 'json', 'files'] as const) {
|
|
390
|
+
const program = diffCommand('12345', { format }).pipe(
|
|
391
|
+
Effect.provide(GerritApiServiceLive),
|
|
392
|
+
Effect.provide(createMockConfigLayer()),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
await expect(Effect.runPromise(program)).resolves.toBeUndefined()
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should accept optional parameters', async () => {
|
|
400
|
+
const program = diffCommand('12345', {
|
|
401
|
+
xml: true,
|
|
402
|
+
file: 'src/test.js',
|
|
403
|
+
filesOnly: false,
|
|
404
|
+
format: 'unified',
|
|
405
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
|
|
406
|
+
|
|
407
|
+
await expect(Effect.runPromise(program)).resolves.toBeUndefined()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should work with minimal options', async () => {
|
|
411
|
+
const program = diffCommand('12345', {}).pipe(
|
|
412
|
+
Effect.provide(GerritApiServiceLive),
|
|
413
|
+
Effect.provide(createMockConfigLayer()),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
await expect(Effect.runPromise(program)).resolves.toBeUndefined()
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import type { ConfigServiceImpl } from '@/services/config'
|
|
3
|
+
import type { GerritCredentials } from '@/schemas/gerrit'
|
|
4
|
+
import type { AiConfig, AppConfig } from '@/schemas/config'
|
|
5
|
+
|
|
6
|
+
export const createMockConfigService = (
|
|
7
|
+
credentials: GerritCredentials = {
|
|
8
|
+
host: 'https://test.gerrit.com',
|
|
9
|
+
username: 'testuser',
|
|
10
|
+
password: 'testpass',
|
|
11
|
+
},
|
|
12
|
+
aiConfig: AiConfig = { autoDetect: true },
|
|
13
|
+
): ConfigServiceImpl => ({
|
|
14
|
+
getCredentials: Effect.succeed(credentials),
|
|
15
|
+
saveCredentials: () => Effect.succeed(undefined as void),
|
|
16
|
+
deleteCredentials: Effect.succeed(undefined as void),
|
|
17
|
+
getAiConfig: Effect.succeed(aiConfig),
|
|
18
|
+
saveAiConfig: () => Effect.succeed(undefined as void),
|
|
19
|
+
getFullConfig: Effect.succeed({
|
|
20
|
+
host: credentials.host,
|
|
21
|
+
username: credentials.username,
|
|
22
|
+
password: credentials.password,
|
|
23
|
+
aiTool: aiConfig.tool,
|
|
24
|
+
aiAutoDetect: aiConfig.autoDetect ?? true,
|
|
25
|
+
} as AppConfig),
|
|
26
|
+
saveFullConfig: () => Effect.succeed(undefined as void),
|
|
27
|
+
})
|