@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +137 -1
  2. package/README.en.md +29 -23
  3. package/README.md +24 -17
  4. package/agents/{claude-architect.md → gaia.md} +6 -6
  5. package/commands/{architect.md → gaia.md} +6 -6
  6. package/config/AGENTS.md +5 -5
  7. package/config/agent-catalog.md +14 -14
  8. package/config/context-contracts.md +4 -4
  9. package/config/embeddings_info.json +14 -0
  10. package/config/intent_embeddings.json +2002 -0
  11. package/config/intent_embeddings.npy +0 -0
  12. package/index.js +3 -1
  13. package/package.json +3 -2
  14. package/speckit/README.en.md +20 -69
  15. package/templates/CLAUDE.template.md +5 -13
  16. package/tests/README.en.md +224 -0
  17. package/tests/README.md +338 -0
  18. package/tests/fixtures/project-context.aws.json +53 -0
  19. package/tests/fixtures/project-context.gcp.json +53 -0
  20. package/tests/integration/RUN_TESTS.md +185 -0
  21. package/tests/integration/__init__.py +0 -0
  22. package/tests/integration/test_hooks_integration.py +473 -0
  23. package/tests/integration/test_hooks_workflow.py +397 -0
  24. package/tests/permissions-validation/MANUAL_VALIDATION.md +434 -0
  25. package/tests/permissions-validation/test_permissions_validation.py +527 -0
  26. package/tests/system/__init__.py +0 -0
  27. package/tests/system/permissions_helpers.py +318 -0
  28. package/tests/system/test_agent_definitions.py +166 -0
  29. package/tests/system/test_configuration_files.py +121 -0
  30. package/tests/system/test_directory_structure.py +231 -0
  31. package/tests/system/test_permissions_system.py +1006 -0
  32. package/tests/tools/__init__.py +0 -0
  33. package/tests/tools/test_agent_router.py +266 -0
  34. package/tests/tools/test_clarify_engine.py +413 -0
  35. package/tests/tools/test_context_provider.py +157 -0
  36. package/tests/validators/__init__.py +0 -0
  37. package/tests/validators/test_approval_gate.py +415 -0
  38. package/tests/validators/test_commit_validator.py +446 -0
  39. package/tools/context_provider.py +28 -7
  40. package/tools/generate_embeddings.py +3 -3
  41. package/tools/semantic_matcher.py +2 -2
