atdd 0.1.0__py3-none-any.whl
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.
- atdd/__init__.py +0 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.1.0.dist-info/METADATA +191 -0
- atdd-0.1.0.dist-info/RECORD +183 -0
- atdd-0.1.0.dist-info/WHEEL +5 -0
- atdd-0.1.0.dist-info/entry_points.txt +2 -0
- atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
- atdd-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test use case structure follows best practices.
|
|
3
|
+
|
|
4
|
+
Validates:
|
|
5
|
+
- Use cases have single responsibility
|
|
6
|
+
- Use cases have proper input/output structure
|
|
7
|
+
- Use cases have execute/call method
|
|
8
|
+
- Use cases don't directly access database/API
|
|
9
|
+
- Use cases coordinate through ports/interfaces
|
|
10
|
+
|
|
11
|
+
Inspired by: .claude/utils/coder/usecase.py
|
|
12
|
+
But: Self-contained, no utility dependencies
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
import re
|
|
17
|
+
import ast
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Tuple, Set
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Path constants
|
|
23
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
24
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
25
|
+
DART_DIRS = [REPO_ROOT / "lib", REPO_ROOT / "dart"]
|
|
26
|
+
TS_DIRS = [REPO_ROOT / "supabase" / "functions", REPO_ROOT / "typescript"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_usecase_files() -> List[Tuple[Path, str]]:
|
|
30
|
+
"""
|
|
31
|
+
Find all use case files across languages.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of (file_path, language) tuples
|
|
35
|
+
"""
|
|
36
|
+
usecase_files = []
|
|
37
|
+
|
|
38
|
+
# Python use cases
|
|
39
|
+
if PYTHON_DIR.exists():
|
|
40
|
+
for py_file in PYTHON_DIR.rglob("*_use_case.py"):
|
|
41
|
+
if '__pycache__' not in str(py_file):
|
|
42
|
+
usecase_files.append((py_file, 'python'))
|
|
43
|
+
for py_file in PYTHON_DIR.rglob("*usecase.py"):
|
|
44
|
+
if '__pycache__' not in str(py_file) and not py_file.name.endswith('_use_case.py'):
|
|
45
|
+
usecase_files.append((py_file, 'python'))
|
|
46
|
+
|
|
47
|
+
# Dart use cases
|
|
48
|
+
for dart_dir in DART_DIRS:
|
|
49
|
+
if dart_dir.exists():
|
|
50
|
+
for dart_file in dart_dir.rglob("*_usecases.dart"):
|
|
51
|
+
if '/build/' not in str(dart_file):
|
|
52
|
+
usecase_files.append((dart_file, 'dart'))
|
|
53
|
+
for dart_file in dart_dir.rglob("*_use_case.dart"):
|
|
54
|
+
if '/build/' not in str(dart_file):
|
|
55
|
+
usecase_files.append((dart_file, 'dart'))
|
|
56
|
+
|
|
57
|
+
# TypeScript use cases
|
|
58
|
+
for ts_dir in TS_DIRS:
|
|
59
|
+
if ts_dir.exists():
|
|
60
|
+
for ts_file in ts_dir.rglob("*-use-case.ts"):
|
|
61
|
+
if 'node_modules' not in str(ts_file):
|
|
62
|
+
usecase_files.append((ts_file, 'typescript'))
|
|
63
|
+
for ts_file in ts_dir.rglob("*-usecase.ts"):
|
|
64
|
+
if 'node_modules' not in str(ts_file):
|
|
65
|
+
usecase_files.append((ts_file, 'typescript'))
|
|
66
|
+
|
|
67
|
+
return usecase_files
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_python_classes(file_path: Path) -> List[str]:
|
|
71
|
+
"""
|
|
72
|
+
Extract class names from Python file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
file_path: Path to Python file
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of class names
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
82
|
+
content = f.read()
|
|
83
|
+
except Exception:
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
tree = ast.parse(content)
|
|
88
|
+
except SyntaxError:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
classes = []
|
|
92
|
+
for node in ast.walk(tree):
|
|
93
|
+
if isinstance(node, ast.ClassDef):
|
|
94
|
+
classes.append(node.name)
|
|
95
|
+
|
|
96
|
+
return classes
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_python_usecase_methods(file_path: Path) -> List[Tuple[str, List[str]]]:
|
|
100
|
+
"""
|
|
101
|
+
Check Python use case classes for required methods.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_path: Path to Python use case file
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of (class_name, method_names) tuples
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
111
|
+
content = f.read()
|
|
112
|
+
except Exception:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
tree = ast.parse(content)
|
|
117
|
+
except SyntaxError:
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
results = []
|
|
121
|
+
|
|
122
|
+
for node in ast.walk(tree):
|
|
123
|
+
if isinstance(node, ast.ClassDef):
|
|
124
|
+
methods = []
|
|
125
|
+
for item in node.body:
|
|
126
|
+
if isinstance(item, ast.FunctionDef):
|
|
127
|
+
methods.append(item.name)
|
|
128
|
+
results.append((node.name, methods))
|
|
129
|
+
|
|
130
|
+
return results
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check_for_direct_database_access(file_path: Path, language: str) -> List[str]:
|
|
134
|
+
"""
|
|
135
|
+
Check if use case directly accesses database/API.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
file_path: Path to use case file
|
|
139
|
+
language: File language (python, dart, typescript)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of violation messages
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
146
|
+
content = f.read()
|
|
147
|
+
except Exception:
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
violations = []
|
|
151
|
+
|
|
152
|
+
# Patterns that indicate direct database/API access
|
|
153
|
+
if language == 'python':
|
|
154
|
+
FORBIDDEN_PATTERNS = [
|
|
155
|
+
(r'import\s+psycopg2', 'Direct PostgreSQL import'),
|
|
156
|
+
(r'import\s+pymongo', 'Direct MongoDB import'),
|
|
157
|
+
(r'import\s+redis', 'Direct Redis import'),
|
|
158
|
+
(r'import\s+requests', 'Direct HTTP import (use repository)'),
|
|
159
|
+
(r'import\s+httpx', 'Direct HTTP import (use repository)'),
|
|
160
|
+
(r'from\s+sqlalchemy', 'Direct SQLAlchemy import'),
|
|
161
|
+
(r'from\s+django\.db', 'Direct Django DB import'),
|
|
162
|
+
]
|
|
163
|
+
elif language == 'dart':
|
|
164
|
+
FORBIDDEN_PATTERNS = [
|
|
165
|
+
(r"import\s+['\"]package:sqflite", 'Direct SQLite import'),
|
|
166
|
+
(r"import\s+['\"]package:http/", 'Direct HTTP import (use repository)'),
|
|
167
|
+
(r"import\s+['\"]package:dio/", 'Direct HTTP client import (use repository)'),
|
|
168
|
+
(r"import\s+['\"]package:supabase/", 'Direct Supabase import (use repository)'),
|
|
169
|
+
]
|
|
170
|
+
else: # typescript
|
|
171
|
+
FORBIDDEN_PATTERNS = [
|
|
172
|
+
(r"import.*?['\"]\@supabase/supabase-js['\"]", 'Direct Supabase import (use repository)'),
|
|
173
|
+
(r"import.*?['\"]axios['\"]", 'Direct HTTP import (use repository)'),
|
|
174
|
+
(r"import.*?['\"]node-fetch['\"]", 'Direct HTTP import (use repository)'),
|
|
175
|
+
(r"import.*?['\"]pg['\"]", 'Direct PostgreSQL import'),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
for pattern, message in FORBIDDEN_PATTERNS:
|
|
179
|
+
if re.search(pattern, content):
|
|
180
|
+
violations.append(message)
|
|
181
|
+
|
|
182
|
+
return list(set(violations))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def count_responsibilities(file_path: Path, language: str) -> int:
|
|
186
|
+
"""
|
|
187
|
+
Count number of distinct responsibilities in use case file.
|
|
188
|
+
|
|
189
|
+
Heuristic: Count number of classes/functions that look like use cases.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
file_path: Path to use case file
|
|
193
|
+
language: File language
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Number of responsibilities (1 is ideal)
|
|
197
|
+
"""
|
|
198
|
+
if language == 'python':
|
|
199
|
+
classes = extract_python_classes(file_path)
|
|
200
|
+
# Filter to use case classes (exclude helpers)
|
|
201
|
+
usecase_classes = [c for c in classes if 'UseCase' in c or 'Command' in c or 'Query' in c]
|
|
202
|
+
return len(usecase_classes)
|
|
203
|
+
|
|
204
|
+
# For other languages, we'll be lenient and return 1
|
|
205
|
+
return 1
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@pytest.mark.coder
|
|
209
|
+
def test_usecases_have_single_responsibility():
|
|
210
|
+
"""
|
|
211
|
+
SPEC-CODER-USECASE-0001: Use cases have single responsibility.
|
|
212
|
+
|
|
213
|
+
Each use case file should contain ONE use case class.
|
|
214
|
+
Multiple use cases should be in separate files.
|
|
215
|
+
|
|
216
|
+
Single Responsibility Principle:
|
|
217
|
+
- One use case = one business workflow
|
|
218
|
+
- Clear, focused purpose
|
|
219
|
+
- Easy to test and maintain
|
|
220
|
+
|
|
221
|
+
Given: Use case files (*_use_case.py, *-use-case.ts, etc.)
|
|
222
|
+
When: Checking number of use case classes
|
|
223
|
+
Then: Each file has exactly one use case
|
|
224
|
+
"""
|
|
225
|
+
usecase_files = find_usecase_files()
|
|
226
|
+
|
|
227
|
+
if not usecase_files:
|
|
228
|
+
pytest.skip("No use case files found to validate")
|
|
229
|
+
|
|
230
|
+
violations = []
|
|
231
|
+
|
|
232
|
+
for file_path, language in usecase_files:
|
|
233
|
+
count = count_responsibilities(file_path, language)
|
|
234
|
+
|
|
235
|
+
if count > 1:
|
|
236
|
+
rel_path = file_path.relative_to(REPO_ROOT)
|
|
237
|
+
violations.append(
|
|
238
|
+
f"{rel_path}\n"
|
|
239
|
+
f" Language: {language}\n"
|
|
240
|
+
f" Use Cases: {count}\n"
|
|
241
|
+
f" Issue: File contains {count} use cases, should have 1"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if violations:
|
|
245
|
+
pytest.fail(
|
|
246
|
+
f"\n\nFound {len(violations)} single responsibility violations:\n\n" +
|
|
247
|
+
"\n\n".join(violations[:10]) +
|
|
248
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
249
|
+
f"\n\nEach use case file should contain exactly one use case.\n" +
|
|
250
|
+
f"Split multiple use cases into separate files."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.coder
|
|
255
|
+
def test_usecases_have_execute_method():
|
|
256
|
+
"""
|
|
257
|
+
SPEC-CODER-USECASE-0002: Use cases have execute/call method.
|
|
258
|
+
|
|
259
|
+
Use cases should have a clear entry point:
|
|
260
|
+
- Python: execute(), __call__(), or run()
|
|
261
|
+
- Dart: call(), execute()
|
|
262
|
+
- TypeScript: execute(), run()
|
|
263
|
+
|
|
264
|
+
This makes use cases invokable and testable.
|
|
265
|
+
|
|
266
|
+
Given: Python use case files
|
|
267
|
+
When: Checking for execute methods
|
|
268
|
+
Then: Each use case has an entry point method
|
|
269
|
+
"""
|
|
270
|
+
usecase_files = find_usecase_files()
|
|
271
|
+
|
|
272
|
+
# Only check Python for now (easier to parse)
|
|
273
|
+
python_usecases = [(f, l) for f, l in usecase_files if l == 'python']
|
|
274
|
+
|
|
275
|
+
if not python_usecases:
|
|
276
|
+
pytest.skip("No Python use case files found to validate")
|
|
277
|
+
|
|
278
|
+
violations = []
|
|
279
|
+
|
|
280
|
+
VALID_METHODS = {'execute', '__call__', 'run', 'handle'}
|
|
281
|
+
|
|
282
|
+
for file_path, _ in python_usecases:
|
|
283
|
+
class_methods = check_python_usecase_methods(file_path)
|
|
284
|
+
|
|
285
|
+
for class_name, methods in class_methods:
|
|
286
|
+
# Skip non-usecase classes
|
|
287
|
+
if 'UseCase' not in class_name and 'Command' not in class_name and 'Query' not in class_name:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
# Skip private/helper classes
|
|
291
|
+
if class_name.startswith('_'):
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
# Check if has valid entry point
|
|
295
|
+
has_entry_point = any(method in VALID_METHODS for method in methods)
|
|
296
|
+
|
|
297
|
+
if not has_entry_point and len(methods) > 0:
|
|
298
|
+
rel_path = file_path.relative_to(REPO_ROOT)
|
|
299
|
+
violations.append(
|
|
300
|
+
f"{rel_path}\n"
|
|
301
|
+
f" Class: {class_name}\n"
|
|
302
|
+
f" Methods: {', '.join(methods)}\n"
|
|
303
|
+
f" Issue: Use case missing entry point (execute, __call__, run, handle)"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if violations:
|
|
307
|
+
pytest.fail(
|
|
308
|
+
f"\n\nFound {len(violations)} missing entry point violations:\n\n" +
|
|
309
|
+
"\n\n".join(violations[:10]) +
|
|
310
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
311
|
+
f"\n\nUse cases should have a clear entry point method:\n" +
|
|
312
|
+
f" - execute() - explicit, clear\n" +
|
|
313
|
+
f" - __call__() - makes use case invokable\n" +
|
|
314
|
+
f" - run() or handle() - also acceptable"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@pytest.mark.coder
|
|
319
|
+
def test_usecases_dont_directly_access_database():
|
|
320
|
+
"""
|
|
321
|
+
SPEC-CODER-USECASE-0003: Use cases don't directly access database/API.
|
|
322
|
+
|
|
323
|
+
Use cases should coordinate through ports/interfaces:
|
|
324
|
+
- Don't import database libraries directly
|
|
325
|
+
- Don't import HTTP clients directly
|
|
326
|
+
- Use repository interfaces instead
|
|
327
|
+
- Use service interfaces for external APIs
|
|
328
|
+
|
|
329
|
+
Clean Architecture principle:
|
|
330
|
+
- Use cases are in Application layer
|
|
331
|
+
- Database/API access is in Integration layer
|
|
332
|
+
- Use cases depend on abstractions (ports)
|
|
333
|
+
|
|
334
|
+
Given: Use case files
|
|
335
|
+
When: Checking imports
|
|
336
|
+
Then: No direct database/API library imports
|
|
337
|
+
"""
|
|
338
|
+
usecase_files = find_usecase_files()
|
|
339
|
+
|
|
340
|
+
if not usecase_files:
|
|
341
|
+
pytest.skip("No use case files found to validate")
|
|
342
|
+
|
|
343
|
+
violations = []
|
|
344
|
+
|
|
345
|
+
for file_path, language in usecase_files:
|
|
346
|
+
direct_access = check_for_direct_database_access(file_path, language)
|
|
347
|
+
|
|
348
|
+
if direct_access:
|
|
349
|
+
rel_path = file_path.relative_to(REPO_ROOT)
|
|
350
|
+
violations.append(
|
|
351
|
+
f"{rel_path}\n"
|
|
352
|
+
f" Language: {language}\n"
|
|
353
|
+
f" Violations:\n" +
|
|
354
|
+
"\n".join(f" - {v}" for v in direct_access)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if violations:
|
|
358
|
+
pytest.fail(
|
|
359
|
+
f"\n\nFound {len(violations)} direct database/API access violations:\n\n" +
|
|
360
|
+
"\n\n".join(violations[:10]) +
|
|
361
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
362
|
+
f"\n\nUse cases should not directly import database/API libraries.\n" +
|
|
363
|
+
f"Use repository interfaces instead (Dependency Inversion Principle)."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@pytest.mark.coder
|
|
368
|
+
def test_usecases_are_in_application_layer():
|
|
369
|
+
"""
|
|
370
|
+
SPEC-CODER-USECASE-0004: Use cases are in application layer.
|
|
371
|
+
|
|
372
|
+
Use cases should live in the application layer:
|
|
373
|
+
- .../application/use_cases/
|
|
374
|
+
- .../application/usecases/
|
|
375
|
+
- .../usecases/ (if no explicit layers)
|
|
376
|
+
|
|
377
|
+
Not in:
|
|
378
|
+
- domain/ (pure business logic)
|
|
379
|
+
- presentation/ (UI/API)
|
|
380
|
+
- integration/ (database/external services)
|
|
381
|
+
|
|
382
|
+
Given: Use case files
|
|
383
|
+
When: Checking file paths
|
|
384
|
+
Then: All use cases in application layer
|
|
385
|
+
"""
|
|
386
|
+
usecase_files = find_usecase_files()
|
|
387
|
+
|
|
388
|
+
if not usecase_files:
|
|
389
|
+
pytest.skip("No use case files found to validate")
|
|
390
|
+
|
|
391
|
+
violations = []
|
|
392
|
+
|
|
393
|
+
for file_path, language in usecase_files:
|
|
394
|
+
path_str = str(file_path).lower()
|
|
395
|
+
|
|
396
|
+
# Check if in application layer or usecases directory
|
|
397
|
+
in_application = '/application/' in path_str or '/usecases/' in path_str or '/use_cases/' in path_str
|
|
398
|
+
|
|
399
|
+
# Check if in wrong layer
|
|
400
|
+
in_domain = '/domain/' in path_str and '/application/' not in path_str
|
|
401
|
+
in_presentation = '/presentation/' in path_str
|
|
402
|
+
in_integration = '/integration/' in path_str or '/infrastructure/' in path_str or '/data/' in path_str
|
|
403
|
+
|
|
404
|
+
if (in_domain or in_presentation or in_integration) and not in_application:
|
|
405
|
+
rel_path = file_path.relative_to(REPO_ROOT)
|
|
406
|
+
violations.append(
|
|
407
|
+
f"{rel_path}\n"
|
|
408
|
+
f" Issue: Use case in wrong layer (should be in application/)"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if violations:
|
|
412
|
+
pytest.fail(
|
|
413
|
+
f"\n\nFound {len(violations)} layer placement violations:\n\n" +
|
|
414
|
+
"\n\n".join(violations[:10]) +
|
|
415
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
416
|
+
f"\n\nUse cases should be in application layer.\n" +
|
|
417
|
+
f"Expected paths:\n" +
|
|
418
|
+
f" - .../application/use_cases/\n" +
|
|
419
|
+
f" - .../application/usecases/\n" +
|
|
420
|
+
f" - .../usecases/ (if no explicit layers)"
|
|
421
|
+
)
|