cloudshellgpt 1.0.0__tar.gz
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.
- cloudshellgpt-1.0.0/.github/workflows/ci.yml +29 -0
- cloudshellgpt-1.0.0/.gitignore +67 -0
- cloudshellgpt-1.0.0/.kiro/hooks/lint-on-save.json +13 -0
- cloudshellgpt-1.0.0/.kiro/hooks/test-mirror-reminder.json +13 -0
- cloudshellgpt-1.0.0/.kiro/settings/mcp.json +13 -0
- cloudshellgpt-1.0.0/.kiro/skills/pytest-unit-creator.md +336 -0
- cloudshellgpt-1.0.0/.kiro/skills/python-module-scaffold.md +138 -0
- cloudshellgpt-1.0.0/.kiro/specs/00-overview.md +104 -0
- cloudshellgpt-1.0.0/.kiro/specs/01-architecture.md +413 -0
- cloudshellgpt-1.0.0/.kiro/specs/02-acceptance-criteria.md +346 -0
- cloudshellgpt-1.0.0/.kiro/specs/03-implementation-plan.md +431 -0
- cloudshellgpt-1.0.0/.kiro/specs/03-implementation-plan.meta.json +60 -0
- cloudshellgpt-1.0.0/.kiro/steering/aws-conventions.md +77 -0
- cloudshellgpt-1.0.0/.kiro/steering/code-style.md +79 -0
- cloudshellgpt-1.0.0/.kiro/steering/commit-conventions.md +109 -0
- cloudshellgpt-1.0.0/.kiro/steering/language.md +20 -0
- cloudshellgpt-1.0.0/.kiro/steering/mcp-development.md +90 -0
- cloudshellgpt-1.0.0/.kiro/steering/project-context.md +145 -0
- cloudshellgpt-1.0.0/.kiro/steering/safety-patterns.md +137 -0
- cloudshellgpt-1.0.0/.kiro/steering/testing-guide.md +186 -0
- cloudshellgpt-1.0.0/.pre-commit-config.yaml +32 -0
- cloudshellgpt-1.0.0/LICENSE +198 -0
- cloudshellgpt-1.0.0/PKG-INFO +426 -0
- cloudshellgpt-1.0.0/README.md +379 -0
- cloudshellgpt-1.0.0/VIDEO_PITCH.md +385 -0
- cloudshellgpt-1.0.0/docs/DEMO_SCRIPT.md +531 -0
- cloudshellgpt-1.0.0/docs/IAM_PERMISSIONS.md +552 -0
- cloudshellgpt-1.0.0/infrastructure/app.py +27 -0
- cloudshellgpt-1.0.0/infrastructure/cdk.json +28 -0
- cloudshellgpt-1.0.0/infrastructure/lib/cloudshellgpt_stack.py +309 -0
- cloudshellgpt-1.0.0/prueba.pdf +0 -0
- cloudshellgpt-1.0.0/pyproject.toml +121 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/__init__.py +11 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/audit.py +175 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/bedrock_translator.py +525 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/cli.py +612 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/config.py +296 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/cost.py +443 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/executor.py +382 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/formatter.py +328 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/i18n.py +203 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/intent.py +1080 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/learning.py +969 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/mcp_server.py +264 -0
- cloudshellgpt-1.0.0/src/cloudshellgpt/safety.py +952 -0
- cloudshellgpt-1.0.0/tests/__init__.py +1 -0
- cloudshellgpt-1.0.0/tests/conftest.py +265 -0
- cloudshellgpt-1.0.0/tests/eval/__init__.py +1 -0
- cloudshellgpt-1.0.0/tests/eval/test_eval.py +401 -0
- cloudshellgpt-1.0.0/tests/eval/translation_eval.yaml +1086 -0
- cloudshellgpt-1.0.0/tests/eval/validate_distribution.py +275 -0
- cloudshellgpt-1.0.0/tests/integration/__init__.py +1 -0
- cloudshellgpt-1.0.0/tests/integration/test_e2e.py +476 -0
- cloudshellgpt-1.0.0/tests/unit/__init__.py +1 -0
- cloudshellgpt-1.0.0/tests/unit/test_audit.py +298 -0
- cloudshellgpt-1.0.0/tests/unit/test_bedrock.py +1216 -0
- cloudshellgpt-1.0.0/tests/unit/test_bedrock_error.py +543 -0
- cloudshellgpt-1.0.0/tests/unit/test_config.py +300 -0
- cloudshellgpt-1.0.0/tests/unit/test_cost.py +664 -0
- cloudshellgpt-1.0.0/tests/unit/test_executor.py +1243 -0
- cloudshellgpt-1.0.0/tests/unit/test_formatter.py +1081 -0
- cloudshellgpt-1.0.0/tests/unit/test_intent.py +977 -0
- cloudshellgpt-1.0.0/tests/unit/test_learning.py +537 -0
- cloudshellgpt-1.0.0/tests/unit/test_mcp_serve.py +239 -0
- cloudshellgpt-1.0.0/tests/unit/test_mcp_server.py +430 -0
- cloudshellgpt-1.0.0/tests/unit/test_mcp_tool_contracts.py +1197 -0
- cloudshellgpt-1.0.0/tests/unit/test_safety.py +1882 -0
- cloudshellgpt-1.0.0/tests/unit/test_smoke.py +27 -0
- cloudshellgpt-1.0.0/uv.lock +2363 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, dev]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, dev]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: astral-sh/setup-uv@v3
|
|
16
|
+
with:
|
|
17
|
+
version: "latest"
|
|
18
|
+
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: uv sync --all-extras
|
|
21
|
+
|
|
22
|
+
- name: Lint (ruff check)
|
|
23
|
+
run: uv run ruff check .
|
|
24
|
+
|
|
25
|
+
- name: Format check (ruff format)
|
|
26
|
+
run: uv run ruff format --check .
|
|
27
|
+
|
|
28
|
+
- name: Unit tests
|
|
29
|
+
run: uv run pytest tests/unit/ --no-cov -q --ignore=tests/unit/test_executor.py -k "not TestLLMIndependence and not TestAssessIntegration and not test_llm_high_stays_high"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
.env
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
*~
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
desktop.ini
|
|
29
|
+
|
|
30
|
+
# AWS & Secrets
|
|
31
|
+
.aws/
|
|
32
|
+
*.pem
|
|
33
|
+
*.key
|
|
34
|
+
credentials
|
|
35
|
+
.env*
|
|
36
|
+
!.env.example
|
|
37
|
+
|
|
38
|
+
# Testing
|
|
39
|
+
.coverage
|
|
40
|
+
htmlcov/
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
.mypy_cache/
|
|
43
|
+
|
|
44
|
+
# CDK
|
|
45
|
+
cdk.out/
|
|
46
|
+
*.js
|
|
47
|
+
!infrastructure/cdk.json
|
|
48
|
+
|
|
49
|
+
# Node (if CDK uses node)
|
|
50
|
+
node_modules/
|
|
51
|
+
|
|
52
|
+
# Local config (user-specific)
|
|
53
|
+
~/.csgpt/
|
|
54
|
+
|
|
55
|
+
# Logs
|
|
56
|
+
*.log
|
|
57
|
+
|
|
58
|
+
# Ruff cache
|
|
59
|
+
.ruff_cache/
|
|
60
|
+
|
|
61
|
+
# Temporary output files
|
|
62
|
+
output.txt
|
|
63
|
+
uv_output2.txt
|
|
64
|
+
|
|
65
|
+
# Distribution
|
|
66
|
+
*.tar.gz
|
|
67
|
+
*.whl
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Lint on Save",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Runs ruff check on saved Python files",
|
|
5
|
+
"when": {
|
|
6
|
+
"type": "fileEdited",
|
|
7
|
+
"patterns": ["*.py"]
|
|
8
|
+
},
|
|
9
|
+
"then": {
|
|
10
|
+
"type": "runCommand",
|
|
11
|
+
"command": "ruff check --fix {file}"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Test Mirror Reminder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reminds to update tests when source files change",
|
|
5
|
+
"when": {
|
|
6
|
+
"type": "fileEdited",
|
|
7
|
+
"patterns": ["src/cloudshellgpt/*.py"]
|
|
8
|
+
},
|
|
9
|
+
"then": {
|
|
10
|
+
"type": "askAgent",
|
|
11
|
+
"prompt": "A source file was modified. Check if the corresponding test file in tests/unit/ needs to be updated to match the changes."
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
inclusion: fileMatch
|
|
3
|
+
fileMatchPattern: "tests/**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Pytest Unit Creator
|
|
7
|
+
|
|
8
|
+
Genera tests unitarios para módulos de CloudShellGPT siguiendo las convenciones del testing-guide.
|
|
9
|
+
|
|
10
|
+
## Cuándo usar
|
|
11
|
+
|
|
12
|
+
Cuando necesites crear tests para un módulo en `src/cloudshellgpt/`. Cada módulo debe tener su test mirror en `tests/unit/`.
|
|
13
|
+
|
|
14
|
+
## Mapeo de archivos
|
|
15
|
+
|
|
16
|
+
| Módulo | Test |
|
|
17
|
+
|--------|------|
|
|
18
|
+
| `src/cloudshellgpt/intent.py` | `tests/unit/test_intent.py` |
|
|
19
|
+
| `src/cloudshellgpt/bedrock_translator.py` | `tests/unit/test_bedrock.py` |
|
|
20
|
+
| `src/cloudshellgpt/safety.py` | `tests/unit/test_safety.py` |
|
|
21
|
+
| `src/cloudshellgpt/cost.py` | `tests/unit/test_cost.py` |
|
|
22
|
+
| `src/cloudshellgpt/executor.py` | `tests/unit/test_executor.py` |
|
|
23
|
+
| `src/cloudshellgpt/formatter.py` | `tests/unit/test_formatter.py` |
|
|
24
|
+
| `src/cloudshellgpt/audit.py` | `tests/unit/test_audit.py` |
|
|
25
|
+
| `src/cloudshellgpt/learning.py` | `tests/unit/test_learning.py` |
|
|
26
|
+
| `src/cloudshellgpt/config.py` | `tests/unit/test_config.py` |
|
|
27
|
+
|
|
28
|
+
## Template base de archivo test
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
"""Tests unitarios para <module_name>."""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from unittest.mock import MagicMock, patch
|
|
36
|
+
|
|
37
|
+
import pytest
|
|
38
|
+
|
|
39
|
+
from cloudshellgpt.<module> import <ClassUnderTest>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestClassName:
|
|
43
|
+
"""Tests para ClassName."""
|
|
44
|
+
|
|
45
|
+
def test_action_condition_expected_result(self) -> None:
|
|
46
|
+
"""Descripción clara de qué verifica este test."""
|
|
47
|
+
# Arrange
|
|
48
|
+
input_data = "..."
|
|
49
|
+
|
|
50
|
+
# Act
|
|
51
|
+
result = function_under_test(input_data)
|
|
52
|
+
|
|
53
|
+
# Assert
|
|
54
|
+
assert result.field == expected_value
|
|
55
|
+
|
|
56
|
+
def test_another_scenario(self) -> None:
|
|
57
|
+
"""Otro escenario."""
|
|
58
|
+
...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Naming convention para tests
|
|
62
|
+
|
|
63
|
+
Formato: `test_<acción>_<condición>_<resultado_esperado>`
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# Buenos nombres:
|
|
67
|
+
def test_parse_spanish_list_intent_returns_high_confidence(): ...
|
|
68
|
+
def test_destructive_command_upgrades_risk_to_critical(): ...
|
|
69
|
+
def test_executor_rejects_pipe_in_command(): ...
|
|
70
|
+
def test_cost_explorer_failure_returns_unknown_status(): ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Malos nombres:
|
|
74
|
+
def test_1(): ...
|
|
75
|
+
def test_intent(): ...
|
|
76
|
+
def test_it_works(): ...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Fixtures — usar conftest.py
|
|
80
|
+
|
|
81
|
+
Fixtures compartidas van en `tests/conftest.py`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
"""Fixtures compartidas para tests."""
|
|
85
|
+
|
|
86
|
+
from __future__ import annotations
|
|
87
|
+
|
|
88
|
+
from pathlib import Path
|
|
89
|
+
|
|
90
|
+
import pytest
|
|
91
|
+
|
|
92
|
+
from cloudshellgpt.config import Config
|
|
93
|
+
from cloudshellgpt.intent import Intent
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture
|
|
97
|
+
def tmp_config(tmp_path: Path) -> Config:
|
|
98
|
+
"""Provides a temporary config for testing."""
|
|
99
|
+
from cloudshellgpt.config import ConfigManager
|
|
100
|
+
|
|
101
|
+
return ConfigManager(config_path=tmp_path / "config.yaml")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.fixture
|
|
105
|
+
def sample_intent() -> Intent:
|
|
106
|
+
"""Provides a sample Intent for testing."""
|
|
107
|
+
return Intent(
|
|
108
|
+
action="list",
|
|
109
|
+
service="s3",
|
|
110
|
+
confidence=0.9,
|
|
111
|
+
raw_input="lista los buckets de S3",
|
|
112
|
+
detected_language="es",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.fixture
|
|
117
|
+
def sample_translation() -> Translation:
|
|
118
|
+
"""Provides a sample Translation for testing."""
|
|
119
|
+
from cloudshellgpt.bedrock_translator import Translation
|
|
120
|
+
|
|
121
|
+
return Translation(
|
|
122
|
+
command="aws s3api list-buckets --output json",
|
|
123
|
+
explanation="Lista todos los buckets",
|
|
124
|
+
detailed_explanation="Usa la API ListBuckets de S3",
|
|
125
|
+
risk_level="low",
|
|
126
|
+
estimated_cost="$0.00",
|
|
127
|
+
requires_dry_run=False,
|
|
128
|
+
affected_resources=["s3:*"],
|
|
129
|
+
flags_used=["--output json"],
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Mocking AWS — patrones
|
|
134
|
+
|
|
135
|
+
### Moto para servicios AWS completos
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
import pytest
|
|
139
|
+
from moto import mock_aws
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@mock_aws
|
|
143
|
+
def test_cost_explorer_returns_estimate() -> None:
|
|
144
|
+
"""Cost Explorer mock retorna estimación válida."""
|
|
145
|
+
# moto intercepta boto3 calls automáticamente
|
|
146
|
+
import boto3
|
|
147
|
+
|
|
148
|
+
client = boto3.client("ce", region_name="us-east-1")
|
|
149
|
+
# ... setup y assertions
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### unittest.mock para subprocess (executor)
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from unittest.mock import MagicMock, patch
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_executor_runs_aws_command() -> None:
|
|
159
|
+
"""Executor ejecuta comando y retorna resultado."""
|
|
160
|
+
with patch("subprocess.run") as mock_run:
|
|
161
|
+
mock_run.return_value = MagicMock(
|
|
162
|
+
stdout='{"Buckets": []}',
|
|
163
|
+
stderr="",
|
|
164
|
+
returncode=0,
|
|
165
|
+
)
|
|
166
|
+
executor = AWSExecutor()
|
|
167
|
+
result = executor.run("aws s3api list-buckets")
|
|
168
|
+
|
|
169
|
+
assert result.exit_code == 0
|
|
170
|
+
assert result.stdout == '{"Buckets": []}'
|
|
171
|
+
mock_run.assert_called_once()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Mock de Bedrock (translator)
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from unittest.mock import MagicMock, patch
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_translator_calls_converse_api() -> None:
|
|
181
|
+
"""Translator usa Converse API, no invoke_model."""
|
|
182
|
+
with patch("boto3.client") as mock_boto:
|
|
183
|
+
mock_client = MagicMock()
|
|
184
|
+
mock_boto.return_value = mock_client
|
|
185
|
+
mock_client.converse.return_value = {
|
|
186
|
+
"output": {
|
|
187
|
+
"message": {"content": [{"text": '{"command": "aws s3 ls", "explanation": "..."}'}]}
|
|
188
|
+
},
|
|
189
|
+
"usage": {"inputTokens": 100, "outputTokens": 50},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
translator = BedrockTranslator()
|
|
193
|
+
result = translator.translate(sample_intent)
|
|
194
|
+
|
|
195
|
+
mock_client.converse.assert_called_once()
|
|
196
|
+
# Verificar que NO se usó invoke_model
|
|
197
|
+
mock_client.invoke_model.assert_not_called()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Patrones por módulo
|
|
201
|
+
|
|
202
|
+
### Tests de IntentParser (sin mocks — lógica pura)
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
class TestIntentParser:
|
|
206
|
+
"""Tests para IntentParser — no requiere mocks AWS."""
|
|
207
|
+
|
|
208
|
+
def test_parse_spanish_s3_list(self) -> None:
|
|
209
|
+
parser = IntentParser()
|
|
210
|
+
intent = parser.parse("lista los buckets de S3")
|
|
211
|
+
assert intent.service == "s3"
|
|
212
|
+
assert intent.action == "list"
|
|
213
|
+
assert intent.detected_language == "es"
|
|
214
|
+
assert intent.confidence >= 0.85
|
|
215
|
+
|
|
216
|
+
def test_parse_english_ec2_create(self) -> None:
|
|
217
|
+
parser = IntentParser()
|
|
218
|
+
intent = parser.parse("create a t3.micro EC2 instance")
|
|
219
|
+
assert intent.service == "ec2"
|
|
220
|
+
assert intent.action == "create"
|
|
221
|
+
assert intent.detected_language == "en"
|
|
222
|
+
|
|
223
|
+
def test_ambiguous_input_low_confidence(self) -> None:
|
|
224
|
+
parser = IntentParser()
|
|
225
|
+
intent = parser.parse("muéstrame las cosas")
|
|
226
|
+
assert intent.confidence < 0.7
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Tests de SafetyLayer (critical path — > 90% coverage)
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
class TestSafetyLayer:
|
|
233
|
+
"""Tests para SafetyLayer — critical path."""
|
|
234
|
+
|
|
235
|
+
def test_read_only_command_is_low(self) -> None:
|
|
236
|
+
safety = SafetyLayer()
|
|
237
|
+
check = safety.evaluate("aws s3api list-buckets")
|
|
238
|
+
assert check.risk_level == "low"
|
|
239
|
+
assert check.requires_confirmation is False
|
|
240
|
+
|
|
241
|
+
def test_delete_command_is_high(self) -> None:
|
|
242
|
+
safety = SafetyLayer()
|
|
243
|
+
check = safety.evaluate("aws s3api delete-bucket --bucket prod")
|
|
244
|
+
assert check.risk_level == "high"
|
|
245
|
+
assert check.requires_confirmation is True
|
|
246
|
+
|
|
247
|
+
def test_recursive_delete_is_critical(self) -> None:
|
|
248
|
+
safety = SafetyLayer()
|
|
249
|
+
check = safety.evaluate("aws s3 rm s3://prod --recursive")
|
|
250
|
+
assert check.risk_level == "critical"
|
|
251
|
+
assert check.requires_dry_run is True
|
|
252
|
+
|
|
253
|
+
def test_never_downgrades_below_llm_risk(self) -> None:
|
|
254
|
+
"""Si LLM dice high, safety no puede poner medium."""
|
|
255
|
+
safety = SafetyLayer()
|
|
256
|
+
check = safety.evaluate("aws s3api list-buckets", llm_risk="high")
|
|
257
|
+
assert check.risk_level in ("high", "critical")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Tests de Executor (critical path — > 90% coverage)
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
class TestAWSExecutor:
|
|
264
|
+
"""Tests para AWSExecutor — shell injection prevention."""
|
|
265
|
+
|
|
266
|
+
@pytest.mark.parametrize("metachar", ["|", "&&", "||", ";", "`", "$(", ">", ">>", "<"])
|
|
267
|
+
def test_rejects_shell_metacharacter(self, metachar: str) -> None:
|
|
268
|
+
executor = AWSExecutor()
|
|
269
|
+
with pytest.raises(ExecutorError, match="metacharacter"):
|
|
270
|
+
executor.run(f"aws s3 ls {metachar} malicious")
|
|
271
|
+
|
|
272
|
+
def test_accepts_stdin_dash_argument(self) -> None:
|
|
273
|
+
"""Argumento `-` es válido en posición de argumento."""
|
|
274
|
+
with patch("subprocess.run") as mock_run:
|
|
275
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
276
|
+
executor = AWSExecutor()
|
|
277
|
+
result = executor.run("aws s3 cp - s3://bucket/file")
|
|
278
|
+
assert result.exit_code == 0
|
|
279
|
+
|
|
280
|
+
def test_rejects_non_aws_command(self) -> None:
|
|
281
|
+
executor = AWSExecutor()
|
|
282
|
+
with pytest.raises(ExecutorError):
|
|
283
|
+
executor.run("rm -rf /")
|
|
284
|
+
|
|
285
|
+
def test_timeout_kills_process(self) -> None:
|
|
286
|
+
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("aws", 30)):
|
|
287
|
+
executor = AWSExecutor(timeout=30)
|
|
288
|
+
with pytest.raises(ExecutorError, match="timeout"):
|
|
289
|
+
executor.run("aws s3 sync s3://bucket .")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Reglas obligatorias
|
|
293
|
+
|
|
294
|
+
1. **NUNCA** hit real AWS en unit tests — siempre moto o unittest.mock
|
|
295
|
+
2. **Type annotations** en todos los tests (→ None para test methods)
|
|
296
|
+
3. **Docstring** en cada test explicando qué verifica
|
|
297
|
+
4. **Arrange → Act → Assert** como estructura interna
|
|
298
|
+
5. **Un assert principal** por test (asserts auxiliares OK para setup validation)
|
|
299
|
+
6. **`@pytest.mark.parametrize`** para variaciones del mismo escenario
|
|
300
|
+
7. **`@pytest.mark.integration`** solo en `tests/integration/` (nunca en unit)
|
|
301
|
+
8. **Fixtures** para setup compartido, nunca setup en el test body repetido
|
|
302
|
+
9. **Nombres descriptivos** — el nombre del test debe explicar el escenario sin leer el body
|
|
303
|
+
|
|
304
|
+
## Ejecución
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Todos los unit tests
|
|
308
|
+
pytest tests/unit/ -v
|
|
309
|
+
|
|
310
|
+
# Un archivo específico
|
|
311
|
+
pytest tests/unit/test_safety.py -v
|
|
312
|
+
|
|
313
|
+
# Un test específico
|
|
314
|
+
pytest tests/unit/test_safety.py::TestSafetyLayer::test_recursive_delete_is_critical -v
|
|
315
|
+
|
|
316
|
+
# Con coverage
|
|
317
|
+
pytest --cov=cloudshellgpt --cov-report=html tests/unit/
|
|
318
|
+
|
|
319
|
+
# Solo tests que matchean nombre
|
|
320
|
+
pytest -k "test_parse_spanish" -v
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Coverage targets
|
|
324
|
+
|
|
325
|
+
| Módulo | Target |
|
|
326
|
+
|--------|--------|
|
|
327
|
+
| `safety.py` | > 90% |
|
|
328
|
+
| `executor.py` | > 90% |
|
|
329
|
+
| `intent.py` | > 80% |
|
|
330
|
+
| `bedrock_translator.py` | > 80% |
|
|
331
|
+
| `cost.py` | > 80% |
|
|
332
|
+
| `formatter.py` | > 70% |
|
|
333
|
+
| `learning.py` | > 70% |
|
|
334
|
+
| `audit.py` | > 80% |
|
|
335
|
+
| `config.py` | > 80% |
|
|
336
|
+
| **Global** | > 80% |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
inclusion: fileMatch
|
|
3
|
+
fileMatchPattern: "src/cloudshellgpt/**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Python Module Scaffold
|
|
7
|
+
|
|
8
|
+
Genera un nuevo módulo Python para CloudShellGPT siguiendo la estructura y convenciones del proyecto.
|
|
9
|
+
|
|
10
|
+
## Cuándo usar
|
|
11
|
+
|
|
12
|
+
Cuando necesites crear un nuevo archivo `.py` en `src/cloudshellgpt/` o agregar estructura base a un módulo existente.
|
|
13
|
+
|
|
14
|
+
## Estructura obligatoria del módulo
|
|
15
|
+
|
|
16
|
+
Todo módulo DEBE seguir este orden exacto:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
"""<Descripción en una línea del propósito del módulo.>"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
# Standard library imports
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
# Third-party imports
|
|
28
|
+
import boto3
|
|
29
|
+
from pydantic import BaseModel, Field
|
|
30
|
+
|
|
31
|
+
# Local imports
|
|
32
|
+
from cloudshellgpt.config import Config
|
|
33
|
+
|
|
34
|
+
# Constants (UPPER_SNAKE_CASE)
|
|
35
|
+
DEFAULT_TIMEOUT: int = 30
|
|
36
|
+
MODULE_NAME: str = "module_name"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Pydantic models
|
|
40
|
+
class MyModel(BaseModel):
|
|
41
|
+
"""Descripción del modelo.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
field_name: Descripción del campo.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
field_name: str = Field(..., description="Descripción del campo")
|
|
48
|
+
optional_field: str | None = Field(default=None, description="Campo opcional")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Classes (PascalCase)
|
|
52
|
+
class MyClass:
|
|
53
|
+
"""Descripción de la clase.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config: Configuración del módulo.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, config: Config | None = None) -> None:
|
|
60
|
+
"""Initialize MyClass."""
|
|
61
|
+
self._config = config
|
|
62
|
+
|
|
63
|
+
def public_method(self, arg: str) -> str:
|
|
64
|
+
"""Descripción del método público.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
arg: Descripción del argumento.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Descripción del retorno.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
MyModuleError: Si algo falla.
|
|
74
|
+
"""
|
|
75
|
+
return self._helper(arg)
|
|
76
|
+
|
|
77
|
+
def _helper(self, arg: str) -> str:
|
|
78
|
+
"""Helper privado."""
|
|
79
|
+
return arg
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Public functions (snake_case)
|
|
83
|
+
def public_function(param: str) -> str:
|
|
84
|
+
"""Descripción de la función pública.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
param: Descripción del parámetro.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Descripción del retorno.
|
|
91
|
+
"""
|
|
92
|
+
return _private_helper(param)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Private helpers (_prefixed)
|
|
96
|
+
def _private_helper(param: str) -> str:
|
|
97
|
+
"""Helper privado."""
|
|
98
|
+
return param
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Reglas obligatorias
|
|
102
|
+
|
|
103
|
+
1. **`from __future__ import annotations`** en la primera línea después del docstring
|
|
104
|
+
2. **Type annotations** en TODAS las funciones (parámetros + retorno)
|
|
105
|
+
3. **Docstrings Google style** en todas las clases y métodos públicos (Args, Returns, Raises)
|
|
106
|
+
4. **Pydantic BaseModel** para todo dato que cruza fronteras entre módulos
|
|
107
|
+
5. **`Field(..., description="...")`** para campos no obvios
|
|
108
|
+
6. **Excepciones custom** por módulo (ej: `BedrockError`, `ExecutorError`)
|
|
109
|
+
7. **Nunca swallow exceptions** silently — log o re-raise
|
|
110
|
+
8. **Line length:** máximo 100 caracteres
|
|
111
|
+
9. **Naming:**
|
|
112
|
+
- Classes: PascalCase
|
|
113
|
+
- Functions/methods: snake_case
|
|
114
|
+
- Constants: UPPER_SNAKE_CASE
|
|
115
|
+
- Private: prefijo `_`
|
|
116
|
+
- Modules: snake_case
|
|
117
|
+
10. **Tipos modernos:** `str | None`, `list[str]`, `dict[str, Any]` (no Optional, List, Dict)
|
|
118
|
+
|
|
119
|
+
## Excepciones custom — patrón
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
class ModuleNameError(Exception):
|
|
123
|
+
"""Error base del módulo."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, message: str, details: str | None = None) -> None:
|
|
126
|
+
self.details = details
|
|
127
|
+
super().__init__(message)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Boto3 — patrón
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# Siempre especificar region_name explícitamente
|
|
134
|
+
client = boto3.client("service-name", region_name=self._region)
|
|
135
|
+
|
|
136
|
+
# Nunca usar boto3.resource() — usar boto3.client()
|
|
137
|
+
# Manejar botocore.exceptions.ClientError con mensajes claros
|
|
138
|
+
```
|