@aaronshaf/ger 0.1.0 → 0.1.3

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.
@@ -1,489 +0,0 @@
1
- import { describe, test, expect, beforeEach, mock } from 'bun:test'
2
- import { Effect, Layer } from 'effect'
3
- import {
4
- AiService,
5
- AiServiceError,
6
- NoAiToolFoundError,
7
- AiResponseParseError,
8
- AiServiceLive,
9
- } from '@/services/ai'
10
- import { ConfigService } from '@/services/config'
11
- import { createMockConfigService } from './helpers/config-mock'
12
-
13
- describe('AI Service', () => {
14
- describe('extractResponseTag', () => {
15
- test('should extract content from response tags', async () => {
16
- const input = `Some text before
17
- <response>
18
- This is the response content
19
- </response>
20
- Some text after`
21
-
22
- const result = await Effect.runPromise(
23
- Effect.gen(function* () {
24
- const service = yield* AiService
25
- return yield* service.extractResponseTag(input)
26
- }).pipe(
27
- Effect.provide(
28
- Layer.succeed(
29
- AiService,
30
- AiService.of({
31
- detectAiTool: () =>
32
- Effect.fail(new NoAiToolFoundError({ message: 'Not implemented' })),
33
- extractResponseTag: (output: string) =>
34
- Effect.gen(function* () {
35
- const responseMatch = output.match(/<response>([\s\S]*?)<\/response>/i)
36
-
37
- if (!responseMatch || !responseMatch[1]) {
38
- return yield* Effect.fail(
39
- new AiResponseParseError({
40
- message: 'No <response> tag found in AI output',
41
- rawOutput: output,
42
- }),
43
- )
44
- }
45
-
46
- return responseMatch[1].trim()
47
- }),
48
- runPrompt: () => Effect.fail(new AiServiceError({ message: 'Not implemented' })),
49
- }),
50
- ),
51
- ),
52
- ),
53
- )
54
-
55
- expect(result).toBe('This is the response content')
56
- })
57
-
58
- test('should handle case-insensitive response tags', async () => {
59
- const input = `<RESPONSE>Content here</RESPONSE>`
60
-
61
- const result = await Effect.runPromise(
62
- Effect.gen(function* () {
63
- const service = yield* AiService
64
- return yield* service.extractResponseTag(input)
65
- }).pipe(
66
- Effect.provide(
67
- Layer.succeed(
68
- AiService,
69
- AiService.of({
70
- detectAiTool: () =>
71
- Effect.fail(new NoAiToolFoundError({ message: 'Not implemented' })),
72
- extractResponseTag: (output: string) =>
73
- Effect.gen(function* () {
74
- const responseMatch = output.match(/<response>([\s\S]*?)<\/response>/i)
75
-
76
- if (!responseMatch || !responseMatch[1]) {
77
- return yield* Effect.fail(
78
- new AiResponseParseError({
79
- message: 'No <response> tag found in AI output',
80
- rawOutput: output,
81
- }),
82
- )
83
- }
84
-
85
- return responseMatch[1].trim()
86
- }),
87
- runPrompt: () => Effect.fail(new AiServiceError({ message: 'Not implemented' })),
88
- }),
89
- ),
90
- ),
91
- ),
92
- )
93
-
94
- expect(result).toBe('Content here')
95
- })
96
-
97
- test('should fail when no response tag is found', async () => {
98
- const input = 'This is just plain text without tags'
99
-
100
- const result = await Effect.runPromise(
101
- Effect.either(
102
- Effect.gen(function* () {
103
- const service = yield* AiService
104
- return yield* service.extractResponseTag(input)
105
- }).pipe(
106
- Effect.provide(
107
- Layer.succeed(
108
- AiService,
109
- AiService.of({
110
- detectAiTool: () =>
111
- Effect.fail(new NoAiToolFoundError({ message: 'Not implemented' })),
112
- extractResponseTag: (output: string) =>
113
- Effect.gen(function* () {
114
- const responseMatch = output.match(/<response>([\s\S]*?)<\/response>/i)
115
-
116
- if (!responseMatch || !responseMatch[1]) {
117
- return yield* Effect.fail(
118
- new AiResponseParseError({
119
- message: 'No <response> tag found in AI output',
120
- rawOutput: output,
121
- }),
122
- )
123
- }
124
-
125
- return responseMatch[1].trim()
126
- }),
127
- runPrompt: () => Effect.fail(new AiServiceError({ message: 'Not implemented' })),
128
- }),
129
- ),
130
- ),
131
- ),
132
- ),
133
- )
134
-
135
- expect(result._tag).toBe('Left')
136
- if (result._tag === 'Left') {
137
- expect(result.left).toBeInstanceOf(AiResponseParseError)
138
- expect((result.left as AiResponseParseError).rawOutput).toBe(input)
139
- }
140
- })
141
-
142
- test('should handle multiline content in response tags', async () => {
143
- const input = `<response>
144
- Line 1
145
- Line 2
146
- Line 3
147
- </response>`
148
-
149
- const result = await Effect.runPromise(
150
- Effect.gen(function* () {
151
- const service = yield* AiService
152
- return yield* service.extractResponseTag(input)
153
- }).pipe(
154
- Effect.provide(
155
- Layer.succeed(
156
- AiService,
157
- AiService.of({
158
- detectAiTool: () =>
159
- Effect.fail(new NoAiToolFoundError({ message: 'Not implemented' })),
160
- extractResponseTag: (output: string) =>
161
- Effect.gen(function* () {
162
- const responseMatch = output.match(/<response>([\s\S]*?)<\/response>/i)
163
-
164
- if (!responseMatch || !responseMatch[1]) {
165
- return yield* Effect.fail(
166
- new AiResponseParseError({
167
- message: 'No <response> tag found in AI output',
168
- rawOutput: output,
169
- }),
170
- )
171
- }
172
-
173
- return responseMatch[1].trim()
174
- }),
175
- runPrompt: () => Effect.fail(new AiServiceError({ message: 'Not implemented' })),
176
- }),
177
- ),
178
- ),
179
- ),
180
- )
181
-
182
- expect(result).toBe('Line 1\nLine 2\nLine 3')
183
- })
184
- })
185
-
186
- describe('Error Types', () => {
187
- test('NoAiToolFoundError should have correct message', () => {
188
- const error = new NoAiToolFoundError({
189
- message: 'No AI tool found. Please install claude, llm, or opencode CLI.',
190
- })
191
- expect(error.message).toBe('No AI tool found. Please install claude, llm, or opencode CLI.')
192
- })
193
-
194
- test('AiResponseParseError should include raw output', () => {
195
- const error = new AiResponseParseError({
196
- message: 'Failed to parse response',
197
- rawOutput: 'Some raw output',
198
- })
199
- expect(error.message).toBe('Failed to parse response')
200
- expect(error.rawOutput).toBe('Some raw output')
201
- })
202
-
203
- test('AiServiceError should have message and optional cause', () => {
204
- const cause = new Error('Original error')
205
- const error = new AiServiceError({
206
- message: 'Service failed',
207
- cause,
208
- })
209
- expect(error.message).toBe('Service failed')
210
- expect(error.cause).toBe(cause)
211
- })
212
- })
213
-
214
- describe('detectAiTool', () => {
215
- test('should detect claude tool when available', async () => {
216
- // Mock which command to return success for claude
217
- const mockExecAsync = mock(() =>
218
- Promise.resolve({ stdout: '/usr/local/bin/claude\n', stderr: '' }),
219
- )
220
-
221
- const mockAiService = AiService.of({
222
- detectAiTool: () => Effect.succeed('claude'),
223
- extractResponseTag: (output: string) => Effect.succeed(output),
224
- runPrompt: () => Effect.succeed('mock response'),
225
- })
226
-
227
- const result = await Effect.runPromise(
228
- Effect.gen(function* () {
229
- const service = yield* AiService
230
- return yield* service.detectAiTool()
231
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
232
- )
233
-
234
- expect(result).toBe('claude')
235
- })
236
-
237
- test('should detect llm tool when claude not available', async () => {
238
- const mockAiService = AiService.of({
239
- detectAiTool: () => Effect.succeed('llm'),
240
- extractResponseTag: (output: string) => Effect.succeed(output),
241
- runPrompt: () => Effect.succeed('mock response'),
242
- })
243
-
244
- const result = await Effect.runPromise(
245
- Effect.gen(function* () {
246
- const service = yield* AiService
247
- return yield* service.detectAiTool()
248
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
249
- )
250
-
251
- expect(result).toBe('llm')
252
- })
253
-
254
- test('should detect opencode tool when others not available', async () => {
255
- const mockAiService = AiService.of({
256
- detectAiTool: () => Effect.succeed('opencode'),
257
- extractResponseTag: (output: string) => Effect.succeed(output),
258
- runPrompt: () => Effect.succeed('mock response'),
259
- })
260
-
261
- const result = await Effect.runPromise(
262
- Effect.gen(function* () {
263
- const service = yield* AiService
264
- return yield* service.detectAiTool()
265
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
266
- )
267
-
268
- expect(result).toBe('opencode')
269
- })
270
-
271
- test('should detect gemini tool when others not available', async () => {
272
- const mockAiService = AiService.of({
273
- detectAiTool: () => Effect.succeed('gemini'),
274
- extractResponseTag: (output: string) => Effect.succeed(output),
275
- runPrompt: () => Effect.succeed('mock response'),
276
- })
277
-
278
- const result = await Effect.runPromise(
279
- Effect.gen(function* () {
280
- const service = yield* AiService
281
- return yield* service.detectAiTool()
282
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
283
- )
284
-
285
- expect(result).toBe('gemini')
286
- })
287
-
288
- test('should fail when no AI tools are available', async () => {
289
- const mockAiService = AiService.of({
290
- detectAiTool: () =>
291
- Effect.fail(
292
- new NoAiToolFoundError({
293
- message: 'No AI tool found. Please install claude, llm, opencode, or gemini CLI.',
294
- }),
295
- ),
296
- extractResponseTag: (output: string) => Effect.succeed(output),
297
- runPrompt: () => Effect.succeed('mock response'),
298
- })
299
-
300
- const result = await Effect.runPromise(
301
- Effect.either(
302
- Effect.gen(function* () {
303
- const service = yield* AiService
304
- return yield* service.detectAiTool()
305
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
306
- ),
307
- )
308
-
309
- expect(result._tag).toBe('Left')
310
- if (result._tag === 'Left') {
311
- expect(result.left).toBeInstanceOf(NoAiToolFoundError)
312
- expect((result.left as NoAiToolFoundError).message).toContain('No AI tool found')
313
- }
314
- })
315
- })
316
-
317
- describe('runPrompt', () => {
318
- test('should successfully run prompt with claude', async () => {
319
- const mockAiService = AiService.of({
320
- detectAiTool: () => Effect.succeed('claude'),
321
- extractResponseTag: (output: string) => Effect.succeed('extracted response'),
322
- runPrompt: (prompt: string, input: string) => {
323
- expect(prompt).toBe('Test prompt')
324
- expect(input).toBe('Test input')
325
- return Effect.succeed('extracted response')
326
- },
327
- })
328
-
329
- const result = await Effect.runPromise(
330
- Effect.gen(function* () {
331
- const service = yield* AiService
332
- return yield* service.runPrompt('Test prompt', 'Test input')
333
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
334
- )
335
-
336
- expect(result).toBe('extracted response')
337
- })
338
-
339
- test('should successfully run prompt with llm', async () => {
340
- const mockAiService = AiService.of({
341
- detectAiTool: () => Effect.succeed('llm'),
342
- extractResponseTag: (output: string) => Effect.succeed('llm response'),
343
- runPrompt: (prompt: string, input: string) => {
344
- return Effect.succeed('llm response')
345
- },
346
- })
347
-
348
- const result = await Effect.runPromise(
349
- Effect.gen(function* () {
350
- const service = yield* AiService
351
- return yield* service.runPrompt('Test prompt', 'Test input')
352
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
353
- )
354
-
355
- expect(result).toBe('llm response')
356
- })
357
-
358
- test('should handle AI tool execution failure', async () => {
359
- const mockAiService = AiService.of({
360
- detectAiTool: () => Effect.succeed('claude'),
361
- extractResponseTag: (output: string) => Effect.succeed(output),
362
- runPrompt: () =>
363
- Effect.fail(
364
- new AiServiceError({
365
- message: 'Failed to run AI tool: Command not found',
366
- cause: new Error('ENOENT'),
367
- }),
368
- ),
369
- })
370
-
371
- const result = await Effect.runPromise(
372
- Effect.either(
373
- Effect.gen(function* () {
374
- const service = yield* AiService
375
- return yield* service.runPrompt('Test prompt', 'Test input')
376
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
377
- ),
378
- )
379
-
380
- expect(result._tag).toBe('Left')
381
- if (result._tag === 'Left') {
382
- expect(result.left).toBeInstanceOf(AiServiceError)
383
- expect((result.left as AiServiceError).message).toContain('Failed to run AI tool')
384
- }
385
- })
386
-
387
- test('should handle missing response tag in AI output', async () => {
388
- const mockAiService = AiService.of({
389
- detectAiTool: () => Effect.succeed('claude'),
390
- extractResponseTag: (output: string) =>
391
- Effect.fail(
392
- new AiResponseParseError({
393
- message: 'No <response> tag found in AI output',
394
- rawOutput: 'Raw AI output without response tags',
395
- }),
396
- ),
397
- runPrompt: () =>
398
- Effect.fail(
399
- new AiResponseParseError({
400
- message: 'No <response> tag found in AI output',
401
- rawOutput: 'Raw AI output without response tags',
402
- }),
403
- ),
404
- })
405
-
406
- const result = await Effect.runPromise(
407
- Effect.either(
408
- Effect.gen(function* () {
409
- const service = yield* AiService
410
- return yield* service.runPrompt('Test prompt', 'Test input')
411
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
412
- ),
413
- )
414
-
415
- expect(result._tag).toBe('Left')
416
- if (result._tag === 'Left') {
417
- expect(result.left).toBeInstanceOf(AiResponseParseError)
418
- expect((result.left as AiResponseParseError).rawOutput).toBe(
419
- 'Raw AI output without response tags',
420
- )
421
- }
422
- })
423
-
424
- test('should handle no AI tool found during prompt execution', async () => {
425
- const mockAiService = AiService.of({
426
- detectAiTool: () =>
427
- Effect.fail(
428
- new NoAiToolFoundError({
429
- message: 'No AI tool found',
430
- }),
431
- ),
432
- extractResponseTag: (output: string) => Effect.succeed(output),
433
- runPrompt: () =>
434
- Effect.fail(
435
- new NoAiToolFoundError({
436
- message: 'No AI tool found',
437
- }),
438
- ),
439
- })
440
-
441
- const result = await Effect.runPromise(
442
- Effect.either(
443
- Effect.gen(function* () {
444
- const service = yield* AiService
445
- return yield* service.runPrompt('Test prompt', 'Test input')
446
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
447
- ),
448
- )
449
-
450
- expect(result._tag).toBe('Left')
451
- if (result._tag === 'Left') {
452
- expect(result.left).toBeInstanceOf(NoAiToolFoundError)
453
- }
454
- })
455
-
456
- test('should format input correctly for AI tool', async () => {
457
- const mockAiService = AiService.of({
458
- detectAiTool: () => Effect.succeed('claude'),
459
- extractResponseTag: (output: string) => Effect.succeed('response content'),
460
- runPrompt: (prompt: string, input: string) => {
461
- // Verify the prompt and input are passed correctly
462
- expect(prompt).toBe('System: Analyze this code')
463
- expect(input).toBe('function test() { return 42; }')
464
- return Effect.succeed('response content')
465
- },
466
- })
467
-
468
- const result = await Effect.runPromise(
469
- Effect.gen(function* () {
470
- const service = yield* AiService
471
- return yield* service.runPrompt(
472
- 'System: Analyze this code',
473
- 'function test() { return 42; }',
474
- )
475
- }).pipe(Effect.provide(Layer.succeed(AiService, mockAiService))),
476
- )
477
-
478
- expect(result).toBe('response content')
479
- })
480
- })
481
-
482
- describe('AiServiceLive integration', () => {
483
- test('should be able to create live service layer', () => {
484
- // Test that the live service layer can be created without errors
485
- expect(AiServiceLive).toBeDefined()
486
- expect(typeof AiServiceLive).toBe('object')
487
- })
488
- })
489
- })