@aaronshaf/ger 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -443,9 +443,9 @@ const formatHumanOutput = (
443
443
  console.log(`File: ${options.file}, Line: ${options.line}`)
444
444
  console.log(`Message: ${options.message}`)
445
445
  if (options.unresolved) console.log(`Status: Unresolved`)
446
- } else {
447
- console.log(`Message: ${review.message}`)
448
446
  }
447
+ // Note: For overall review messages, we don't display the content here
448
+ // since it was already shown in the "OVERALL REVIEW TO POST" section
449
449
  })
450
450
 
451
451
  // Main output formatter
@@ -89,7 +89,7 @@ interface ReviewOptions {
89
89
  comment?: boolean
90
90
  yes?: boolean
91
91
  prompt?: string
92
- provider?: string
92
+ tool?: string
93
93
  systemPrompt?: string
94
94
  }
95
95
 
@@ -336,7 +336,7 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
336
336
  }
337
337
 
338
338
  // Select strategy based on user preference
339
- const selectedStrategy = yield* reviewStrategy.selectStrategy(options.provider)
339
+ const selectedStrategy = yield* reviewStrategy.selectStrategy(options.tool)
340
340
  yield* Console.log(`✓ Using AI tool: ${selectedStrategy.name}`)
341
341
 
342
342
  // Load custom review prompt if provided
@@ -66,14 +66,54 @@ const verifyCredentials = (credentials: GerritCredentials) =>
66
66
  },
