@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,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()
|