@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,323 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { Effect, Layer } from 'effect'
|
|
3
|
+
import { delay, HttpResponse, http } from 'msw'
|
|
4
|
+
import { setupServer } from 'msw/node'
|
|
5
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
6
|
+
import { commentsCommand } from '@/cli/commands/comments'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { commentHandlers, emptyCommentsHandlers } from './mocks/msw-handlers'
|
|
9
|
+
|
|
10
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
11
|
+
// Create MSW server
|
|
12
|
+
const server = setupServer(
|
|
13
|
+
// Default handler for auth check
|
|
14
|
+
http.get('*/a/accounts/self', ({ request }) => {
|
|
15
|
+
const auth = request.headers.get('Authorization')
|
|
16
|
+
if (!auth || !auth.startsWith('Basic ')) {
|
|
17
|
+
return HttpResponse.text('Unauthorized', { status: 401 })
|
|
18
|
+
}
|
|
19
|
+
return HttpResponse.json({
|
|
20
|
+
_account_id: 1000,
|
|
21
|
+
name: 'Test User',
|
|
22
|
+
email: 'test@example.com',
|
|
23
|
+
})
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
describe('comments command', () => {
|
|
28
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
29
|
+
let mockConsoleError: ReturnType<typeof mock>
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
// Start MSW server before all tests
|
|
33
|
+
server.listen({ onUnhandledRequest: 'bypass' })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
// Clean up after all tests
|
|
38
|
+
server.close()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
// Reset handlers to defaults before each test
|
|
43
|
+
server.resetHandlers()
|
|
44
|
+
|
|
45
|
+
mockConsoleLog = mock(() => {})
|
|
46
|
+
mockConsoleError = mock(() => {})
|
|
47
|
+
console.log = mockConsoleLog
|
|
48
|
+
console.error = mockConsoleError
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
// Clean up after each test
|
|
53
|
+
server.resetHandlers()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should fetch and display comments in pretty format', async () => {
|
|
57
|
+
// Add comment handlers for this test
|
|
58
|
+
server.use(...commentHandlers)
|
|
59
|
+
|
|
60
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
61
|
+
|
|
62
|
+
const program = commentsCommand('12345', {}).pipe(
|
|
63
|
+
Effect.provide(GerritApiServiceLive),
|
|
64
|
+
Effect.provide(mockConfigLayer),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await Effect.runPromise(program)
|
|
68
|
+
|
|
69
|
+
// Check that comments were displayed
|
|
70
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
71
|
+
expect(output).toContain('Found 3 comments')
|
|
72
|
+
expect(output).toContain('Commit Message')
|
|
73
|
+
expect(output).toContain('Please update the commit message')
|
|
74
|
+
expect(output).toContain('src/main.ts')
|
|
75
|
+
expect(output).toContain('Consider using a more descriptive variable name')
|
|
76
|
+
expect(output).toContain('[UNRESOLVED]')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should output XML format when --xml flag is used', async () => {
|
|
80
|
+
// Add comment handlers for this test
|
|
81
|
+
server.use(...commentHandlers)
|
|
82
|
+
|
|
83
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
84
|
+
|
|
85
|
+
const program = commentsCommand('12345', { xml: true }).pipe(
|
|
86
|
+
Effect.provide(GerritApiServiceLive),
|
|
87
|
+
Effect.provide(mockConfigLayer),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
await Effect.runPromise(program)
|
|
91
|
+
|
|
92
|
+
// Check XML output structure
|
|
93
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
94
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
95
|
+
expect(output).toContain('<comments_result>')
|
|
96
|
+
expect(output).toContain('<change_id>12345</change_id>')
|
|
97
|
+
expect(output).toContain('<comment_count>3</comment_count>')
|
|
98
|
+
expect(output).toContain('<message><![CDATA[Please update the commit message]]></message>')
|
|
99
|
+
expect(output).toContain('<unresolved>true</unresolved>')
|
|
100
|
+
expect(output).toContain('</comments_result>')
|
|
101
|
+
|
|
102
|
+
// Verify XML is well-formed
|
|
103
|
+
expect(output.match(/<comment>/g)?.length).toBe(3)
|
|
104
|
+
expect(output.match(/<\/comment>/g)?.length).toBe(3)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle no comments gracefully', async () => {
|
|
108
|
+
// Use empty comments handlers for this test
|
|
109
|
+
server.use(...emptyCommentsHandlers)
|
|
110
|
+
|
|
111
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
112
|
+
|
|
113
|
+
const program = commentsCommand('12345', {}).pipe(
|
|
114
|
+
Effect.provide(GerritApiServiceLive),
|
|
115
|
+
Effect.provide(mockConfigLayer),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
await Effect.runPromise(program)
|
|
119
|
+
|
|
120
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
121
|
+
expect(output).toContain('No comments found on this change')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should handle network failures gracefully', async () => {
|
|
125
|
+
// Configure server to return network error
|
|
126
|
+
server.use(
|
|
127
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
128
|
+
return HttpResponse.error()
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
133
|
+
|
|
134
|
+
const program = commentsCommand('12345', {}).pipe(
|
|
135
|
+
Effect.provide(GerritApiServiceLive),
|
|
136
|
+
Effect.provide(mockConfigLayer),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
await Effect.runPromise(program)
|
|
140
|
+
|
|
141
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
142
|
+
expect(errorOutput).toContain('Failed to fetch comments')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should handle network failures gracefully in XML mode', async () => {
|
|
146
|
+
// Configure server to return network error
|
|
147
|
+
server.use(
|
|
148
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
149
|
+
return HttpResponse.error()
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
154
|
+
|
|
155
|
+
const program = commentsCommand('12345', { xml: true }).pipe(
|
|
156
|
+
Effect.provide(GerritApiServiceLive),
|
|
157
|
+
Effect.provide(mockConfigLayer),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
await Effect.runPromise(program)
|
|
161
|
+
|
|
162
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
163
|
+
expect(output).toContain('<status>error</status>')
|
|
164
|
+
expect(output).toContain('<error><![CDATA[')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should handle diff fetch failures gracefully', async () => {
|
|
168
|
+
// Comments endpoint works
|
|
169
|
+
server.use(
|
|
170
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
171
|
+
return HttpResponse.text(`)]}'\n{
|
|
172
|
+
"src/file.ts": [{
|
|
173
|
+
"id": "test1",
|
|
174
|
+
"message": "Test comment",
|
|
175
|
+
"line": 10,
|
|
176
|
+
"author": {"name": "Test User"}
|
|
177
|
+
}]
|
|
178
|
+
}`)
|
|
179
|
+
}),
|
|
180
|
+
// Diff endpoint fails
|
|
181
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/files/:filePath/diff', () => {
|
|
182
|
+
return HttpResponse.error()
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
187
|
+
|
|
188
|
+
const program = commentsCommand('12345', {}).pipe(
|
|
189
|
+
Effect.provide(GerritApiServiceLive),
|
|
190
|
+
Effect.provide(mockConfigLayer),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
await Effect.runPromise(program)
|
|
194
|
+
|
|
195
|
+
// Should still display comment without context
|
|
196
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
197
|
+
expect(output).toContain('Test comment')
|
|
198
|
+
expect(output).toContain('src/file.ts')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should handle concurrent API calls efficiently', async () => {
|
|
202
|
+
let _commentCallTime: number | null = null
|
|
203
|
+
let diffCallCount = 0
|
|
204
|
+
const diffCallTimes: number[] = []
|
|
205
|
+
|
|
206
|
+
server.use(
|
|
207
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', async () => {
|
|
208
|
+
_commentCallTime = Date.now()
|
|
209
|
+
await delay(50) // Simulate network delay
|
|
210
|
+
return HttpResponse.text(`)]}'\n{
|
|
211
|
+
"file1.ts": [{"id": "c1", "message": "Comment 1", "line": 10}],
|
|
212
|
+
"file2.ts": [{"id": "c2", "message": "Comment 2", "line": 20}],
|
|
213
|
+
"file3.ts": [{"id": "c3", "message": "Comment 3", "line": 30}]
|
|
214
|
+
}`)
|
|
215
|
+
}),
|
|
216
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/files/:filePath/diff', async () => {
|
|
217
|
+
diffCallCount++
|
|
218
|
+
diffCallTimes.push(Date.now())
|
|
219
|
+
await delay(100) // Simulate network delay
|
|
220
|
+
return HttpResponse.text(`)]}'\n{
|
|
221
|
+
"content": [{"ab": ["line 1", "line 2"]}]
|
|
222
|
+
}`)
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
227
|
+
|
|
228
|
+
const startTime = Date.now()
|
|
229
|
+
const program = commentsCommand('12345', {}).pipe(
|
|
230
|
+
Effect.provide(GerritApiServiceLive),
|
|
231
|
+
Effect.provide(mockConfigLayer),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
await Effect.runPromise(program)
|
|
235
|
+
const totalTime = Date.now() - startTime
|
|
236
|
+
|
|
237
|
+
// Verify concurrent execution
|
|
238
|
+
expect(diffCallCount).toBe(3) // 3 diff calls made
|
|
239
|
+
|
|
240
|
+
// All diff calls should start close together (within 100ms)
|
|
241
|
+
// indicating concurrent execution, not sequential
|
|
242
|
+
const firstDiffTime = diffCallTimes[0]
|
|
243
|
+
const lastDiffTime = diffCallTimes[diffCallTimes.length - 1]
|
|
244
|
+
expect(lastDiffTime - firstDiffTime).toBeLessThan(100)
|
|
245
|
+
|
|
246
|
+
// Total time should be less than sequential execution would take
|
|
247
|
+
// Sequential: 50ms (comments) + 3 * 100ms (diffs) = 350ms
|
|
248
|
+
// Concurrent: 50ms (comments) + 100ms (parallel diffs) = 150ms (plus overhead)
|
|
249
|
+
expect(totalTime).toBeLessThan(250)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should properly escape XML special characters', async () => {
|
|
253
|
+
server.use(
|
|
254
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
255
|
+
return HttpResponse.text(`)]}'\n{
|
|
256
|
+
"test.xml": [{
|
|
257
|
+
"id": "xml-test",
|
|
258
|
+
"message": "Test <script>alert('XSS')</script> & entities",
|
|
259
|
+
"author": {
|
|
260
|
+
"name": "User <>&\\"'",
|
|
261
|
+
"email": "test@example.com"
|
|
262
|
+
}
|
|
263
|
+
}]
|
|
264
|
+
}`)
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
269
|
+
|
|
270
|
+
const program = commentsCommand('12345', { xml: true }).pipe(
|
|
271
|
+
Effect.provide(GerritApiServiceLive),
|
|
272
|
+
Effect.provide(mockConfigLayer),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
await Effect.runPromise(program)
|
|
276
|
+
|
|
277
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
278
|
+
// Message should be in CDATA
|
|
279
|
+
expect(output).toContain(
|
|
280
|
+
"<message><![CDATA[Test <script>alert('XSS')</script> & entities]]></message>",
|
|
281
|
+
)
|
|
282
|
+
// Author name should be in CDATA
|
|
283
|
+
expect(output).toContain('<name><![CDATA[User <>&"\']]></name>')
|
|
284
|
+
// Email should be escaped
|
|
285
|
+
expect(output).toContain('<email>test@example.com</email>')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should handle comments with ranges correctly', async () => {
|
|
289
|
+
server.use(
|
|
290
|
+
http.get('*/a/changes/:changeId/revisions/:revisionId/comments', () => {
|
|
291
|
+
return HttpResponse.text(`)]}'\n{
|
|
292
|
+
"src/range.ts": [{
|
|
293
|
+
"id": "range-comment",
|
|
294
|
+
"message": "Multi-line comment",
|
|
295
|
+
"range": {
|
|
296
|
+
"start_line": 10,
|
|
297
|
+
"end_line": 15,
|
|
298
|
+
"start_character": 5,
|
|
299
|
+
"end_character": 20
|
|
300
|
+
}
|
|
301
|
+
}]
|
|
302
|
+
}`)
|
|
303
|
+
}),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
307
|
+
|
|
308
|
+
const program = commentsCommand('12345', { xml: true }).pipe(
|
|
309
|
+
Effect.provide(GerritApiServiceLive),
|
|
310
|
+
Effect.provide(mockConfigLayer),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
await Effect.runPromise(program)
|
|
314
|
+
|
|
315
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
316
|
+
expect(output).toContain('<range>')
|
|
317
|
+
expect(output).toContain('<start_line>10</start_line>')
|
|
318
|
+
expect(output).toContain('<end_line>15</end_line>')
|
|
319
|
+
expect(output).toContain('<start_character>5</start_character>')
|
|
320
|
+
expect(output).toContain('<end_character>20</end_character>')
|
|
321
|
+
expect(output).toContain('</range>')
|
|
322
|
+
})
|
|
323
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { ConfigService, ConfigError, ConfigServiceLive } from '@/services/config'
|
|
4
|
+
import { GerritCredentials } from '@/schemas/gerrit'
|
|
5
|
+
import { AiConfig, AppConfig } from '@/schemas/config'
|
|
6
|
+
|
|
7
|
+
describe('Config Service Simple Tests', () => {
|
|
8
|
+
describe('ConfigError', () => {
|
|
9
|
+
test('should create ConfigError with message', () => {
|
|
10
|
+
const error = new ConfigError({ message: 'Test error' })
|
|
11
|
+
expect(error.message).toBe('Test error')
|
|
12
|
+
expect(error._tag).toBe('ConfigError')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('should be throwable and catchable', () => {
|
|
16
|
+
const error = new ConfigError({ message: 'Test error' })
|
|
17
|
+
expect(() => {
|
|
18
|
+
throw error
|
|
19
|
+
}).toThrow('Test error')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('should be instanceof ConfigError', () => {
|
|
23
|
+
const error = new ConfigError({ message: 'Test error' })
|
|
24
|
+
expect(error).toBeInstanceOf(ConfigError)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('ConfigServiceLive layer', () => {
|
|
29
|
+
test('should be able to create live service layer', () => {
|
|
30
|
+
expect(ConfigServiceLive).toBeDefined()
|
|
31
|
+
expect(typeof ConfigServiceLive).toBe('object')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should provide all required service methods', async () => {
|
|
35
|
+
const service = await Effect.runPromise(
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
return yield* ConfigService
|
|
38
|
+
}).pipe(Effect.provide(ConfigServiceLive)),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(typeof service.getCredentials).toBe('object') // Effect object
|
|
42
|
+
expect(typeof service.saveCredentials).toBe('function')
|
|
43
|
+
expect(typeof service.deleteCredentials).toBe('object') // Effect object
|
|
44
|
+
expect(typeof service.getAiConfig).toBe('object') // Effect object
|
|
45
|
+
expect(typeof service.saveAiConfig).toBe('function')
|
|
46
|
+
expect(typeof service.getFullConfig).toBe('object') // Effect object
|
|
47
|
+
expect(typeof service.saveFullConfig).toBe('function')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Note: Config behavior tests removed as they depend on filesystem state
|
|
52
|
+
// which varies between test environments
|
|
53
|
+
|
|
54
|
+
describe('Schema validation', () => {
|
|
55
|
+
test('should validate valid credentials schema', () => {
|
|
56
|
+
const validCredentials: GerritCredentials = {
|
|
57
|
+
host: 'https://gerrit.example.com',
|
|
58
|
+
username: 'testuser',
|
|
59
|
+
password: 'testpass',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
expect(validCredentials.host).toBe('https://gerrit.example.com')
|
|
63
|
+
expect(validCredentials.username).toBe('testuser')
|
|
64
|
+
expect(validCredentials.password).toBe('testpass')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('should validate valid AI config schema', () => {
|
|
68
|
+
const validAiConfig: AiConfig = {
|
|
69
|
+
autoDetect: true,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(validAiConfig.autoDetect).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('should validate AI config with tool', () => {
|
|
76
|
+
const validAiConfig: AiConfig = {
|
|
77
|
+
autoDetect: false,
|
|
78
|
+
tool: 'claude',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(validAiConfig.autoDetect).toBe(false)
|
|
82
|
+
expect(validAiConfig.tool).toBe('claude')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('should validate full app config schema', () => {
|
|
86
|
+
const validAppConfig: AppConfig = {
|
|
87
|
+
host: 'https://gerrit.example.com',
|
|
88
|
+
username: 'testuser',
|
|
89
|
+
password: 'testpass',
|
|
90
|
+
aiAutoDetect: true,
|
|
91
|
+
aiTool: 'claude',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect(validAppConfig.host).toBe('https://gerrit.example.com')
|
|
95
|
+
expect(validAppConfig.username).toBe('testuser')
|
|
96
|
+
expect(validAppConfig.aiAutoDetect).toBe(true)
|
|
97
|
+
expect(validAppConfig.aiTool).toBe('claude')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
})
|