67
67
  catch: (error) => {
68
68
  if (error instanceof Error) {
69
+ // Authentication/permission errors
69
70
  if (error.message.includes('401')) {
70
71
  return new ConfigError({
71
72
  message: 'Invalid credentials. Please check your username and password.',
72
73
  })
73
74
  }
75
+ if (error.message.includes('403')) {
76
+ return new ConfigError({
77
+ message: 'Access denied. Please verify your credentials and server permissions.',
78
+ })
79
+ }
80
+
81
+ // Network/hostname errors
74
82
  if (error.message.includes('ENOTFOUND')) {
75
- return new ConfigError({ message: 'Could not connect to Gerrit. Please check the URL.' })
83
+ return new ConfigError({
84
+ message: `Hostname not found. Please check that the Gerrit URL is correct.\nExample: https://gerrit.example.com (without /a/ or paths)`,
85
+ })
86
+ }
87
+ if (error.message.includes('ECONNREFUSED')) {
88
+ return new ConfigError({
89
+ message: `Connection refused. The server may be down or the port may be incorrect.\nPlease verify the URL and try again.`,
90
+ })
76
91
  }
92
+ if (error.message.includes('ETIMEDOUT')) {
93
+ return new ConfigError({
94
+ message: `Connection timed out. Please check:\n• Your internet connection\n• The Gerrit server URL\n• Any firewall or VPN settings`,
95
+ })
96
+ }
97
+ if (error.message.includes('certificate') || error.message.includes('SSL')) {
98
+ return new ConfigError({
99
+ message: `SSL/Certificate error. Please ensure the URL uses HTTPS and the certificate is valid.`,
100
+ })
101
+ }
102
+
103
+ // URL format errors
104
+ if (error.message.includes('Invalid URL') || error.message.includes('fetch failed')) {
105
+ return new ConfigError({
106
+ message: `Invalid URL format. Please use the full URL including https://\nExample: https://gerrit.example.com`,
107
+ })
108
+ }
109
+
110
+ // Generic network errors
111
+ if (error.message.includes('network') || error.message.includes('fetch')) {
112
+ return new ConfigError({
113
+ message: `Network error: ${error.message}\nPlease check your connection and the Gerrit server URL.`,
114
+ })
115
+ }
116
+
77
117
  return new ConfigError({ message: error.message })
78
118
  }
79
119
  return new ConfigError({ message: 'Unknown error occurred' })
@@ -112,64 +152,82 @@ const setupEffect = (configService: ConfigServiceImpl) =>
112
152
  try: async () => {
113
153
  console.log('')
114
154
 
115
- // Gerrit Host URL
116
- const host = await input({
117
- message: 'Gerrit Host URL (e.g., https://gerrit.example.com)',
118
- default: existingConfig?.host,
119
- })
155
+ // Enable raw mode for proper password masking
156
+ const wasRawMode = process.stdin.isRaw
157
+ if (process.stdin.isTTY && !wasRawMode) {
158
+ process.stdin.setRawMode(true)
159
+ }
120
160
 
121
- // Username
122
- const username = await input({
123
- message: 'Username (your Gerrit username)',
124
- default: existingConfig?.username,
125
- })
161
+ try {
162
+ // Gerrit Host URL
163
+ const host = await input({
164
+ message: 'Gerrit Host URL (e.g., https://gerrit.example.com)',
165
+ default: existingConfig?.host,
166
+ })
126
167
 
127
- // Password - similar to ji's pattern
128
- const passwordValue =
129
- (await password({
130
- message: 'HTTP Password (generated from Gerrit settings)',
131
- })) ||
132
- existingConfig?.password ||
133
- ''
168
+ // Username
169
+ const username = await input({
170
+ message: 'Username (your Gerrit username)',
171
+ default: existingConfig?.username,
172
+ })
134
173
 
135
- console.log('')
136
- console.log(chalk.yellow('Optional: AI Configuration'))
174
+ // Password - with proper masking and visual feedback
175
+ const passwordMessage = existingConfig?.password
176
+ ? `HTTP Password (generated from Gerrit settings) ${chalk.dim('(press Enter to keep existing)')}`
177
+ : 'HTTP Password (generated from Gerrit settings)'
137
178
 
138
- // Show detected AI tools
139
- if (availableTools.length > 0) {
140
- console.log(chalk.dim(`Detected AI tools: ${availableTools.join(', ')}`))
141
- }
179
+ const passwordValue =
180
+ (await password({
181
+ message: passwordMessage,
182
+ mask: true, // Show * characters as user types
183
+ })) ||
184
+ existingConfig?.password ||
185
+ ''
142
186
 
143
- // Get default suggestion
144
- const defaultCommand =
145
- existingConfig?.aiTool ||
146
- (availableTools.includes('claude') ? 'claude' : availableTools[0]) ||
147
- ''
148
-
149
- // AI tool command with smart default
150
- const aiToolCommand = await input({
151
- message:
152
- availableTools.length > 0
153
- ? 'AI tool command (detected from system)'
154
- : 'AI tool command (e.g., claude, llm, opencode, gemini)',
155
- default: defaultCommand || 'claude',
156
- })
187
+ console.log('')
188
+ console.log(chalk.yellow('Optional: AI Configuration'))
157
189
 
158
- // Build flat config
159
- const configData = {
160
- host: host.trim().replace(/\/$/, ''), // Remove trailing slash
161
- username: username.trim(),
162
- password: passwordValue,
163
- ...(aiToolCommand && {
164
- aiTool: aiToolCommand,
165
- }),
166
- aiAutoDetect: !aiToolCommand,
167
- }
190
+ // Show detected AI tools
191
+ if (availableTools.length > 0) {
192
+ console.log(chalk.dim(`Detected AI tools: ${availableTools.join(', ')}`))
193
+ }
168
194
 
169
- // Validate config using Schema instead of type assertion
170
- const fullConfig = Schema.decodeUnknownSync(AppConfig)(configData)
195
+ // Get default suggestion
196
+ const defaultCommand =
197
+ existingConfig?.aiTool ||
198
+ (availableTools.includes('claude') ? 'claude' : availableTools[0]) ||
199
+ ''
171
200
 
172
- return fullConfig
201
+ // AI tool command with smart default
202
+ const aiToolCommand = await input({
203
+ message:
204
+ availableTools.length > 0
205
+ ? 'AI tool command (detected from system)'
206
+ : 'AI tool command (e.g., claude, llm, opencode, gemini)',
207
+ default: defaultCommand || 'claude',
208
+ })
209
+
210
+ // Build flat config
211
+ const configData = {
212
+ host: host.trim().replace(/\/$/, ''), // Remove trailing slash
213
+ username: username.trim(),
214
+ password: passwordValue,
215
+ ...(aiToolCommand && {
216
+ aiTool: aiToolCommand,
217
+ }),
218
+ aiAutoDetect: !aiToolCommand,
219
+ }
220
+
221
+ // Validate config using Schema instead of type assertion
222
+ const fullConfig = Schema.decodeUnknownSync(AppConfig)(configData)
223
+
224
+ return fullConfig
225
+ } finally {
226
+ // Restore raw mode state
227
+ if (process.stdin.isTTY && !wasRawMode) {
228
+ process.stdin.setRawMode(false)
229
+ }
230
+ }
173
231
  },
174
232
  catch: (error) => {
175
233
  if (error instanceof Error && error.message.includes('User force closed')) {
@@ -205,11 +263,8 @@ const setupEffect = (configService: ConfigServiceImpl) =>
205
263
  Effect.catchAll((error) =>
206
264
  pipe(
207
265
  Console.error(
208
- chalk.red(
209
- `\nAuthentication failed: ${error instanceof ConfigError ? error.message : 'Unknown error'}`,
210
- ),
266
+ chalk.red(`\n${error instanceof ConfigError ? error.message : `Setup failed: ${error}`}`),
211
267
  ),
212
- Effect.flatMap(() => Console.error('Please check your credentials and try again.')),
213
268
  Effect.flatMap(() => Effect.fail(error)),
214
269
  ),
215
270
  ),
package/src/cli/index.ts CHANGED
@@ -368,12 +368,12 @@ program
368
368
  .option('-y, --yes', 'Skip confirmation prompts when posting comments')
369
369
  .option('--debug', 'Show debug output including AI responses')
370
370
  .option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
371
- .option('--provider <provider>', 'Preferred AI provider (claude-sdk, claude, gemini, opencode)')
371
+ .option('--tool <tool>', 'Preferred AI tool (claude, gemini, opencode)')
372
372
  .option('--system-prompt <prompt>', 'Custom system prompt for the AI')
373
373
  .addHelpText(
374
374
  'after',
375
375
  `
376
- This command uses AI (Claude SDK, claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
376
+ This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
377
377
  It performs a two-stage review process:
378
378
 
379
379
  1. Generates inline comments for specific code issues
@@ -384,7 +384,7 @@ Use --comment to post the review to Gerrit (with confirmation prompts).
384
384
  Use --comment --yes to post without confirmation.
385
385
 
386
386
  Requirements:
387
- - One of these AI tools must be available: Claude SDK (ANTHROPIC_API_KEY), claude CLI, gemini CLI, or opencode CLI
387
+ - One of these AI tools must be available: claude CLI, gemini CLI, or opencode CLI
388
388
  - Gerrit credentials must be configured (run 'ger setup' first)
389
389
 
390
390
  Examples:
@@ -397,6 +397,9 @@ Examples:
397
397
  # Review and auto-post comments without prompting
398
398
  $ ger review 12345 --comment --yes
399
399
 
400
+ # Use specific AI tool
401
+ $ ger review 12345 --tool gemini
402
+
400
403
  # Show debug output to troubleshoot issues
401
404
  $ ger review 12345 --debug
402
405
  `,
@@ -408,7 +411,7 @@ Examples:
408
411
  yes: options.yes,
409
412
  debug: options.debug,
410
413
  prompt: options.prompt,
411
- provider: options.provider,
414
+ tool: options.tool,
412
415
  systemPrompt: options.systemPrompt,
413
416
  }).pipe(
414
417
  Effect.provide(ReviewStrategyServiceLive),
@@ -1,9 +1,15 @@
1
- ## CRITICAL OUTPUT REQUIREMENT
1
+ ## IMMEDIATE TASK: ANALYZE CODE AND GENERATE INLINE COMMENTS
2
2
 
3
- **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
4
- **NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors.**
3
+ **YOU MUST ANALYZE THE PROVIDED CODE CHANGES RIGHT NOW AND GENERATE INLINE COMMENTS.**
5
4
 
6
- Output ONLY a JSON array wrapped in response tags. **EMPTY ARRAY IS PERFECTLY VALID** for clean code without issues. No other text before or after the tags.
5
+ **CRITICAL OUTPUT REQUIREMENT:**
6
+ - YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS
7
+ - NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors
8
+ - Output ONLY a JSON array wrapped in response tags
9
+ - **EMPTY ARRAY IS PERFECTLY VALID** for clean code without issues
10
+ - No other text before or after the tags
11
+
12
+ **START YOUR ANALYSIS NOW. DO NOT ASK QUESTIONS. DO NOT WAIT FOR MORE INPUT.**
7
13
 
8
14
  ## JSON Structure for Inline Comments
9
15
 
@@ -21,12 +27,17 @@ The JSON array must contain inline comment objects with these fields:
21
27
  - "start_character": Optional column start (integer)
22
28
  - "end_character": Optional column end (integer)
23
29
 
24
- **IMPORTANT**: Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
30
+ **CRITICAL LINE NUMBER RULES:**
31
+ 1. **ALWAYS use final file line numbers, NEVER diff line numbers**
32
+ 2. Line numbers must match the NEW version of the file after all changes
33
+ 3. Use `git show HEAD:path/to/file` or examine the final file to get correct line numbers
34
+ 4. If you see "+50" in a diff, the actual line number is NOT 50 - check the final file
35
+ 5. Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
25
36
 
26
37
  ### Optional Fields
27
38
  - "side": "REVISION" (new code, default) or "PARENT" (original code)
28
39
 
29
- Line numbers refer to the final file (REVISION), not the diff.
40
+ **VERIFICATION STEP**: Before adding any comment, verify the line number by checking the final file content to ensure your line number points to the exact code you're commenting on.
30
41
 
31
42
  ## Comment Quality Guidelines
32
43
 
@@ -85,11 +96,11 @@ You are running in a git repository with full access to:
85
96
  - All project files for architectural understanding
86
97
  - Use these commands to provide comprehensive, accurate reviews
87
98
 
88
- ## FINAL REMINDER
99
+ ## FINAL TASK INSTRUCTION
89
100
 
90
- **CRITICAL: Your ENTIRE output must be a JSON array wrapped in <response></response> tags.**
101
+ **ANALYZE THE CODE CHANGES NOW AND OUTPUT YOUR INLINE COMMENTS IMMEDIATELY.**
91
102
 
92
- Example formats:
103
+ Your output format must be:
93
104
  ```
94
105
  <response>
95
106
  []
@@ -97,6 +108,8 @@ Example formats:
97
108
  ```
98
109
  (Empty array for clean code - this is GOOD!)
99
110
 
111
+ OR:
112
+
100
113
  ```
101
114
  <response>
102
115
  [{"file": "auth.js", "line": 42, "message": "🤖 SQL injection vulnerability: query uses string concatenation"}]
@@ -104,10 +117,12 @@ Example formats:
104
117
  ```
105
118
  (Only comment on real problems)
106
119
 
107
- **REQUIREMENTS**:
120
+ **CRITICAL REQUIREMENTS**:
108
121
  - Every message must start with "🤖 "
109
122
  - Never use backticks in your response
110
123
  - Empty arrays are encouraged for clean code
111
124
  - Focus on bugs, security, crashes - ignore style preferences
112
125
  - Use git commands to understand context before commenting
113
- - NO TEXT OUTSIDE THE <response></response> TAGS
126
+ - NO TEXT OUTSIDE THE <response></response> TAGS
127
+
128
+ **DO YOUR ANALYSIS NOW. STOP ASKING QUESTIONS. GENERATE THE REVIEW.**
@@ -41,11 +41,16 @@ Gerrit uses a LIMITED markdown subset. Follow these rules EXACTLY:
41
41
 
42
42
  **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
43
43
 
44
+ **IMMEDIATE TASK**: Analyze the code changes provided below and write a comprehensive engineering review.
45
+
44
46
  Start with "🤖 [Your Tool Name] ([Your Model])" then provide a **CONCISE** engineering assessment. Examples:
45
47
  - If you are Claude Sonnet 4: "🤖 Claude (Sonnet 4)"
48
+ - If you are Gemini: "🤖 Gemini (1.5 Pro)" or "🤖 Gemini (1.5 Flash)"
46
49
  - For clean code: "No significant issues found. Change is ready for merge."
47
50
  - For problematic code: Focus only on critical/important issues, skip minor concerns
48
51
 
52
+ **YOU MUST ANALYZE THE PROVIDED CODE CHANGES AND WRITE A REVIEW NOW.**
53
+
49
54
  ## Example Output Format
50
55
 
51
56
  <response>
@@ -175,3 +180,9 @@ CRITICAL FORMATTING RULES:
175
180
  - Use exactly 4 spaces to start each line of code blocks
176
181
  - Keep code blocks simple and readable
177
182
  - Add proper spacing for readability
183
+
184
+ ## TASK SUMMARY
185
+
186
+ **ANALYZE THE CODE CHANGES PROVIDED ABOVE AND WRITE YOUR ENGINEERING REVIEW IMMEDIATELY.**
187
+
188
+ Do not ask for clarification. Do not wait for more input. Analyze the provided code changes, git history, and changed files, then write your review following the format requirements above.
@@ -4,6 +4,13 @@ import { promisify } from 'node:util'
4
4
 
5
5
  const execAsync = promisify(exec)
6
6
 
7
+ // Shared response extraction logic for all AI tools
8
+ const extractResponse = (stdout: string): string => {
9
+ // Extract response from <response> tags or use full output
10
+ const responseMatch = stdout.match(/<response>([\s\S]*?)<\/response>/i)
11
+ return responseMatch ? responseMatch[1].trim() : stdout.trim()
12
+ }
13
+
7
14
  // Simple strategy focused only on review needs
8
15
  export class ReviewStrategyError extends Data.TaggedError('ReviewStrategyError')<{
9
16
  message: string
@@ -75,9 +82,7 @@ export const claudeCliStrategy: ReviewStrategy = {
75
82
  }),
76
83
  })
77
84
 
78
- // Extract response from <response> tags or use full output
79
- const responseMatch = result.stdout.match(/<response>([\s\S]*?)<\/response>/i)
80
- return responseMatch ? responseMatch[1].trim() : result.stdout.trim()
85
+ return extractResponse(result.stdout)
81
86
  }),
