@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,88 @@
|
|
|
1
|
+
## CRITICAL OUTPUT REQUIREMENT
|
|
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.**
|
|
5
|
+
|
|
6
|
+
Output ONLY a JSON array wrapped in response tags. No other text before or after the tags.
|
|
7
|
+
|
|
8
|
+
## JSON Structure for Inline Comments
|
|
9
|
+
|
|
10
|
+
The JSON array must contain inline comment objects with these fields:
|
|
11
|
+
|
|
12
|
+
### Required Fields
|
|
13
|
+
- "file": **Complete file path** as shown in the diff (e.g., "app/controllers/users_controller.rb", not just "users_controller.rb")
|
|
14
|
+
- "message": Your comment text (MUST start with "🤖 ")
|
|
15
|
+
|
|
16
|
+
### Line Specification (MUST use one approach)
|
|
17
|
+
- "line": For single-line comments (integer line number - REQUIRED for single line comments)
|
|
18
|
+
- "range": For multi-line comments (object with):
|
|
19
|
+
- "start_line": First line of the issue (integer)
|
|
20
|
+
- "end_line": Last line of the issue (integer)
|
|
21
|
+
- "start_character": Optional column start (integer)
|
|
22
|
+
- "end_character": Optional column end (integer)
|
|
23
|
+
|
|
24
|
+
**IMPORTANT**: Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
|
|
25
|
+
|
|
26
|
+
### Optional Fields
|
|
27
|
+
- "side": "REVISION" (new code, default) or "PARENT" (original code)
|
|
28
|
+
|
|
29
|
+
Line numbers refer to the final file (REVISION), not the diff.
|
|
30
|
+
|
|
31
|
+
## Comment Quality Guidelines
|
|
32
|
+
|
|
33
|
+
1. **Be Specific**: Reference exact variables, functions, or patterns
|
|
34
|
+
2. **Explain Impact**: What could go wrong and why it matters
|
|
35
|
+
3. **Suggest Fixes**: Provide actionable corrections when possible
|
|
36
|
+
4. **Group Logically**: Use range for related lines, separate comments for distinct issues
|
|
37
|
+
5. **Prioritize**: Comment on significant issues, not style preferences
|
|
38
|
+
|
|
39
|
+
## Example Output Formats
|
|
40
|
+
|
|
41
|
+
### Example 1: Mixed Single and Multi-line Comments
|
|
42
|
+
<response>
|
|
43
|
+
[
|
|
44
|
+
{"file": "app/controllers/auth/validator.rb", "line": 45, "message": "🤖 Missing validation for email format - accepts invalid emails like 'user@'. Use a proper email regex or validation library."},
|
|
45
|
+
{"file": "app/controllers/auth/validator.rb", "line": 67, "message": "🤖 Password strength check allows common passwords. Consider checking against a common password list."},
|
|
46
|
+
{"file": "lib/database/connection.rb", "range": {"start_line": 23, "end_line": 35}, "message": "🤖 Database connection retry logic has exponential backoff but no maximum retry limit. This could retry indefinitely on persistent failures. Add a max retry count."},
|
|
47
|
+
{"file": "app/controllers/api/users_controller.rb", "line": 89, "message": "🤖 SQL injection vulnerability: Query uses string concatenation with userId. Use parameterized queries with ActiveRecord methods.", "side": "REVISION"}
|
|
48
|
+
]
|
|
49
|
+
</response>
|
|
50
|
+
|
|
51
|
+
### Example 2: Critical Security Issues
|
|
52
|
+
<response>
|
|
53
|
+
[
|
|
54
|
+
{"file": "app/middleware/authentication_middleware.rb", "line": 34, "message": "🤖 Authentication bypass: Debug header check allows skipping auth. This MUST be removed before production."},
|
|
55
|
+
{"file": "lib/utils/crypto_helper.rb", "range": {"start_line": 12, "end_line": 18}, "message": "🤖 Weak encryption: MD5 is cryptographically broken. Use bcrypt for password hashing."},
|
|
56
|
+
{"file": "app/controllers/api/files_controller.rb", "line": 156, "message": "🤖 Path traversal vulnerability: User input directly used in file path without sanitization. An attacker could access files outside intended directory using '../'."}
|
|
57
|
+
]
|
|
58
|
+
</response>
|
|
59
|
+
|
|
60
|
+
## Priority Guidelines for Inline Comments
|
|
61
|
+
|
|
62
|
+
### ALWAYS Comment On
|
|
63
|
+
- Security vulnerabilities (injection, auth bypass, data exposure)
|
|
64
|
+
- Data corruption or loss risks
|
|
65
|
+
- Logic errors that produce wrong results
|
|
66
|
+
- Resource leaks (memory, connections, handles)
|
|
67
|
+
- Race conditions and concurrency bugs
|
|
68
|
+
|
|
69
|
+
### USUALLY Comment On
|
|
70
|
+
- Missing error handling for likely failure cases
|
|
71
|
+
- Performance problems (N+1 queries, unbounded loops)
|
|
72
|
+
- Type safety issues and invalid casts
|
|
73
|
+
- Missing input validation
|
|
74
|
+
- Incorrect API usage
|
|
75
|
+
|
|
76
|
+
### RARELY Comment On
|
|
77
|
+
- Style preferences (unless egregious)
|
|
78
|
+
- Minor optimizations without measurement
|
|
79
|
+
- Alternative approaches that are equivalent
|
|
80
|
+
- Issues in unchanged code
|
|
81
|
+
- Formatting (unless it obscures logic)
|
|
82
|
+
|
|
83
|
+
## FINAL REMINDER
|
|
84
|
+
|
|
85
|
+
Your ENTIRE output must be a JSON array wrapped in <response></response> tags.
|
|
86
|
+
Every message must start with "🤖 ".
|
|
87
|
+
Never use backticks in your response.
|
|
88
|
+
Focus on substantial technical issues, not preferences.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
## Review Structure and Formatting
|
|
2
|
+
|
|
3
|
+
### Section Headers (Use Only What's Relevant)
|
|
4
|
+
|
|
5
|
+
Use CAPS for section headers. Include ONLY sections where you have substantive content:
|
|
6
|
+
|
|
7
|
+
- OVERALL ASSESSMENT - High-level verdict and summary
|
|
8
|
+
- CRITICAL ISSUES - Must be fixed before merge
|
|
9
|
+
- SIGNIFICANT CONCERNS - Should be addressed
|
|
10
|
+
- CODE QUALITY - Improvements for maintainability
|
|
11
|
+
- SECURITY ASSESSMENT - Security-specific findings
|
|
12
|
+
- PERFORMANCE ANALYSIS - Performance implications
|
|
13
|
+
- TEST COVERAGE - Testing observations
|
|
14
|
+
- ARCHITECTURE NOTES - Design and pattern feedback
|
|
15
|
+
- RECOMMENDATIONS - Actionable suggestions
|
|
16
|
+
|
|
17
|
+
### Gerrit Formatting Requirements
|
|
18
|
+
|
|
19
|
+
Gerrit uses a LIMITED markdown subset. Follow these rules EXACTLY:
|
|
20
|
+
|
|
21
|
+
- NO markdown bold (**text**) or italic (*text*) - use CAPS for emphasis
|
|
22
|
+
- NO headers with # or ## - use CAPS section titles
|
|
23
|
+
- NO backticks (`) for code - use quotes 'code' or "code" for inline
|
|
24
|
+
- Code blocks: Start EACH line with exactly 4 spaces, add blank lines before and after
|
|
25
|
+
- Bullet points: Use * or - at line start
|
|
26
|
+
- Block quotes: Start line with >
|
|
27
|
+
- Reference files as path/to/file.ext:123 (with line numbers)
|
|
28
|
+
- Always add blank lines between sections for readability
|
|
29
|
+
- Keep code blocks simple and well-spaced
|
|
30
|
+
|
|
31
|
+
### Content Guidelines
|
|
32
|
+
|
|
33
|
+
1. Start with the most important findings
|
|
34
|
+
2. Group related issues together
|
|
35
|
+
3. Be specific with file paths and line numbers
|
|
36
|
+
4. Explain the "why" behind each issue
|
|
37
|
+
5. Provide actionable fixes or alternatives
|
|
38
|
+
6. Use concrete examples when helpful
|
|
39
|
+
|
|
40
|
+
## CRITICAL OUTPUT REQUIREMENT
|
|
41
|
+
|
|
42
|
+
**YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
|
|
43
|
+
|
|
44
|
+
The review content inside the response tags should start with "🤖 [Your Tool Name] ([Your Model])" followed by your analysis. For example:
|
|
45
|
+
- If you are Claude Sonnet 4: "🤖 Claude (Sonnet 4)"
|
|
46
|
+
- If you are GPT-4: "🤖 OpenAI (GPT-4)"
|
|
47
|
+
- If you are Llama: "🤖 Llama (70B)"
|
|
48
|
+
- etc.
|
|
49
|
+
|
|
50
|
+
## Example Output Format
|
|
51
|
+
|
|
52
|
+
<response>
|
|
53
|
+
🤖 Claude (Sonnet 4)
|
|
54
|
+
|
|
55
|
+
OVERALL ASSESSMENT
|
|
56
|
+
|
|
57
|
+
This change successfully implements the new authentication flow with proper error handling and test coverage. However, there are critical security concerns and performance issues that need addressing before merge.
|
|
58
|
+
|
|
59
|
+
CRITICAL ISSUES
|
|
60
|
+
|
|
61
|
+
1. SQL Injection Vulnerability - src/api/users.ts:45
|
|
62
|
+
|
|
63
|
+
The query construction uses string concatenation with user input:
|
|
64
|
+
|
|
65
|
+
const query = "SELECT * FROM users WHERE id = " + userId
|
|
66
|
+
|
|
67
|
+
This allows SQL injection attacks. Use parameterized queries:
|
|
68
|
+
|
|
69
|
+
const query = "SELECT * FROM users WHERE id = $1"
|
|
70
|
+
const result = await db.query(query, [userId])
|
|
71
|
+
|
|
72
|
+
2. Authentication Bypass - src/middleware/auth.ts:78-82
|
|
73
|
+
|
|
74
|
+
The token validation can be bypassed when 'debug' header is present:
|
|
75
|
+
|
|
76
|
+
if (req.headers.debug) return next()
|
|
77
|
+
|
|
78
|
+
This MUST be removed from production code.
|
|
79
|
+
|
|
80
|
+
SIGNIFICANT CONCERNS
|
|
81
|
+
|
|
82
|
+
Resource Leak - src/services/cache.ts:156
|
|
83
|
+
|
|
84
|
+
The Redis connection is created but never closed on error:
|
|
85
|
+
|
|
86
|
+
* Connection opens on line 145
|
|
87
|
+
* Error path at line 156 doesn't call client.disconnect()
|
|
88
|
+
* This will exhaust connection pool over time
|
|
89
|
+
|
|
90
|
+
Add proper cleanup in a finally block:
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await client.connect()
|
|
94
|
+
// ... operations
|
|
95
|
+
} finally {
|
|
96
|
+
await client.disconnect()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
PERFORMANCE ANALYSIS
|
|
100
|
+
|
|
101
|
+
- N+1 Query Pattern in src/api/posts.ts:234-248
|
|
102
|
+
Loading comments for each post individually causes N+1 queries.
|
|
103
|
+
Consider using a single query with JOIN or batch loading.
|
|
104
|
+
|
|
105
|
+
- Unbounded Memory Usage in src/utils/processor.ts:89
|
|
106
|
+
Loading entire dataset into memory without pagination.
|
|
107
|
+
For large datasets, this will cause OOM errors.
|
|
108
|
+
|
|
109
|
+
TEST COVERAGE
|
|
110
|
+
|
|
111
|
+
- Missing error path tests for the new authentication flow
|
|
112
|
+
- No integration tests for the rate limiting middleware
|
|
113
|
+
- Edge cases around token expiry not covered
|
|
114
|
+
|
|
115
|
+
RECOMMENDATIONS
|
|
116
|
+
|
|
117
|
+
1. Add rate limiting to authentication endpoints
|
|
118
|
+
2. Implement request validation using a schema library
|
|
119
|
+
3. Add monitoring for the new cache layer
|
|
120
|
+
4. Consider adding database transaction support for multi-step operations
|
|
121
|
+
|
|
122
|
+
The security issues are blocking and must be fixed. The performance concerns should be addressed before this scales to production load.
|
|
123
|
+
</response>
|
|
124
|
+
|
|
125
|
+
## Review Tone and Approach
|
|
126
|
+
|
|
127
|
+
1. **Be Direct but Constructive**
|
|
128
|
+
- State issues clearly without hedging
|
|
129
|
+
- Explain impact and provide solutions
|
|
130
|
+
- Focus on the code, not the coder
|
|
131
|
+
|
|
132
|
+
2. **Prioritize Effectively**
|
|
133
|
+
- Lead with blocking issues
|
|
134
|
+
- Group related problems
|
|
135
|
+
- Don't bury critical findings
|
|
136
|
+
|
|
137
|
+
3. **Provide Value**
|
|
138
|
+
- Every comment should help improve the code
|
|
139
|
+
- Skip trivial issues unless they indicate patterns
|
|
140
|
+
- Include concrete fix suggestions
|
|
141
|
+
|
|
142
|
+
## FINAL REMINDER
|
|
143
|
+
|
|
144
|
+
Your ENTIRE output must be wrapped in <response></response> tags.
|
|
145
|
+
Start with "🤖 [Your Tool Name] ([Your Model])" then proceed with your analysis.
|
|
146
|
+
Use Gerrit's limited markdown format - NO backticks, NO markdown bold/italic.
|
|
147
|
+
|
|
148
|
+
CRITICAL FORMATTING RULES:
|
|
149
|
+
- Add blank lines between sections and before/after code blocks
|
|
150
|
+
- Use exactly 4 spaces to start each line of code blocks
|
|
151
|
+
- Keep code blocks simple and readable
|
|
152
|
+
- Add proper spacing for readability
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Schema } from '@effect/schema'
|
|
3
|
+
import { Effect } from 'effect'
|
|
4
|
+
import { AppConfig, AiConfig, aiConfigFromFlat, migrateFromNestedConfig } from './config'
|
|
5
|
+
|
|
6
|
+
describe('Config Schemas', () => {
|
|
7
|
+
describe('AppConfig (Flat Structure)', () => {
|
|
8
|
+
test('validates complete flat config', () => {
|
|
9
|
+
const validConfig = {
|
|
10
|
+
host: 'https://gerrit.example.com',
|
|
11
|
+
username: 'testuser',
|
|
12
|
+
password: 'testpass123',
|
|
13
|
+
aiTool: 'claude' as const,
|
|
14
|
+
aiAutoDetect: true,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = Schema.decodeUnknownSync(AppConfig)(validConfig)
|
|
18
|
+
expect(result).toEqual(validConfig)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('validates minimal flat config with defaults', () => {
|
|
22
|
+
const minimalConfig = {
|
|
23
|
+
host: 'https://gerrit.example.com',
|
|
24
|
+
username: 'testuser',
|
|
25
|
+
password: 'testpass123',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = Schema.decodeUnknownSync(AppConfig)(minimalConfig)
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
...minimalConfig,
|
|
31
|
+
aiAutoDetect: true, // default value
|
|
32
|
+
aiTool: undefined,
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('rejects invalid host URL', () => {
|
|
37
|
+
const invalidConfig = {
|
|
38
|
+
host: 'not-a-url',
|
|
39
|
+
username: 'testuser',
|
|
40
|
+
password: 'testpass123',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('rejects empty username', () => {
|
|
47
|
+
const invalidConfig = {
|
|
48
|
+
host: 'https://gerrit.example.com',
|
|
49
|
+
username: '',
|
|
50
|
+
password: 'testpass123',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('rejects empty password', () => {
|
|
57
|
+
const invalidConfig = {
|
|
58
|
+
host: 'https://gerrit.example.com',
|
|
59
|
+
username: 'testuser',
|
|
60
|
+
password: '',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('validates all AI tool options', () => {
|
|
67
|
+
const tools = ['claude', 'llm', 'opencode', 'gemini'] as const
|
|
68
|
+
|
|
69
|
+
tools.forEach((tool) => {
|
|
70
|
+
const config = {
|
|
71
|
+
host: 'https://gerrit.example.com',
|
|
72
|
+
username: 'testuser',
|
|
73
|
+
password: 'testpass123',
|
|
74
|
+
aiTool: tool,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = Schema.decodeUnknownSync(AppConfig)(config)
|
|
78
|
+
expect(result.aiTool).toBe(tool)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('rejects invalid AI tool', () => {
|
|
83
|
+
const invalidConfig = {
|
|
84
|
+
host: 'https://gerrit.example.com',
|
|
85
|
+
username: 'testuser',
|
|
86
|
+
password: 'testpass123',
|
|
87
|
+
aiTool: 'invalid-tool',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('Legacy AiConfig (Backward Compatibility)', () => {
|
|
95
|
+
test('validates legacy AI config structure', () => {
|
|
96
|
+
const validAiConfig = {
|
|
97
|
+
tool: 'claude' as const,
|
|
98
|
+
autoDetect: false,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = Schema.decodeUnknownSync(AiConfig)(validAiConfig)
|
|
102
|
+
expect(result).toEqual(validAiConfig)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('validates minimal legacy AI config with defaults', () => {
|
|
106
|
+
const minimalAiConfig = {}
|
|
107
|
+
|
|
108
|
+
const result = Schema.decodeUnknownSync(AiConfig)(minimalAiConfig)
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
autoDetect: true, // default value
|
|
111
|
+
tool: undefined,
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('Helper Functions', () => {
|
|
117
|
+
test('aiConfigFromFlat converts flat config to legacy AI config', () => {
|
|
118
|
+
const flatConfig = {
|
|
119
|
+
host: 'https://gerrit.example.com',
|
|
120
|
+
username: 'testuser',
|
|
121
|
+
password: 'testpass123',
|
|
122
|
+
aiTool: 'claude' as const,
|
|
123
|
+
aiAutoDetect: false,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const aiConfig = aiConfigFromFlat(flatConfig)
|
|
127
|
+
expect(aiConfig).toEqual({
|
|
128
|
+
tool: 'claude',
|
|
129
|
+
autoDetect: false,
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('aiConfigFromFlat handles undefined AI options', () => {
|
|
134
|
+
const flatConfig = {
|
|
135
|
+
host: 'https://gerrit.example.com',
|
|
136
|
+
username: 'testuser',
|
|
137
|
+
password: 'testpass123',
|
|
138
|
+
aiAutoDetect: true,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const aiConfig = aiConfigFromFlat(flatConfig)
|
|
142
|
+
expect(aiConfig).toEqual({
|
|
143
|
+
tool: undefined,
|
|
144
|
+
autoDetect: true,
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('migrateFromNestedConfig converts old nested format', () => {
|
|
149
|
+
const nestedConfig = {
|
|
150
|
+
credentials: {
|
|
151
|
+
host: 'https://gerrit.example.com',
|
|
152
|
+
username: 'testuser',
|
|
153
|
+
password: 'testpass123',
|
|
154
|
+
},
|
|
155
|
+
ai: {
|
|
156
|
+
tool: 'claude' as const,
|
|
157
|
+
autoDetect: false,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const flatConfig = migrateFromNestedConfig(nestedConfig)
|
|
162
|
+
expect(flatConfig).toEqual({
|
|
163
|
+
host: 'https://gerrit.example.com',
|
|
164
|
+
username: 'testuser',
|
|
165
|
+
password: 'testpass123',
|
|
166
|
+
aiTool: 'claude',
|
|
167
|
+
aiAutoDetect: false,
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('migrateFromNestedConfig handles missing AI config', () => {
|
|
172
|
+
const nestedConfig = {
|
|
173
|
+
credentials: {
|
|
174
|
+
host: 'https://gerrit.example.com',
|
|
175
|
+
username: 'testuser',
|
|
176
|
+
password: 'testpass123',
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const flatConfig = migrateFromNestedConfig(nestedConfig)
|
|
181
|
+
expect(flatConfig).toEqual({
|
|
182
|
+
host: 'https://gerrit.example.com',
|
|
183
|
+
username: 'testuser',
|
|
184
|
+
password: 'testpass123',
|
|
185
|
+
aiTool: undefined,
|
|
186
|
+
aiAutoDetect: true, // default
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('migrateFromNestedConfig handles partial AI config', () => {
|
|
191
|
+
const nestedConfig = {
|
|
192
|
+
credentials: {
|
|
193
|
+
host: 'https://gerrit.example.com',
|
|
194
|
+
username: 'testuser',
|
|
195
|
+
password: 'testpass123',
|
|
196
|
+
},
|
|
197
|
+
ai: {
|
|
198
|
+
tool: 'llm' as const,
|
|
199
|
+
// autoDetect missing
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const flatConfig = migrateFromNestedConfig(nestedConfig)
|
|
204
|
+
expect(flatConfig).toEqual({
|
|
205
|
+
host: 'https://gerrit.example.com',
|
|
206
|
+
username: 'testuser',
|
|
207
|
+
password: 'testpass123',
|
|
208
|
+
aiTool: 'llm',
|
|
209
|
+
aiAutoDetect: true, // default when missing
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Effect Schema Integration', () => {
|
|
215
|
+
test('Effect.gen with valid flat config', async () => {
|
|
216
|
+
const config = {
|
|
217
|
+
host: 'https://gerrit.example.com',
|
|
218
|
+
username: 'testuser',
|
|
219
|
+
password: 'testpass123',
|
|
220
|
+
aiTool: 'claude' as const,
|
|
221
|
+
aiAutoDetect: true,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await Effect.gen(function* () {
|
|
225
|
+
return yield* Schema.decodeUnknown(AppConfig)(config)
|
|
226
|
+
}).pipe(Effect.runPromise)
|
|
227
|
+
|
|
228
|
+
expect(result).toEqual(config)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('Effect.gen with validation error', async () => {
|
|
232
|
+
const invalidConfig = {
|
|
233
|
+
host: 'not-a-url',
|
|
234
|
+
username: 'testuser',
|
|
235
|
+
password: 'testpass123',
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await expect(
|
|
239
|
+
Effect.gen(function* () {
|
|
240
|
+
return yield* Schema.decodeUnknown(AppConfig)(invalidConfig)
|
|
241
|
+
}).pipe(Effect.runPromise),
|
|
242
|
+
).rejects.toThrow()
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Schema } from '@effect/schema'
|
|
2
|
+
|
|
3
|
+
// Flat Application Configuration (similar to ji structure)
|
|
4
|
+
export const AppConfig = Schema.Struct({
|
|
5
|
+
// Gerrit credentials (flattened)
|
|
6
|
+
host: Schema.String.pipe(
|
|
7
|
+
Schema.pattern(/^https?:\/\/.+$/),
|
|
8
|
+
Schema.annotations({ description: 'Gerrit server URL' }),
|
|
9
|
+
),
|
|
10
|
+
username: Schema.String.pipe(
|
|
11
|
+
Schema.minLength(1),
|
|
12
|
+
Schema.annotations({ description: 'Gerrit username' }),
|
|
13
|
+
),
|
|
14
|
+
password: Schema.String.pipe(
|
|
15
|
+
Schema.minLength(1),
|
|
16
|
+
Schema.annotations({ description: 'HTTP password or API token' }),
|
|
17
|
+
),
|
|
18
|
+
// AI configuration (flattened)
|
|
19
|
+
aiTool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
20
|
+
aiAutoDetect: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export type AppConfig = Schema.Schema.Type<typeof AppConfig>
|
|
24
|
+
|
|
25
|
+
// Legacy schemas for backward compatibility (deprecated)
|
|
26
|
+
export const AiConfig = Schema.Struct({
|
|
27
|
+
tool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
28
|
+
autoDetect: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export type AiConfig = Schema.Schema.Type<typeof AiConfig>
|
|
32
|
+
|
|
33
|
+
// Helper to convert from flat config to legacy AI config
|
|
34
|
+
export const aiConfigFromFlat = (config: AppConfig): AiConfig => ({
|
|
35
|
+
tool: config.aiTool,
|
|
36
|
+
autoDetect: config.aiAutoDetect,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Schema for validating legacy nested config structure
|
|
40
|
+
const LegacyNestedConfig = Schema.Struct({
|
|
41
|
+
credentials: Schema.Struct({
|
|
42
|
+
host: Schema.String.pipe(
|
|
43
|
+
Schema.pattern(/^https?:\/\/.+$/),
|
|
44
|
+
Schema.annotations({ description: 'Gerrit server URL' }),
|
|
45
|
+
),
|
|
46
|
+
username: Schema.String.pipe(Schema.minLength(1)),
|
|
47
|
+
password: Schema.String.pipe(Schema.minLength(1)),
|
|
48
|
+
}),
|
|
49
|
+
ai: Schema.optional(
|
|
50
|
+
Schema.Struct({
|
|
51
|
+
tool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
52
|
+
autoDetect: Schema.optional(Schema.Boolean),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
type LegacyNestedConfig = Schema.Schema.Type<typeof LegacyNestedConfig>
|
|
58
|
+
|
|
59
|
+
// Helper to convert from legacy nested format to flat format with validation
|
|
60
|
+
export const migrateFromNestedConfig = (nested: unknown): AppConfig => {
|
|
61
|
+
// Validate input structure using Schema
|
|
62
|
+
const validatedNested = Schema.decodeUnknownSync(LegacyNestedConfig)(nested)
|
|
63
|
+
|
|
64
|
+
// Convert to flat structure
|
|
65
|
+
const flatConfig = {
|
|
66
|
+
host: validatedNested.credentials.host,
|
|
67
|
+
username: validatedNested.credentials.username,
|
|
68
|
+
password: validatedNested.credentials.password,
|
|
69
|
+
aiTool: validatedNested.ai?.tool,
|
|
70
|
+
aiAutoDetect: validatedNested.ai?.autoDetect ?? true,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate the resulting flat config
|
|
74
|
+
return Schema.decodeUnknownSync(AppConfig)(flatConfig)
|
|
75
|
+
}
|