@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 +1 -1
- package/src/cli/commands/comment.ts +2 -2
- package/src/cli/commands/review.ts +2 -2
- package/src/cli/commands/setup.ts +110 -55
- package/src/cli/index.ts +7 -4
- package/src/prompts/system-inline-review.md +26 -11
- package/src/prompts/system-overall-review.md +11 -0
- package/src/services/review-strategy.ts +10 -5
- package/tests/comment.test.ts +2 -1
- package/tests/unit/services/review-strategy.test.ts +12 -157
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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({
|
|
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
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
195
|
+
// Get default suggestion
|
|
196
|
+
const defaultCommand =
|
|
197
|
+
existingConfig?.aiTool ||
|
|
198
|
+
(availableTools.includes('claude') ? 'claude' : availableTools[0]) ||
|
|
199
|
+
''
|
|
171
200
|
|
|
172
|
-
|
|
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('--
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
414
|
+
tool: options.tool,
|
|
412
415
|
systemPrompt: options.systemPrompt,
|
|
413
416
|
}).pipe(
|
|
414
417
|
Effect.provide(ReviewStrategyServiceLive),
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
##
|
|
1
|
+
## IMMEDIATE TASK: ANALYZE CODE AND GENERATE INLINE COMMENTS
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
99
|
+
## FINAL TASK INSTRUCTION
|
|
89
100
|
|
|
90
|
-
**
|
|
101
|
+
**ANALYZE THE CODE CHANGES NOW AND OUTPUT YOUR INLINE COMMENTS IMMEDIATELY.**
|
|
91
102
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
201
|
+
return extractResponse(result.stdout)
|
|
197
202
|
}),
|
|
198
203
|
}
|
|
199
204
|
|
package/tests/comment.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|