82
87
  }
83
88
 
@@ -135,7 +140,7 @@ export const geminiCliStrategy: ReviewStrategy = {
135
140
  }),
136
141
  })
137
142
 
138
- return result.stdout.trim()
143
+ return extractResponse(result.stdout)
139
144
  }),
140
145
  }
141
146
 
@@ -193,7 +198,7 @@ export const openCodeCliStrategy: ReviewStrategy = {
193
198
  }),
194
199
  })
195
200
 
196
- return result.stdout.trim()
201
+ return extractResponse(result.stdout)
197
202
  }),
198
203
  }
199
204
 
@@ -556,7 +556,8 @@ describe('comment command', () => {
556
556
 
557
557
  const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
558
558
  expect(output).toContain('✓ Comment posted successfully!')
559
- expect(output).toContain('Message: Piped comment message')
559
+ // Note: The message content is no longer displayed after successful posting
560
+ // to avoid duplication with the review preview section
560
561
 
561
562
  // Restore process.stdin
562
563
  Object.defineProperty(process, 'stdin', {
@@ -7,11 +7,6 @@ interface MockDeps {
7
7
  spawn: (command: string, options: any) => any
8
8
  }
9
9
 
10
- // Mock for Claude SDK
11
- interface MockClaudeSDK {
12
- query: any // Will be a Bun mock function
13
- }
14
-
15
10
  // Test implementation that mirrors the real strategy structure
16
11
  const createTestStrategy = (name: string, command: string, flags: string[], deps: MockDeps) => ({
17
12
  name,
@@ -74,47 +69,6 @@ const createTestStrategy = (name: string, command: string, flags: string[], deps
74
69
  }),
75
70
  })
76
71
 
77
- // Test implementation for Claude SDK strategy
78
- const createSDKStrategy = (
79
- name: string,
80
- deps: { sdk: MockClaudeSDK | null; hasApiKey: boolean },
81
- ) => ({
82
- name,
83
- isAvailable: () =>
84
- Effect.gen(function* () {
85
- return Boolean(deps.sdk && deps.hasApiKey)
86
- }),
87
- executeReview: (prompt: string, options: { cwd?: string; systemPrompt?: string } = {}) =>
88
- Effect.gen(function* () {
89
- if (!deps.sdk) {
90
- return yield* Effect.fail(new Error(`${name} not available`))
91
- }
92
-
93
- const result = yield* Effect.tryPromise({
94
- try: async () => {
95
- for await (const message of deps.sdk!.query({
96
- prompt,
97
- options: {
98
- maxTurns: 3,
99
- customSystemPrompt: options.systemPrompt || 'You are a code review expert.',
100
- allowedTools: ['Read', 'Grep', 'Glob'],
101
- cwd: options.cwd,
102
- },
103
- })) {
104
- if (message.type === 'result' && message.subtype === 'success' && message.result) {
105
- return message.result
106
- }
107
- }
108
- throw new Error('No result received')
109
- },
110
- catch: (error) =>
111
- new Error(`${name} failed: ${error instanceof Error ? error.message : String(error)}`),
112
- })
113
-
114
- return result
115
- }),
116
- })
117
-
118
72
  describe('Review Strategy', () => {
119
73
  let mockExecAsync: any
120
74
  let mockSpawn: any
@@ -266,6 +220,14 @@ describe('Review Strategy', () => {
266
220
  expect(response).toBe('Gemini response')
267
221
  expect(mockSpawn).toHaveBeenCalledWith('gemini -p', expect.any(Object))
268
222
  })
223
+
224
+ it('should extract response from tags', async () => {
225
+ setupSuccessfulExecution('<response>Gemini tagged content</response>')
226
+
227
+ const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
228
+
229
+ expect(response).toBe('Gemini tagged content')
230
+ })
269
231
  })
270
232
 
271
233
  describe('OpenCode CLI Strategy', () => {
@@ -295,120 +257,13 @@ describe('Review Strategy', () => {
295
257
  expect(response).toBe('OpenCode response')
296
258
  expect(mockSpawn).toHaveBeenCalledWith('opencode -p', expect.any(Object))
297
259
  })
298
- })
299
260
 
300
- describe('Claude SDK Strategy', () => {
301
- let mockSDK: MockClaudeSDK
302
- let claudeSDKStrategy: any
303
-
304
- beforeEach(() => {
305
- mockSDK = {
306
- query: mock(),
307
- }
308
-
309
- claudeSDKStrategy = createSDKStrategy('Claude SDK', {
310
- sdk: mockSDK,
311
- hasApiKey: true,
312
- })
313
- })
314
-
315
- it('should check availability when SDK and API key are present', async () => {
316
- const available = await Effect.runPromise(claudeSDKStrategy.isAvailable())
317
-
318
- expect(available).toBe(true)
319
- })
320
-
321
- it('should check availability when SDK is missing', async () => {
322
- const strategy = createSDKStrategy('Claude SDK', {
323
- sdk: null,
324
- hasApiKey: true,
325
- })
326
-
327
- const available = await Effect.runPromise(strategy.isAvailable())
328
-
329
- expect(available).toBe(false)
330
- })
331
-
332
- it('should check availability when API key is missing', async () => {
333
- const strategy = createSDKStrategy('Claude SDK', {
334
- sdk: mockSDK,
335
- hasApiKey: false,
336
- })
337
-
338
- const available = await Effect.runPromise(strategy.isAvailable())
339
-
340
- expect(available).toBe(false)
341
- })
342
-
343
- it('should execute review successfully', async () => {
344
- mockSDK.query = mock(async function* () {
345
- yield { type: 'message', content: 'Thinking...' }
346
- yield { type: 'result', subtype: 'success', result: 'Claude SDK response' }
347
- })
348
-
349
- const response = await Effect.runPromise(
350
- claudeSDKStrategy.executeReview('Test prompt', { cwd: '/tmp' }),
351
- )
352
-
353
- expect(response).toBe('Claude SDK response')
354
- expect(mockSDK.query).toHaveBeenCalledWith({
355
- prompt: 'Test prompt',
356
- options: {
357
- maxTurns: 3,
358
- customSystemPrompt: 'You are a code review expert.',
359
- allowedTools: ['Read', 'Grep', 'Glob'],
360
- cwd: '/tmp',
361
- },
362
- })
363
- })
364
-
365
- it('should use custom system prompt', async () => {
366
- mockSDK.query = mock(async function* () {
367
- yield { type: 'result', subtype: 'success', result: 'Custom prompt response' }
368
- })
369
-
370
- const response = await Effect.runPromise(
371
- claudeSDKStrategy.executeReview('Test prompt', {
372
- systemPrompt: 'Custom review prompt',
373
- }),
374
- )
375
-
376
- expect(response).toBe('Custom prompt response')
377
- expect(mockSDK.query).toHaveBeenCalledWith({
378
- prompt: 'Test prompt',
379
- options: {
380
- maxTurns: 3,
381
- customSystemPrompt: 'Custom review prompt',
382
- allowedTools: ['Read', 'Grep', 'Glob'],
383
- cwd: undefined,
384
- },
385
- })
386
- })
387
-
388
- it('should handle SDK failure', async () => {
389
- mockSDK.query = mock(async function* () {
390
- throw new Error('SDK error')
391
- })
392
-
393
- try {
394
- await Effect.runPromise(claudeSDKStrategy.executeReview('Test prompt'))
395
- expect(false).toBe(true) // Should not reach here
396
- } catch (error: any) {
397
- expect(error.message).toContain('Claude SDK failed')
398
- }
399
- })
261
+ it('should extract response from tags', async () => {
262
+ setupSuccessfulExecution('<response>OpenCode tagged content</response>')
400
263
 
401
- it('should handle no result received', async () => {
402
- mockSDK.query = mock(async function* () {
403
- yield { type: 'message', content: 'No result' }
404
- })
264
+ const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
405
265
 
406
- try {
407
- await Effect.runPromise(claudeSDKStrategy.executeReview('Test prompt'))
408
- expect(false).toBe(true) // Should not reach here
409
- } catch (error: any) {
410
- expect(error.message).toContain('No result received')
411
- }
266
+ expect(response).toBe('OpenCode tagged content')
412
267
  })
413
268
  })
414
269