@jaguilar87/gaia-ops 2.2.0 → 2.2.2
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/CHANGELOG.md +137 -1
- package/README.en.md +29 -23
- package/README.md +24 -17
- package/agents/{claude-architect.md → gaia.md} +6 -6
- package/commands/{architect.md → gaia.md} +6 -6
- package/config/AGENTS.md +5 -5
- package/config/agent-catalog.md +14 -14
- package/config/context-contracts.md +4 -4
- package/config/embeddings_info.json +14 -0
- package/config/intent_embeddings.json +2002 -0
- package/config/intent_embeddings.npy +0 -0
- package/index.js +3 -1
- package/package.json +3 -2
- package/speckit/README.en.md +20 -69
- package/templates/CLAUDE.template.md +5 -13
- package/tests/README.en.md +224 -0
- package/tests/README.md +338 -0
- package/tests/fixtures/project-context.aws.json +53 -0
- package/tests/fixtures/project-context.gcp.json +53 -0
- package/tests/integration/RUN_TESTS.md +185 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/test_hooks_integration.py +473 -0
- package/tests/integration/test_hooks_workflow.py +397 -0
- package/tests/permissions-validation/MANUAL_VALIDATION.md +434 -0
- package/tests/permissions-validation/test_permissions_validation.py +527 -0
- package/tests/system/__init__.py +0 -0
- package/tests/system/permissions_helpers.py +318 -0
- package/tests/system/test_agent_definitions.py +166 -0
- package/tests/system/test_configuration_files.py +121 -0
- package/tests/system/test_directory_structure.py +231 -0
- package/tests/system/test_permissions_system.py +1006 -0
- package/tests/tools/__init__.py +0 -0
- package/tests/tools/test_agent_router.py +266 -0
- package/tests/tools/test_clarify_engine.py +413 -0
- package/tests/tools/test_context_provider.py +157 -0
- package/tests/validators/__init__.py +0 -0
- package/tests/validators/test_approval_gate.py +415 -0
- package/tests/validators/test_commit_validator.py +446 -0
- package/tools/context_provider.py +28 -7
- package/tools/generate_embeddings.py +3 -3
- package/tools/semantic_matcher.py +2 -2
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for commit_validator.py
|
|
3
|
+
|
|
4
|
+
Tests the Git Commit Message Validator that prevents commits with
|
|
5
|
+
forbidden footers or incorrect format from being executed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
# Add parent directory to path to import commit_validator
|
|
15
|
+
test_dir = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
claude_tools_path = os.path.join(test_dir, '../../../.claude/tools')
|
|
17
|
+
sys.path.insert(0, claude_tools_path)
|
|
18
|
+
|
|
19
|
+
from commit_validator import (
|
|
20
|
+
CommitMessageValidator,
|
|
21
|
+
ValidationResult,
|
|
22
|
+
validate_commit_message,
|
|
23
|
+
safe_validate_before_commit
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestCommitMessageValidator(unittest.TestCase):
|
|
28
|
+
"""Test cases for CommitMessageValidator class."""
|
|
29
|
+
|
|
30
|
+
def setUp(self):
|
|
31
|
+
"""Set up test fixtures."""
|
|
32
|
+
self.validator = CommitMessageValidator()
|
|
33
|
+
|
|
34
|
+
def test_valid_feat_commit(self):
|
|
35
|
+
"""Test validation of valid feat commit."""
|
|
36
|
+
message = "feat(helmrelease): add Phase 3.3 services"
|
|
37
|
+
validation = self.validator.validate(message)
|
|
38
|
+
|
|
39
|
+
self.assertTrue(validation.valid)
|
|
40
|
+
self.assertEqual(len(validation.errors), 0)
|
|
41
|
+
|
|
42
|
+
def test_valid_fix_commit(self):
|
|
43
|
+
"""Test validation of valid fix commit."""
|
|
44
|
+
message = "fix(pg-non-prod): correct API key environment variable mappings"
|
|
45
|
+
validation = self.validator.validate(message)
|
|
46
|
+
|
|
47
|
+
self.assertTrue(validation.valid)
|
|
48
|
+
self.assertEqual(len(validation.errors), 0)
|
|
49
|
+
|
|
50
|
+
def test_valid_commit_without_scope(self):
|
|
51
|
+
"""Test validation of valid commit without scope."""
|
|
52
|
+
message = "refactor: simplify context provider logic"
|
|
53
|
+
validation = self.validator.validate(message)
|
|
54
|
+
|
|
55
|
+
self.assertTrue(validation.valid)
|
|
56
|
+
self.assertEqual(len(validation.errors), 0)
|
|
57
|
+
|
|
58
|
+
def test_valid_commit_with_body(self):
|
|
59
|
+
"""Test validation of valid commit with body."""
|
|
60
|
+
message = """feat(helmrelease): add Phase 3.3 services
|
|
61
|
+
|
|
62
|
+
Deploys 6 microservices for PG RAG Agent:
|
|
63
|
+
- admin-ui
|
|
64
|
+
- query-api
|
|
65
|
+
- admin-api
|
|
66
|
+
- embedding-worker
|
|
67
|
+
- ingestion-worker
|
|
68
|
+
- query-worker"""
|
|
69
|
+
validation = self.validator.validate(message)
|
|
70
|
+
|
|
71
|
+
self.assertTrue(validation.valid)
|
|
72
|
+
self.assertEqual(len(validation.errors), 0)
|
|
73
|
+
|
|
74
|
+
def test_forbidden_footer_claude_code(self):
|
|
75
|
+
"""Test rejection of commit with Claude Code footer."""
|
|
76
|
+
message = """feat: add new feature
|
|
77
|
+
|
|
78
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)"""
|
|
79
|
+
|
|
80
|
+
validation = self.validator.validate(message)
|
|
81
|
+
|
|
82
|
+
self.assertFalse(validation.valid)
|
|
83
|
+
# Should detect at least one forbidden footer (either "Claude Code" or "🤖 Generated with")
|
|
84
|
+
self.assertGreaterEqual(len(validation.errors), 1)
|
|
85
|
+
self.assertEqual(validation.errors[0]['type'], 'FORBIDDEN_FOOTER')
|
|
86
|
+
# Check that message contains one of the forbidden terms
|
|
87
|
+
error_msg = validation.errors[0]['message']
|
|
88
|
+
self.assertTrue(
|
|
89
|
+
'Claude Code' in error_msg or 'Generated with' in error_msg,
|
|
90
|
+
f"Expected forbidden footer in error message, got: {error_msg}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def test_forbidden_footer_co_authored(self):
|
|
94
|
+
"""Test rejection of commit with Co-Authored-By Claude footer."""
|
|
95
|
+
message = """fix: update secrets
|
|
96
|
+
|
|
97
|
+
Co-Authored-By: Claude <noreply@anthropic.com>"""
|
|
98
|
+
|
|
99
|
+
validation = self.validator.validate(message)
|
|
100
|
+
|
|
101
|
+
self.assertFalse(validation.valid)
|
|
102
|
+
self.assertEqual(len(validation.errors), 1)
|
|
103
|
+
self.assertEqual(validation.errors[0]['type'], 'FORBIDDEN_FOOTER')
|
|
104
|
+
self.assertIn('Co-Authored-By: Claude', validation.errors[0]['message'])
|
|
105
|
+
|
|
106
|
+
def test_multiple_forbidden_footers(self):
|
|
107
|
+
"""Test rejection of commit with multiple forbidden footers."""
|
|
108
|
+
message = """feat: add feature
|
|
109
|
+
|
|
110
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
111
|
+
|
|
112
|
+
Co-Authored-By: Claude <noreply@anthropic.com>"""
|
|
113
|
+
|
|
114
|
+
validation = self.validator.validate(message)
|
|
115
|
+
|
|
116
|
+
self.assertFalse(validation.valid)
|
|
117
|
+
self.assertEqual(len(validation.errors), 2)
|
|
118
|
+
|
|
119
|
+
error_types = [error['type'] for error in validation.errors]
|
|
120
|
+
self.assertEqual(error_types.count('FORBIDDEN_FOOTER'), 2)
|
|
121
|
+
|
|
122
|
+
def test_invalid_format_no_type(self):
|
|
123
|
+
"""Test rejection of commit without type."""
|
|
124
|
+
message = "Added new feature"
|
|
125
|
+
validation = self.validator.validate(message)
|
|
126
|
+
|
|
127
|
+
self.assertFalse(validation.valid)
|
|
128
|
+
self.assertTrue(
|
|
129
|
+
any(error['type'] == 'INVALID_FORMAT' for error in validation.errors)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def test_invalid_format_wrong_type(self):
|
|
133
|
+
"""Test rejection of commit with invalid type."""
|
|
134
|
+
message = "added: new feature"
|
|
135
|
+
validation = self.validator.validate(message)
|
|
136
|
+
|
|
137
|
+
self.assertFalse(validation.valid)
|
|
138
|
+
self.assertTrue(
|
|
139
|
+
any(error['type'] == 'INVALID_FORMAT' for error in validation.errors)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def test_invalid_format_missing_description(self):
|
|
143
|
+
"""Test rejection of commit without description."""
|
|
144
|
+
message = "feat:"
|
|
145
|
+
validation = self.validator.validate(message)
|
|
146
|
+
|
|
147
|
+
self.assertFalse(validation.valid)
|
|
148
|
+
|
|
149
|
+
def test_subject_too_long(self):
|
|
150
|
+
"""Test rejection of commit with subject exceeding max length."""
|
|
151
|
+
# Create a subject longer than 72 characters
|
|
152
|
+
long_description = "a" * 80
|
|
153
|
+
message = f"feat: {long_description}"
|
|
154
|
+
validation = self.validator.validate(message)
|
|
155
|
+
|
|
156
|
+
self.assertFalse(validation.valid)
|
|
157
|
+
self.assertTrue(
|
|
158
|
+
any(error['type'] == 'SUBJECT_TOO_LONG' for error in validation.errors)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def test_subject_ends_with_period(self):
|
|
162
|
+
"""Test rejection of commit with period at end."""
|
|
163
|
+
message = "feat: add new feature."
|
|
164
|
+
validation = self.validator.validate(message)
|
|
165
|
+
|
|
166
|
+
self.assertFalse(validation.valid)
|
|
167
|
+
self.assertTrue(
|
|
168
|
+
any(error['type'] == 'SUBJECT_ENDS_WITH_PERIOD' for error in validation.errors)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def test_allowed_footer_breaking_change(self):
|
|
172
|
+
"""Test that allowed footers are not rejected."""
|
|
173
|
+
message = """feat: add breaking change
|
|
174
|
+
|
|
175
|
+
BREAKING CHANGE: API endpoint changed from v1 to v2"""
|
|
176
|
+
|
|
177
|
+
validation = self.validator.validate(message)
|
|
178
|
+
|
|
179
|
+
# Should be valid (BREAKING CHANGE is allowed)
|
|
180
|
+
self.assertTrue(validation.valid)
|
|
181
|
+
|
|
182
|
+
def test_allowed_footer_closes(self):
|
|
183
|
+
"""Test that Closes footer is allowed."""
|
|
184
|
+
message = """fix: resolve authentication bug
|
|
185
|
+
|
|
186
|
+
Closes: #123"""
|
|
187
|
+
|
|
188
|
+
validation = self.validator.validate(message)
|
|
189
|
+
|
|
190
|
+
self.assertTrue(validation.valid)
|
|
191
|
+
|
|
192
|
+
def test_case_insensitive_forbidden_footer_detection(self):
|
|
193
|
+
"""Test that forbidden footer detection is case-insensitive."""
|
|
194
|
+
message = """feat: add feature
|
|
195
|
+
|
|
196
|
+
generated with claude code"""
|
|
197
|
+
|
|
198
|
+
validation = self.validator.validate(message)
|
|
199
|
+
|
|
200
|
+
self.assertFalse(validation.valid)
|
|
201
|
+
self.assertTrue(
|
|
202
|
+
any(error['type'] == 'FORBIDDEN_FOOTER' for error in validation.errors)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def test_all_allowed_types(self):
|
|
206
|
+
"""Test that all allowed types are accepted."""
|
|
207
|
+
allowed_types = [
|
|
208
|
+
"feat", "fix", "refactor", "docs", "test",
|
|
209
|
+
"chore", "ci", "perf", "style", "build"
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
for commit_type in allowed_types:
|
|
213
|
+
message = f"{commit_type}: do something"
|
|
214
|
+
validation = self.validator.validate(message)
|
|
215
|
+
|
|
216
|
+
self.assertTrue(
|
|
217
|
+
validation.valid,
|
|
218
|
+
f"Type '{commit_type}' should be valid"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestValidationResult(unittest.TestCase):
|
|
223
|
+
"""Test cases for ValidationResult dataclass."""
|
|
224
|
+
|
|
225
|
+
def test_validation_result_valid(self):
|
|
226
|
+
"""Test ValidationResult for valid message."""
|
|
227
|
+
result = ValidationResult(valid=True, errors=[])
|
|
228
|
+
|
|
229
|
+
self.assertTrue(result.valid)
|
|
230
|
+
self.assertEqual(len(result.errors), 0)
|
|
231
|
+
self.assertEqual(len(result.warnings), 0)
|
|
232
|
+
|
|
233
|
+
def test_validation_result_with_errors(self):
|
|
234
|
+
"""Test ValidationResult with errors."""
|
|
235
|
+
errors = [
|
|
236
|
+
{'type': 'FORBIDDEN_FOOTER', 'message': 'Error message'}
|
|
237
|
+
]
|
|
238
|
+
result = ValidationResult(valid=False, errors=errors)
|
|
239
|
+
|
|
240
|
+
self.assertFalse(result.valid)
|
|
241
|
+
self.assertEqual(len(result.errors), 1)
|
|
242
|
+
|
|
243
|
+
def test_validation_result_with_warnings(self):
|
|
244
|
+
"""Test ValidationResult with warnings."""
|
|
245
|
+
warnings = [
|
|
246
|
+
{'type': 'BODY_LINE_TOO_LONG', 'message': 'Warning message'}
|
|
247
|
+
]
|
|
248
|
+
result = ValidationResult(valid=True, errors=[], warnings=warnings)
|
|
249
|
+
|
|
250
|
+
self.assertTrue(result.valid)
|
|
251
|
+
self.assertEqual(len(result.warnings), 1)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestConvenienceFunctions(unittest.TestCase):
|
|
255
|
+
"""Test cases for module-level convenience functions."""
|
|
256
|
+
|
|
257
|
+
def test_validate_commit_message_valid(self):
|
|
258
|
+
"""Test validate_commit_message with valid message."""
|
|
259
|
+
message = "feat: add new feature"
|
|
260
|
+
validation = validate_commit_message(message)
|
|
261
|
+
|
|
262
|
+
self.assertTrue(validation.valid)
|
|
263
|
+
|
|
264
|
+
def test_validate_commit_message_invalid(self):
|
|
265
|
+
"""Test validate_commit_message with invalid message."""
|
|
266
|
+
message = "Added new feature"
|
|
267
|
+
validation = validate_commit_message(message)
|
|
268
|
+
|
|
269
|
+
self.assertFalse(validation.valid)
|
|
270
|
+
|
|
271
|
+
def test_safe_validate_before_commit_valid(self):
|
|
272
|
+
"""Test safe_validate_before_commit with valid message."""
|
|
273
|
+
message = "fix: correct bug"
|
|
274
|
+
result = safe_validate_before_commit(message)
|
|
275
|
+
|
|
276
|
+
self.assertTrue(result)
|
|
277
|
+
|
|
278
|
+
def test_safe_validate_before_commit_invalid(self):
|
|
279
|
+
"""Test safe_validate_before_commit with invalid message."""
|
|
280
|
+
message = "Fixed bug"
|
|
281
|
+
result = safe_validate_before_commit(message)
|
|
282
|
+
|
|
283
|
+
self.assertFalse(result)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TestHelperMethods(unittest.TestCase):
|
|
287
|
+
"""Test cases for helper methods."""
|
|
288
|
+
|
|
289
|
+
def setUp(self):
|
|
290
|
+
"""Set up test fixtures."""
|
|
291
|
+
self.validator = CommitMessageValidator()
|
|
292
|
+
|
|
293
|
+
def test_get_examples(self):
|
|
294
|
+
"""Test get_examples returns valid and invalid examples."""
|
|
295
|
+
examples = self.validator.get_examples()
|
|
296
|
+
|
|
297
|
+
self.assertIn('valid', examples)
|
|
298
|
+
self.assertIn('invalid', examples)
|
|
299
|
+
self.assertIsInstance(examples['valid'], list)
|
|
300
|
+
self.assertIsInstance(examples['invalid'], list)
|
|
301
|
+
|
|
302
|
+
def test_get_allowed_types(self):
|
|
303
|
+
"""Test get_allowed_types returns list of types."""
|
|
304
|
+
types = self.validator.get_allowed_types()
|
|
305
|
+
|
|
306
|
+
self.assertIsInstance(types, list)
|
|
307
|
+
self.assertIn('feat', types)
|
|
308
|
+
self.assertIn('fix', types)
|
|
309
|
+
self.assertIn('refactor', types)
|
|
310
|
+
|
|
311
|
+
def test_format_error_message_valid(self):
|
|
312
|
+
"""Test format_error_message for valid result."""
|
|
313
|
+
validation = ValidationResult(valid=True, errors=[])
|
|
314
|
+
formatted = self.validator.format_error_message(validation)
|
|
315
|
+
|
|
316
|
+
self.assertIn("✅", formatted)
|
|
317
|
+
self.assertIn("valid", formatted.lower())
|
|
318
|
+
|
|
319
|
+
def test_format_error_message_invalid(self):
|
|
320
|
+
"""Test format_error_message for invalid result."""
|
|
321
|
+
errors = [{
|
|
322
|
+
'type': 'FORBIDDEN_FOOTER',
|
|
323
|
+
'message': 'Contains forbidden footer',
|
|
324
|
+
'fix': 'Remove the footer'
|
|
325
|
+
}]
|
|
326
|
+
validation = ValidationResult(valid=False, errors=errors)
|
|
327
|
+
formatted = self.validator.format_error_message(validation)
|
|
328
|
+
|
|
329
|
+
self.assertIn("❌", formatted)
|
|
330
|
+
self.assertIn("FORBIDDEN_FOOTER", formatted)
|
|
331
|
+
self.assertIn("Contains forbidden footer", formatted)
|
|
332
|
+
self.assertIn("Fix:", formatted)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class TestLogging(unittest.TestCase):
|
|
336
|
+
"""Test cases for violation logging."""
|
|
337
|
+
|
|
338
|
+
def test_log_violation(self):
|
|
339
|
+
"""Test that violations are logged when enabled."""
|
|
340
|
+
# Create temporary config with logging enabled
|
|
341
|
+
temp_config = tempfile.NamedTemporaryFile(
|
|
342
|
+
mode='w', suffix='.json', delete=False
|
|
343
|
+
)
|
|
344
|
+
temp_log = tempfile.NamedTemporaryFile(
|
|
345
|
+
mode='w', suffix='.jsonl', delete=False
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
config = {
|
|
349
|
+
"commit_message": {
|
|
350
|
+
"format": "conventional_commits",
|
|
351
|
+
"type_allowed": ["feat", "fix"],
|
|
352
|
+
"footer_forbidden": ["Generated with Claude"]
|
|
353
|
+
},
|
|
354
|
+
"enforcement": {
|
|
355
|
+
"enabled": True,
|
|
356
|
+
"log_violations": True,
|
|
357
|
+
"log_path": temp_log.name
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
json.dump(config, temp_config)
|
|
362
|
+
temp_config.close()
|
|
363
|
+
temp_log.close()
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
# Create validator with custom config
|
|
367
|
+
validator = CommitMessageValidator(config_path=temp_config.name)
|
|
368
|
+
|
|
369
|
+
# Validate invalid message (should log)
|
|
370
|
+
message = "Invalid commit message"
|
|
371
|
+
validation = validator.validate(message)
|
|
372
|
+
|
|
373
|
+
self.assertFalse(validation.valid)
|
|
374
|
+
|
|
375
|
+
# Check that log file was created and contains entry
|
|
376
|
+
with open(temp_log.name, 'r') as f:
|
|
377
|
+
log_entries = f.readlines()
|
|
378
|
+
|
|
379
|
+
self.assertGreater(len(log_entries), 0)
|
|
380
|
+
|
|
381
|
+
# Parse first log entry
|
|
382
|
+
log_entry = json.loads(log_entries[0])
|
|
383
|
+
self.assertIn('timestamp', log_entry)
|
|
384
|
+
self.assertIn('message', log_entry)
|
|
385
|
+
self.assertIn('errors', log_entry)
|
|
386
|
+
|
|
387
|
+
finally:
|
|
388
|
+
# Clean up temp files
|
|
389
|
+
os.remove(temp_config.name)
|
|
390
|
+
os.remove(temp_log.name)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class TestRealWorldCommits(unittest.TestCase):
|
|
394
|
+
"""Test with real-world commit message examples."""
|
|
395
|
+
|
|
396
|
+
def setUp(self):
|
|
397
|
+
"""Set up test fixtures."""
|
|
398
|
+
self.validator = CommitMessageValidator()
|
|
399
|
+
|
|
400
|
+
def test_real_commit_from_log_forbidden(self):
|
|
401
|
+
"""Test the actual commit from the log that was interrupted."""
|
|
402
|
+
message = """fix(pg-non-prod): add explicit API key environment variable mappings
|
|
403
|
+
|
|
404
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
405
|
+
|
|
406
|
+
Co-Authored-By: Claude <noreply@anthropic.com>"""
|
|
407
|
+
|
|
408
|
+
validation = self.validator.validate(message)
|
|
409
|
+
|
|
410
|
+
# Should be INVALID due to forbidden footers
|
|
411
|
+
self.assertFalse(validation.valid)
|
|
412
|
+
self.assertGreaterEqual(len(validation.errors), 2) # At least 2 forbidden footers
|
|
413
|
+
|
|
414
|
+
def test_real_commit_from_log_corrected(self):
|
|
415
|
+
"""Test the corrected version without forbidden footers."""
|
|
416
|
+
message = """fix(pg-non-prod): add explicit API key environment variable mappings
|
|
417
|
+
|
|
418
|
+
Maps snake_case secret keys to SCREAMING_SNAKE_CASE env vars
|
|
419
|
+
required by NestJS applications."""
|
|
420
|
+
|
|
421
|
+
validation = self.validator.validate(message)
|
|
422
|
+
|
|
423
|
+
# Should be VALID
|
|
424
|
+
self.assertTrue(validation.valid)
|
|
425
|
+
self.assertEqual(len(validation.errors), 0)
|
|
426
|
+
|
|
427
|
+
def test_complex_commit_with_multiple_issues(self):
|
|
428
|
+
"""Test commit with multiple validation issues."""
|
|
429
|
+
message = """Added new feature.
|
|
430
|
+
|
|
431
|
+
This is a long body without proper wrapping and it exceeds the maximum line length specified in the configuration file.
|
|
432
|
+
|
|
433
|
+
🤖 Generated with Claude Code"""
|
|
434
|
+
|
|
435
|
+
validation = self.validator.validate(message)
|
|
436
|
+
|
|
437
|
+
self.assertFalse(validation.valid)
|
|
438
|
+
|
|
439
|
+
# Should have multiple errors
|
|
440
|
+
error_types = [error['type'] for error in validation.errors]
|
|
441
|
+
self.assertIn('INVALID_FORMAT', error_types)
|
|
442
|
+
self.assertIn('FORBIDDEN_FOOTER', error_types)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
if __name__ == '__main__':
|
|
446
|
+
unittest.main()
|
|
@@ -6,9 +6,30 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Dict, List, Any, Optional
|
|
7
7
|
|
|
8
8
|
# This script is expected to be run from the root of the repository.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
9
|
+
# Historically the project context lived at `.claude/project-context.json`, but
|
|
10
|
+
# newer installs keep it under `.claude/project-context/project-context.json`.
|
|
11
|
+
# We auto-detect the most common locations (and honor GAIA_CONTEXT_PATH) so agent
|
|
12
|
+
# routing never breaks if the file isn't symlinked to the legacy path.
|
|
13
|
+
def get_default_context_path() -> Path:
|
|
14
|
+
"""Detects the project-context.json location, honoring GAIA_CONTEXT_PATH."""
|
|
15
|
+
env_override = os.environ.get("GAIA_CONTEXT_PATH")
|
|
16
|
+
if env_override:
|
|
17
|
+
return Path(env_override).expanduser()
|
|
18
|
+
|
|
19
|
+
candidates = [
|
|
20
|
+
Path(".claude/project-context.json"),
|
|
21
|
+
Path(".claude/project-context/project-context.json"),
|
|
22
|
+
Path("project-context.json"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
for candidate in candidates:
|
|
26
|
+
if candidate.is_file():
|
|
27
|
+
return candidate
|
|
28
|
+
|
|
29
|
+
# Fall back to the legacy expectation; load_project_context will surface error
|
|
30
|
+
return candidates[0]
|
|
31
|
+
|
|
32
|
+
DEFAULT_CONTEXT_PATH = get_default_context_path()
|
|
12
33
|
|
|
13
34
|
# Path to provider-specific contract files
|
|
14
35
|
# When running from installed project: .claude/config (symlinked)
|
|
@@ -275,10 +296,10 @@ def get_contract_context(
|
|
|
275
296
|
agent_contract = provider_contracts["agents"].get(agent_name)
|
|
276
297
|
if not agent_contract:
|
|
277
298
|
print(
|
|
278
|
-
f"
|
|
299
|
+
f"ERROR: Invalid agent '{agent_name}'. Agent not found in provider contracts.",
|
|
279
300
|
file=sys.stderr,
|
|
280
301
|
)
|
|
281
|
-
|
|
302
|
+
sys.exit(1)
|
|
282
303
|
|
|
283
304
|
contract_keys = agent_contract.get("required", [])
|
|
284
305
|
else:
|
|
@@ -286,10 +307,10 @@ def get_contract_context(
|
|
|
286
307
|
contract_keys = LEGACY_AGENT_CONTRACTS.get(agent_name)
|
|
287
308
|
if not contract_keys:
|
|
288
309
|
print(
|
|
289
|
-
f"
|
|
310
|
+
f"ERROR: Invalid agent '{agent_name}'. Agent not recognized.",
|
|
290
311
|
file=sys.stderr,
|
|
291
312
|
)
|
|
292
|
-
|
|
313
|
+
sys.exit(1)
|
|
293
314
|
|
|
294
315
|
sections = project_context.get("sections", {})
|
|
295
316
|
if not sections:
|
|
@@ -9,8 +9,8 @@ Usage:
|
|
|
9
9
|
python3 generate_embeddings.py
|
|
10
10
|
|
|
11
11
|
Output:
|
|
12
|
-
- .claude/
|
|
13
|
-
- .claude/
|
|
12
|
+
- .claude/config/intent_embeddings.npy (binary, ~5MB)
|
|
13
|
+
- .claude/config/intent_embeddings.json (readable metadata)
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import json
|
|
@@ -117,7 +117,7 @@ def generate_embeddings():
|
|
|
117
117
|
print(f"\n Total examples processed: {total_examples}")
|
|
118
118
|
|
|
119
119
|
# Save as JSON (readable metadata)
|
|
120
|
-
output_dir = Path(__file__).parent.parent / "
|
|
120
|
+
output_dir = Path(__file__).parent.parent / "config"
|
|
121
121
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
122
122
|
|
|
123
123
|
json_path = output_dir / "intent_embeddings.json"
|
|
@@ -32,10 +32,10 @@ class SemanticMatcher:
|
|
|
32
32
|
Initialize semantic matcher
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
|
-
embeddings_dir: Directory containing embeddings (defaults to .claude/
|
|
35
|
+
embeddings_dir: Directory containing embeddings (defaults to .claude/config/)
|
|
36
36
|
"""
|
|
37
37
|
if embeddings_dir is None:
|
|
38
|
-
embeddings_dir = Path(__file__).parent.parent / "
|
|
38
|
+
embeddings_dir = Path(__file__).parent.parent / "config"
|
|
39
39
|
|
|
40
40
|
self.embeddings_dir = embeddings_dir
|
|
41
41
|
self.embeddings: Dict[str, Any] = {}
|