@@ -0,0 +1,157 @@
1
+ import pytest
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ # Calculate correct tools directory (2 levels up from tests/tools/)
8
+ TOOLS_DIR = Path(__file__).resolve().parents[2] / "tools"
9
+ if TOOLS_DIR.is_symlink():
10
+ TOOLS_DIR = TOOLS_DIR.resolve()
11
+
12
+ sys.path.insert(0, str(TOOLS_DIR))
13
+
14
+ @pytest.fixture
15
+ def temp_project_context(tmp_path: Path) -> Path:
16
+ """Creates a temporary project-context.json file for isolated testing."""
17
+ claude_dir = tmp_path / ".claude"
18
+ claude_dir.mkdir()
19
+ context_file = claude_dir / "project-context.json"
20
+
21
+ mock_context = {
22
+ "metadata": {
23
+ "version": "1.0",
24
+ "last_updated": "2025-01-01T00:00:00Z",
25
+ "project_name": "Test Project",
26
+ "cloud_provider": "GCP",
27
+ "environment": "non-prod"
28
+ },
29
+ "sections": {
30
+ "project_details": {"id": "test-project", "region": "us-central1"},
31
+ "terraform_infrastructure": {"layout": {"base_path": "infra/test"}},
32
+ "gitops_configuration": {"repository": {"path": "gitops/test"}},
33
+ "cluster_details": {"name": "test-cluster"},
34
+ "application_services": [
35
+ {"name": "frontend-app", "port": 80},
36
+ {"name": "backend-api", "port": 5000}
37
+ ],
38
+ "operational_guidelines": {"commit_standards": "conventional"},
39
+ "security_policies": {"iam": "strict"}
40
+ }
41
+ }
42
+
43
+ context_file.write_text(json.dumps(mock_context))
44
+ return context_file
45
+
46
+ def run_script(context_file: Path, agent: str, task: str) -> dict:
47
+ """Helper function to run the context_provider.py script and parse its output."""
48
+ script_path = TOOLS_DIR / "context_provider.py"
49
+
50
+ if not script_path.exists():
51
+ pytest.fail(f"context_provider.py not found at {script_path}")
52
+
53
+ process = subprocess.run(
54
+ [sys.executable, str(script_path), agent, task, "--context-file", str(context_file)],
55
+ capture_output=True,
56
+ text=True,
57
+ check=True,
58
+ cwd=context_file.parent.parent
59
+ )
60
+ return json.loads(process.stdout)
61
+
62
+ def test_terraform_architect_contract(temp_project_context: Path):
63
+ """Verify the terraform-architect gets its complete contract."""
64
+ agent = "terraform-architect"
65
+ task = "Create a new GCS bucket."
66
+
67
+ result = run_script(temp_project_context, agent, task)
68
+
69
+ assert "contract" in result
70
+ contract = result["contract"]
71
+
72
+ assert "project_details" in contract
73
+ assert "terraform_infrastructure" in contract
74
+ assert "operational_guidelines" in contract
75
+ assert "gitops_configuration" not in contract
76
+
77
+ def test_gitops_operator_contract(temp_project_context: Path):
78
+ """Verify the gitops-operator gets its complete contract."""
79
+ agent = "gitops-operator"
80
+ task = "Deploy the frontend-app to the cluster."
81
+
82
+ result = run_script(temp_project_context, agent, task)
83
+
84
+ assert "contract" in result
85
+ contract = result["contract"]
86
+
87
+ assert "project_details" in contract
88
+ assert "gitops_configuration" in contract
89
+ assert "cluster_details" in contract
90
+ assert "operational_guidelines" in contract
91
+ assert "terraform_infrastructure" not in contract
92
+
93
+ def test_troubleshooter_contract(temp_project_context: Path):
94
+ """Verify troubleshooters get the right contract (required fields only)."""
95
+ agent = "gcp-troubleshooter"
96
+ task = "Why is the backend-api crashing?"
97
+
98
+ result = run_script(temp_project_context, agent, task)
99
+
100
+ assert "contract" in result
101
+ contract = result["contract"]
102
+
103
+ # Check required fields per context-contracts.gcp.json
104
+ assert "project_details" in contract
105
+ assert "terraform_infrastructure" in contract
106
+ assert "gitops_configuration" in contract
107
+ # application_services is OPTIONAL per contract, not required
108
+
109
+ def test_enrichment_by_keyword(temp_project_context: Path):
110
+ """Verify enrichment adds relevant sections based on keywords."""
111
+ agent = "terraform-architect"
112
+ task = "Review our security_policies for the new bucket."
113
+
114
+ result = run_script(temp_project_context, agent, task)
115
+
116
+ assert "enrichment" in result
117
+ enrichment = result["enrichment"]
118
+
119
+ # Should include security_policies due to keyword match
120
+ assert "security_policies" in enrichment
121
+
122
+ def test_enrichment_by_service_name(temp_project_context: Path):
123
+ """Verify enrichment adds services when mentioned in task."""
124
+ agent = "gcp-troubleshooter"
125
+ task = "Check the logs for the frontend-app."
126
+
127
+ result = run_script(temp_project_context, agent, task)
128
+
129
+ assert "enrichment" in result
130
+ # Should recognize "frontend-app" and include it
131
+
132
+ def test_empty_enrichment(temp_project_context: Path):
133
+ """Verify enrichment is empty when no keywords match."""
134
+ agent = "terraform-architect"
135
+ task = "Generate a new storage unit."
136
+
137
+ result = run_script(temp_project_context, agent, task)
138
+
139
+ assert "enrichment" in result
140
+ # Enrichment may be empty or minimal
141
+
142
+ def test_invalid_agent(temp_project_context: Path):
143
+ """Verify script rejects invalid agent names."""
144
+ agent = "unknown-agent"
145
+ task = "Do something."
146
+
147
+ script_path = TOOLS_DIR / "context_provider.py"
148
+ process = subprocess.run(
149
+ [sys.executable, str(script_path), agent, task, "--context-file", str(temp_project_context)],
150
+ capture_output=True,
151
+ text=True
152
+ )
153
+
154
+ # Should fail with non-zero exit code
155
+ assert process.returncode != 0
156
+ # Error message should mention invalid agent
157
+ assert "invalid" in process.stderr.lower() or "unknown" in process.stderr.lower()
File without changes
@@ -0,0 +1,415 @@
1
+ """
2
+ Unit tests for approval_gate.py
3
+
4
+ Tests the Approval Gate enforcement mechanism that ensures no realization
5
+ occurs without explicit user approval.
6
+ """
7
+
8
+ import unittest
9
+ import json
10
+ import os
11
+ import tempfile
12
+ import sys
13
+ from datetime import datetime
14
+
15
+ # Add parent directory to path to import approval_gate
16
+ # From /home/jaguilar/aaxis/rnd/repositories/ops/.claude-shared/tests
17
+ # To /home/jaguilar/aaxis/rnd/repositories/.claude/tools
18
+ test_dir = os.path.dirname(os.path.abspath(__file__))
19
+ claude_tools_path = os.path.join(test_dir, '../../../.claude/tools')
20
+ sys.path.insert(0, claude_tools_path)
21
+
22
+ from approval_gate import (
23
+ ApprovalGate,
24
+ request_approval,
25
+ process_approval_response
26
+ )
27
+
28
+
29
+ class TestApprovalGate(unittest.TestCase):
30
+ """Test cases for ApprovalGate class."""
31
+
32
+ def setUp(self):
33
+ """Set up test fixtures."""
34
+ # Create temporary log directory
35
+ self.temp_dir = tempfile.mkdtemp()
36
+ self.log_path = os.path.join(self.temp_dir, "approvals.jsonl")
37
+
38
+ # Create ApprovalGate instance with custom log path
39
+ self.gate = ApprovalGate()
40
+ self.gate.approval_log_path = self.log_path
41
+
42
+ # Sample realization package
43
+ self.realization_package = {
44
+ "files": [
45
+ {"path": "releases/pg-non-prod/admin-ui/release.yaml", "action": "create"},
46
+ {"path": "releases/pg-non-prod/query-api/release.yaml", "action": "create"},
47
+ {"path": "releases/pg-non-prod/admin-api/release.yaml", "action": "create"}
48
+ ],
49
+ "git_operations": {
50
+ "commit_message": "feat(helmrelease): add Phase 3.3 services",
51
+ "branch": "main",
52
+ "remote": "origin"
53
+ },
54
+ "resources_affected": {
55
+ "HelmReleases": ["pg-admin-ui", "pg-query-api", "pg-admin-api"]
56
+ }
57
+ }
58
+
59
+ def tearDown(self):
60
+ """Clean up test fixtures."""
61
+ # Remove temporary log file
62
+ if os.path.exists(self.log_path):
63
+ os.remove(self.log_path)
64
+ os.rmdir(self.temp_dir)
65
+
66
+ def test_generate_summary(self):
67
+ """Test that summary generation includes all key information."""
68
+ summary = self.gate.generate_summary(self.realization_package)
69
+
70
+ # Check that summary contains key sections
71
+ self.assertIn("REALIZATION PACKAGE", summary)
72
+ self.assertIn("Archivos a crear/modificar:", summary)
73
+ self.assertIn("Git Operations:", summary)
74
+ self.assertIn("Recursos Afectados", summary)
75
+
76
+ # Check specific details
77
+ self.assertIn("admin-ui", summary)
78
+ self.assertIn("feat(helmrelease)", summary)
79
+ self.assertIn("git push", summary)
80
+
81
+ def test_generate_approval_question(self):
82
+ """Test that approval question is correctly structured."""
83
+ question_config = self.gate.generate_approval_question(
84
+ self.realization_package,
85
+ "gitops-operator",
86
+ "Phase 3.3"
87
+ )
88
+
89
+ # Check structure
90
+ self.assertIn("questions", question_config)
91
+ self.assertEqual(len(question_config["questions"]), 1)
92
+
93
+ question = question_config["questions"][0]
94
+
95
+ # Check question fields
96
+ self.assertIn("question", question)
97
+ self.assertIn("header", question)
98
+ self.assertIn("multiSelect", question)
99
+ self.assertIn("options", question)
100
+
101
+ # Check header
102
+ self.assertEqual(question["header"], "Approval")
103
+
104
+ # Check multiSelect is False
105
+ self.assertFalse(question["multiSelect"])
106
+
107
+ # Check options (should have exactly 2: Aprobar and Rechazar)
108
+ self.assertEqual(len(question["options"]), 2)
109
+
110
+ labels = [opt["label"] for opt in question["options"]]
111
+ self.assertIn("✅ Aprobar y ejecutar", labels)
112
+ self.assertIn("❌ Rechazar", labels)
113
+
114
+ def test_get_critical_operations_git_only(self):
115
+ """Test extraction of critical operations (git only)."""
116
+ ops = self.gate._get_critical_operations(self.realization_package)
117
+ self.assertEqual(ops, "git push origin main")
118
+
119
+ def test_get_critical_operations_multiple(self):
120
+ """Test extraction with multiple operation types."""
121
+ package = {
122
+ **self.realization_package,
123
+ "kubectl_operations": ["apply -f release.yaml"],
124
+ "terraform_operations": {"command": "apply"}
125
+ }
126
+
127
+ ops = self.gate._get_critical_operations(package)
128
+
129
+ # Should contain all operation types
130
+ self.assertIn("git push", ops)
131
+ self.assertIn("kubectl apply", ops)
132
+ self.assertIn("terraform apply", ops)
133
+
134
+ def test_get_operation_count(self):
135
+ """Test counting of total operations."""
136
+ count = self.gate._get_operation_count(self.realization_package)
137
+
138
+ # 3 files + 2 git operations (commit + push) = 5
139
+ self.assertEqual(count, 5)
140
+
141
+ def test_validate_approval_response_approved(self):
142
+ """Test validation of approved response."""
143
+ validation = self.gate.validate_approval_response("✅ Aprobar y ejecutar")
144
+
145
+ self.assertTrue(validation["approved"])
146
+ self.assertEqual(validation["action"], "proceed_to_realization")
147
+ self.assertIn("Procediendo", validation["message"])
148
+
149
+ def test_validate_approval_response_rejected(self):
150
+ """Test validation of rejected response."""
151
+ validation = self.gate.validate_approval_response("❌ Rechazar")
152
+
153
+ self.assertFalse(validation["approved"])
154
+ self.assertEqual(validation["action"], "halt_workflow")
155
+ self.assertIn("rechazada", validation["message"])
156
+
157
+ def test_validate_approval_response_custom(self):
158
+ """Test validation of custom (Other) response."""
159
+ validation = self.gate.validate_approval_response("Wait, let me review first")
160
+
161
+ self.assertFalse(validation["approved"])
162
+ self.assertEqual(validation["action"], "clarify_with_user")
163
+ self.assertIn("no estándar", validation["message"])
164
+ self.assertEqual(validation["user_input"], "Wait, let me review first")
165
+
166
+ def test_log_approval(self):
167
+ """Test that approval decisions are logged correctly."""
168
+ self.gate.log_approval(
169
+ realization_package=self.realization_package,
170
+ user_response="✅ Aprobar y ejecutar",
171
+ approved=True,
172
+ agent_name="gitops-operator",
173
+ phase="Phase 3.3"
174
+ )
175
+
176
+ # Check that log file was created
177
+ self.assertTrue(os.path.exists(self.log_path))
178
+
179
+ # Read and parse log entry
180
+ with open(self.log_path, 'r') as f:
181
+ log_entry = json.loads(f.readline())
182
+
183
+ # Verify log entry contents
184
+ self.assertEqual(log_entry["agent"], "gitops-operator")
185
+ self.assertEqual(log_entry["phase"], "Phase 3.3")
186
+ self.assertTrue(log_entry["approved"])
187
+ self.assertEqual(log_entry["user_response"], "✅ Aprobar y ejecutar")
188
+ self.assertEqual(log_entry["files_count"], 3)
189
+ self.assertIn("git push", log_entry["operations"])
190
+
191
+ # Verify timestamp format
192
+ datetime.fromisoformat(log_entry["timestamp"]) # Should not raise
193
+
194
+ def test_log_multiple_approvals(self):
195
+ """Test that multiple approvals are appended to log."""
196
+ # First approval
197
+ self.gate.log_approval(
198
+ self.realization_package,
199
+ "✅ Aprobar y ejecutar",
200
+ True,
201
+ "gitops-operator",
202
+ "Phase 3.3"
203
+ )
204
+
205
+ # Second approval
206
+ self.gate.log_approval(
207
+ self.realization_package,
208
+ "❌ Rechazar",
209
+ False,
210
+ "terraform-architect",
211
+ "Phase 3.1"
212
+ )
213
+
214
+ # Read all log entries
215
+ with open(self.log_path, 'r') as f:
216
+ lines = f.readlines()
217
+
218
+ self.assertEqual(len(lines), 2)
219
+
220
+ # Parse entries
221
+ entry1 = json.loads(lines[0])
222
+ entry2 = json.loads(lines[1])
223
+
224
+ self.assertTrue(entry1["approved"])
225
+ self.assertFalse(entry2["approved"])
226
+ self.assertEqual(entry1["agent"], "gitops-operator")
227
+ self.assertEqual(entry2["agent"], "terraform-architect")
228
+
229
+
230
+ class TestConvenienceFunctions(unittest.TestCase):
231
+ """Test cases for module-level convenience functions."""
232
+
233
+ def setUp(self):
234
+ """Set up test fixtures."""
235
+ self.realization_package = {
236
+ "files": [
237
+ {"path": "infrastructure/main.tf", "action": "modify"}
238
+ ],
239
+ "terraform_operations": {
240
+ "command": "apply",
241
+ "path": "infrastructure/"
242
+ }
243
+ }
244
+
245
+ def test_request_approval(self):
246
+ """Test request_approval convenience function."""
247
+ result = request_approval(
248
+ realization_package=self.realization_package,
249
+ agent_name="terraform-architect",
250
+ phase="Phase 3.1"
251
+ )
252
+
253
+ # Check returned structure
254
+ self.assertIn("summary", result)
255
+ self.assertIn("question_config", result)
256
+ self.assertIn("gate_instance", result)
257
+
258
+ # Check that summary is a string
259
+ self.assertIsInstance(result["summary"], str)
260
+
261
+ # Check that question_config has correct structure
262
+ self.assertIn("questions", result["question_config"])
263
+
264
+ # Check that gate_instance is an ApprovalGate
265
+ self.assertIsInstance(result["gate_instance"], ApprovalGate)
266
+
267
+ def test_process_approval_response_approved(self):
268
+ """Test process_approval_response with approved response."""
269
+ # First request approval
270
+ approval_data = request_approval(
271
+ realization_package=self.realization_package,
272
+ agent_name="terraform-architect",
273
+ phase="Phase 3.1"
274
+ )
275
+
276
+ # Override log path to temp file
277
+ temp_log = tempfile.NamedTemporaryFile(delete=False, suffix='.jsonl')
278
+ temp_log.close()
279
+ approval_data["gate_instance"].approval_log_path = temp_log.name
280
+
281
+ try:
282
+ # Process approval
283
+ validation = process_approval_response(
284
+ gate_instance=approval_data["gate_instance"],
285
+ user_response="✅ Aprobar y ejecutar",
286
+ realization_package=self.realization_package,
287
+ agent_name="terraform-architect",
288
+ phase="Phase 3.1"
289
+ )
290
+
291
+ # Check validation result
292
+ self.assertTrue(validation["approved"])
293
+ self.assertEqual(validation["action"], "proceed_to_realization")
294
+
295
+ # Verify log was created
296
+ with open(temp_log.name, 'r') as f:
297
+ log_entry = json.loads(f.readline())
298
+
299
+ self.assertEqual(log_entry["agent"], "terraform-architect")
300
+ self.assertTrue(log_entry["approved"])
301
+
302
+ finally:
303
+ # Clean up temp file
304
+ os.remove(temp_log.name)
305
+
306
+ def test_process_approval_response_rejected(self):
307
+ """Test process_approval_response with rejected response."""
308
+ approval_data = request_approval(
309
+ realization_package=self.realization_package,
310
+ agent_name="terraform-architect",
311
+ phase="Phase 3.1"
312
+ )
313
+
314
+ # Override log path
315
+ temp_log = tempfile.NamedTemporaryFile(delete=False, suffix='.jsonl')
316
+ temp_log.close()
317
+ approval_data["gate_instance"].approval_log_path = temp_log.name
318
+
319
+ try:
320
+ validation = process_approval_response(
321
+ gate_instance=approval_data["gate_instance"],
322
+ user_response="❌ Rechazar",
323
+ realization_package=self.realization_package,
324
+ agent_name="terraform-architect",
325
+ phase="Phase 3.1"
326
+ )
327
+
328
+ # Check validation result
329
+ self.assertFalse(validation["approved"])
330
+ self.assertEqual(validation["action"], "halt_workflow")
331
+
332
+ # Verify log was created with rejected status
333
+ with open(temp_log.name, 'r') as f:
334
+ log_entry = json.loads(f.readline())
335
+
336
+ self.assertFalse(log_entry["approved"])
337
+
338
+ finally:
339
+ os.remove(temp_log.name)
340
+
341
+
342
+ class TestEdgeCases(unittest.TestCase):
343
+ """Test edge cases and error conditions."""
344
+
345
+ def test_empty_realization_package(self):
346
+ """Test handling of empty realization package."""
347
+ gate = ApprovalGate()
348
+ empty_package = {}
349
+
350
+ # Should not crash
351
+ summary = gate.generate_summary(empty_package)
352
+ self.assertIsInstance(summary, str)
353
+
354
+ ops = gate._get_critical_operations(empty_package)
355
+ self.assertEqual(ops, "cambios al repositorio")
356
+
357
+ count = gate._get_operation_count(empty_package)
358
+ self.assertEqual(count, 0)
359
+
360
+ def test_realization_package_with_validation_warnings(self):
361
+ """Test summary includes validation warnings if present."""
362
+ gate = ApprovalGate()
363
+ package = {
364
+ "files": [{"path": "test.yaml", "action": "create"}],
365
+ "validation_results": {
366
+ "status": "passed_with_warnings",
367
+ "warnings": [
368
+ "Image tag 'latest' is not recommended",
369
+ "Resource limits not set"
370
+ ]
371
+ }
372
+ }
373
+
374
+ summary = gate.generate_summary(package)
375
+
376
+ # Should include validation section
377
+ self.assertIn("Pre-Deployment Validation", summary)
378
+ self.assertIn("Warnings:", summary)
379
+ self.assertIn("Image tag", summary)
380
+
381
+ def test_realization_package_with_estimated_impact(self):
382
+ """Test summary includes estimated impact if present."""
383
+ gate = ApprovalGate()
384
+ package = {
385
+ "files": [{"path": "test.yaml", "action": "create"}],
386
+ "estimated_impact": {
387
+ "downtime": "~5 minutes",
388
+ "risk_level": "Medium"
389
+ }
390
+ }
391
+
392
+ summary = gate.generate_summary(package)
393
+
394
+ # Should include impact section
395
+ self.assertIn("Estimated Impact", summary)
396
+ self.assertIn("Downtime:", summary)
397
+ self.assertIn("Risk Level:", summary)
398
+
399
+ def test_many_files_truncated_in_summary(self):
400
+ """Test that summary truncates long file lists."""
401
+ gate = ApprovalGate()
402
+
403
+ # Create package with 15 files
404
+ files = [{"path": f"file{i}.yaml", "action": "create"} for i in range(15)]
405
+ package = {"files": files}
406
+
407
+ summary = gate.generate_summary(package)
408
+
409
+ # Should show "... y X archivos más"
410
+ self.assertIn("y 5 archivos más", summary)
411
+ self.assertIn("15", summary) # Total count
412
+
413
+
414
+ if __name__ == '__main__':
415
+ unittest.main()