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,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Python test files follow naming conventions.
|
|
3
|
+
|
|
4
|
+
Validates:
|
|
5
|
+
- Test files are named test_*.py
|
|
6
|
+
- Test files are in test/ or tests/ directories (accept both singular and plural)
|
|
7
|
+
- Test files have mandatory slugs (test_{wmbt}_{harness}_{nnn}_{slug}.py)
|
|
8
|
+
- Test functions start with test_
|
|
9
|
+
- Test classes start with Test
|
|
10
|
+
|
|
11
|
+
Inspired by: .claude/utils/tester/filename.py
|
|
12
|
+
But: Self-contained, no utility dependencies
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Path constants
|
|
21
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
22
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_test_files() -> list:
|
|
26
|
+
"""
|
|
27
|
+
Find all test files in python/ directory.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of Path objects pointing to test files
|
|
31
|
+
"""
|
|
32
|
+
if not PYTHON_DIR.exists():
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
test_files = []
|
|
36
|
+
|
|
37
|
+
# Find test files
|
|
38
|
+
for py_file in PYTHON_DIR.rglob("*.py"):
|
|
39
|
+
# Check if in test directory or named test_*
|
|
40
|
+
if '/test/' in str(py_file) or py_file.name.startswith('test_'):
|
|
41
|
+
test_files.append(py_file)
|
|
42
|
+
|
|
43
|
+
return test_files
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_test_functions(file_path: Path) -> list:
|
|
47
|
+
"""
|
|
48
|
+
Extract test function names from Python file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
file_path: Path to test file
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of function names
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
58
|
+
content = f.read()
|
|
59
|
+
except Exception:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
# Match: def test_something(...):
|
|
63
|
+
# or: async def test_something(...):
|
|
64
|
+
functions = re.findall(r'(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(', content)
|
|
65
|
+
|
|
66
|
+
return functions
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def extract_test_classes(file_path: Path) -> list:
|
|
70
|
+
"""
|
|
71
|
+
Extract test class names from Python file.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
file_path: Path to test file
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of class names
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
81
|
+
content = f.read()
|
|
82
|
+
except Exception:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
# Match: class TestSomething(...):
|
|
86
|
+
classes = re.findall(r'class\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[:\(]', content)
|
|
87
|
+
|
|
88
|
+
return classes
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.tester
|
|
92
|
+
def test_python_test_files_named_correctly():
|
|
93
|
+
"""
|
|
94
|
+
SPEC-TESTER-NAMING-0001: Python test files follow naming convention.
|
|
95
|
+
|
|
96
|
+
Convention:
|
|
97
|
+
- Test files must be named test_*.py
|
|
98
|
+
- Test files should be in test/ or tests/ directories (both accepted)
|
|
99
|
+
|
|
100
|
+
Given: Python test files
|
|
101
|
+
When: Checking file names
|
|
102
|
+
Then: All test files follow convention
|
|
103
|
+
"""
|
|
104
|
+
test_files = find_test_files()
|
|
105
|
+
|
|
106
|
+
if not test_files:
|
|
107
|
+
pytest.skip("No Python test files found")
|
|
108
|
+
|
|
109
|
+
violations = []
|
|
110
|
+
|
|
111
|
+
for test_file in test_files:
|
|
112
|
+
filename = test_file.name
|
|
113
|
+
|
|
114
|
+
# Skip special pytest files
|
|
115
|
+
if filename in ['conftest.py', '__init__.py']:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Check naming convention
|
|
119
|
+
if not filename.startswith('test_') and not filename.endswith('_test.py'):
|
|
120
|
+
violations.append(
|
|
121
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
122
|
+
f" Issue: Test file should be named test_*.py\\n"
|
|
123
|
+
f" Found: {filename}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Check if in test/ or tests/ directory (accept both singular and plural)
|
|
127
|
+
if '/test/' not in str(test_file) and '/tests/' not in str(test_file):
|
|
128
|
+
violations.append(
|
|
129
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
130
|
+
f" Issue: Test file should be in test/ or tests/ directory\\n"
|
|
131
|
+
f" Found: Not in test/ or tests/ directory"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if violations:
|
|
135
|
+
pytest.fail(
|
|
136
|
+
f"\\n\\nFound {len(violations)} naming violations:\\n\\n" +
|
|
137
|
+
"\\n\\n".join(violations[:10]) +
|
|
138
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.tester
|
|
143
|
+
def test_python_test_functions_named_correctly():
|
|
144
|
+
"""
|
|
145
|
+
SPEC-TESTER-NAMING-0002: Python test functions follow naming convention.
|
|
146
|
+
|
|
147
|
+
Convention:
|
|
148
|
+
- Test functions must start with test_
|
|
149
|
+
- Test functions should use snake_case
|
|
150
|
+
- Test functions should be descriptive
|
|
151
|
+
|
|
152
|
+
Given: Python test files
|
|
153
|
+
When: Checking function names
|
|
154
|
+
Then: All test functions follow convention
|
|
155
|
+
"""
|
|
156
|
+
test_files = find_test_files()
|
|
157
|
+
|
|
158
|
+
if not test_files:
|
|
159
|
+
pytest.skip("No Python test files found")
|
|
160
|
+
|
|
161
|
+
violations = []
|
|
162
|
+
|
|
163
|
+
for test_file in test_files:
|
|
164
|
+
functions = extract_test_functions(test_file)
|
|
165
|
+
|
|
166
|
+
for func_name in functions:
|
|
167
|
+
# Skip helper functions (private)
|
|
168
|
+
if func_name.startswith('_'):
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Skip fixture functions
|
|
172
|
+
if func_name in ['setup', 'teardown', 'setup_class', 'teardown_class',
|
|
173
|
+
'setup_method', 'teardown_method']:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Check if test function follows convention
|
|
177
|
+
if not func_name.startswith('test_'):
|
|
178
|
+
# Not a test function (could be helper)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Check snake_case (allow uppercase for WMBT codes like E003, UNIT, etc.)
|
|
182
|
+
# Pattern accepts: test_e003_unit_001_... OR test_E003_UNIT_001_...
|
|
183
|
+
if not re.match(r'^test_[a-zA-Z0-9_]+$', func_name):
|
|
184
|
+
violations.append(
|
|
185
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
186
|
+
f" Function: {func_name}\\n"
|
|
187
|
+
f" Issue: Test function should start with test_ and use alphanumeric_underscore pattern"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Check if too short
|
|
191
|
+
if len(func_name) < 10:
|
|
192
|
+
violations.append(
|
|
193
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
194
|
+
f" Function: {func_name}\\n"
|
|
195
|
+
f" Issue: Test function name too short (should be descriptive)"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if violations:
|
|
199
|
+
pytest.fail(
|
|
200
|
+
f"\\n\\nFound {len(violations)} function naming violations:\\n\\n" +
|
|
201
|
+
"\\n\\n".join(violations[:10]) +
|
|
202
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.tester
|
|
207
|
+
def test_python_test_classes_named_correctly():
|
|
208
|
+
"""
|
|
209
|
+
SPEC-TESTER-NAMING-0003: Python test classes follow naming convention.
|
|
210
|
+
|
|
211
|
+
Convention:
|
|
212
|
+
- Test classes must start with Test
|
|
213
|
+
- Test classes should use PascalCase
|
|
214
|
+
- Test classes should be descriptive
|
|
215
|
+
|
|
216
|
+
Given: Python test files
|
|
217
|
+
When: Checking class names
|
|
218
|
+
Then: All test classes follow convention
|
|
219
|
+
"""
|
|
220
|
+
test_files = find_test_files()
|
|
221
|
+
|
|
222
|
+
if not test_files:
|
|
223
|
+
pytest.skip("No Python test files found")
|
|
224
|
+
|
|
225
|
+
violations = []
|
|
226
|
+
|
|
227
|
+
for test_file in test_files:
|
|
228
|
+
classes = extract_test_classes(test_file)
|
|
229
|
+
|
|
230
|
+
for class_name in classes:
|
|
231
|
+
# Skip if not a test class (could be helper)
|
|
232
|
+
if not class_name.startswith('Test'):
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Check PascalCase
|
|
236
|
+
if not re.match(r'^Test[A-Z][a-zA-Z0-9]*$', class_name):
|
|
237
|
+
violations.append(
|
|
238
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
239
|
+
f" Class: {class_name}\\n"
|
|
240
|
+
f" Issue: Test class should use PascalCase after 'Test'"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Check if too short
|
|
244
|
+
if len(class_name) < 8: # Test + at least 3 chars
|
|
245
|
+
violations.append(
|
|
246
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
247
|
+
f" Class: {class_name}\\n"
|
|
248
|
+
f" Issue: Test class name too short (should be descriptive)"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if violations:
|
|
252
|
+
pytest.fail(
|
|
253
|
+
f"\\n\\nFound {len(violations)} class naming violations:\\n\\n" +
|
|
254
|
+
"\\n\\n".join(violations[:10]) +
|
|
255
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@pytest.mark.tester
|
|
260
|
+
def test_python_test_files_have_mandatory_slugs():
|
|
261
|
+
"""
|
|
262
|
+
SPEC-TESTER-NAMING-0004: Python test files in feature-based structure have mandatory slugs.
|
|
263
|
+
|
|
264
|
+
Convention (from filename.convention.yaml):
|
|
265
|
+
- Pattern: test_{wmbt_lower}_{harness_lower}_{nnn}_{slug_snake}.py
|
|
266
|
+
- Slug is MANDATORY (not optional)
|
|
267
|
+
- Slug derived from acceptance.identity.purpose
|
|
268
|
+
- Example: test_l001_unit_001_uuid_v7_generation_completes_within.py
|
|
269
|
+
|
|
270
|
+
Given: Python test files in python/{wagon}/{feature}/test/ directories
|
|
271
|
+
When: Checking file names
|
|
272
|
+
Then: All test files include mandatory slug component
|
|
273
|
+
"""
|
|
274
|
+
test_files = find_test_files()
|
|
275
|
+
|
|
276
|
+
if not test_files:
|
|
277
|
+
pytest.skip("No Python test files found")
|
|
278
|
+
|
|
279
|
+
violations = []
|
|
280
|
+
|
|
281
|
+
# Pattern: test_{wmbt}_{harness}_{nnn}_{slug}.py
|
|
282
|
+
# WMBT: 1-4 letters (L, P, C, etc.) followed by 3 digits
|
|
283
|
+
# Harness: unit, integration, load, http, e2e, etc.
|
|
284
|
+
# NNN: 3 digits (001, 002, etc.)
|
|
285
|
+
# Slug: descriptive snake_case (MANDATORY)
|
|
286
|
+
slug_pattern = re.compile(
|
|
287
|
+
r'^test_([a-z]\d{3})_([a-z]+)_(\d{3})_([a-z][a-z0-9_]+)\.py$'
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
for test_file in test_files:
|
|
291
|
+
filename = test_file.name
|
|
292
|
+
|
|
293
|
+
# Skip special pytest files and wagon-level tests
|
|
294
|
+
if filename in ['conftest.py', '__init__.py', 'test_contracts.py', 'test_telemetry.py']:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Only check files in feature-based structure: python/{wagon}/{feature}/test/
|
|
298
|
+
test_path_str = str(test_file)
|
|
299
|
+
if '/python/' not in test_path_str or '/test/' not in test_path_str:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Check if in feature-based directory structure
|
|
303
|
+
parts = test_file.parts
|
|
304
|
+
try:
|
|
305
|
+
python_idx = parts.index('python')
|
|
306
|
+
# Feature-based: python/{wagon}/{feature}/test/
|
|
307
|
+
if len(parts) > python_idx + 3 and parts[python_idx + 3] == 'test':
|
|
308
|
+
# This is a feature-based test file
|
|
309
|
+
match = slug_pattern.match(filename)
|
|
310
|
+
|
|
311
|
+
if not match:
|
|
312
|
+
# Try to parse what we have
|
|
313
|
+
basic_pattern = re.compile(r'^test_([a-z]\d{3})_([a-z]+)_(\d{3})\.py$')
|
|
314
|
+
basic_match = basic_pattern.match(filename)
|
|
315
|
+
|
|
316
|
+
if basic_match:
|
|
317
|
+
wmbt = basic_match.group(1)
|
|
318
|
+
harness = basic_match.group(2)
|
|
319
|
+
nnn = basic_match.group(3)
|
|
320
|
+
violations.append(
|
|
321
|
+
f"{test_file.relative_to(REPO_ROOT)}\n"
|
|
322
|
+
f" Issue: Missing mandatory slug component\n"
|
|
323
|
+
f" Pattern: test_{{wmbt}}_{{harness}}_{{nnn}}_{{slug}}.py\n"
|
|
324
|
+
f" Found: test_{wmbt}_{harness}_{nnn}.py (no slug)\n"
|
|
325
|
+
f" Expected: test_{wmbt}_{harness}_{nnn}_<descriptive_slug>.py\n"
|
|
326
|
+
f" Note: Slug must be derived from acceptance.identity.purpose"
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
violations.append(
|
|
330
|
+
f"{test_file.relative_to(REPO_ROOT)}\n"
|
|
331
|
+
f" Issue: Does not match required pattern\n"
|
|
332
|
+
f" Pattern: test_{{wmbt}}_{{harness}}_{{nnn}}_{{slug}}.py\n"
|
|
333
|
+
f" Found: {filename}\n"
|
|
334
|
+
f" Note: All 4 components required (wmbt, harness, nnn, slug)"
|
|
335
|
+
)
|
|
336
|
+
except (ValueError, IndexError):
|
|
337
|
+
# Not in feature-based structure, skip
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
if violations:
|
|
341
|
+
pytest.fail(
|
|
342
|
+
f"\n\nFound {len(violations)} test files without mandatory slugs:\n\n" +
|
|
343
|
+
"\n\n".join(violations[:10]) +
|
|
344
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
345
|
+
"\n\nSlug Convention:\n" +
|
|
346
|
+
"- Slugs are MANDATORY (not optional)\n" +
|
|
347
|
+
"- Derived from acceptance.identity.purpose field\n" +
|
|
348
|
+
"- Process: Remove 'Verify', lowercase, replace spaces with underscores\n" +
|
|
349
|
+
"- Example: 'Verify UUID v7 generation completes within' → 'uuid_v7_generation_completes_within'"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.mark.tester
|
|
354
|
+
def test_python_test_files_are_in_correct_locations():
|
|
355
|
+
"""
|
|
356
|
+
SPEC-TESTER-NAMING-0004: Test files are in correct locations.
|
|
357
|
+
|
|
358
|
+
Convention:
|
|
359
|
+
- Test files should be in {module}/test/ directory
|
|
360
|
+
- Test files should mirror source structure when possible
|
|
361
|
+
|
|
362
|
+
Given: Python test files
|
|
363
|
+
When: Checking locations
|
|
364
|
+
Then: Test files are in appropriate test/ directories
|
|
365
|
+
"""
|
|
366
|
+
test_files = find_test_files()
|
|
367
|
+
|
|
368
|
+
if not test_files:
|
|
369
|
+
pytest.skip("No Python test files found")
|
|
370
|
+
|
|
371
|
+
violations = []
|
|
372
|
+
|
|
373
|
+
for test_file in test_files:
|
|
374
|
+
# Check if in test/ or tests/ directory (accept both)
|
|
375
|
+
if '/test/' not in str(test_file) and '/tests/' not in str(test_file):
|
|
376
|
+
violations.append(
|
|
377
|
+
f"{test_file.relative_to(REPO_ROOT)}\\n"
|
|
378
|
+
f" Issue: Test file not in test/ or tests/ directory"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Check if test file has corresponding source structure
|
|
382
|
+
# Example: module/test/test_foo.py should have module/src/foo.py
|
|
383
|
+
# Accept both test/ and tests/ directories
|
|
384
|
+
if ('/test/' in str(test_file) or '/tests/' in str(test_file)) and test_file.name.startswith('test_'):
|
|
385
|
+
# Get potential source file name
|
|
386
|
+
source_name = test_file.name.replace('test_', '', 1)
|
|
387
|
+
test_dir = test_file.parent
|
|
388
|
+
|
|
389
|
+
# Check for src/ sibling
|
|
390
|
+
module_root = test_dir.parent
|
|
391
|
+
src_dir = module_root / 'src'
|
|
392
|
+
|
|
393
|
+
if src_dir.exists():
|
|
394
|
+
# Look for corresponding source file
|
|
395
|
+
potential_source = src_dir / source_name
|
|
396
|
+
# Also check subdirectories (domain, application, etc.)
|
|
397
|
+
source_exists = potential_source.exists() or \
|
|
398
|
+
any((src_dir / subdir / source_name).exists()
|
|
399
|
+
for subdir in ['domain', 'application', 'integration', 'presentation'])
|
|
400
|
+
|
|
401
|
+
if not source_exists and source_name != '__init__.py':
|
|
402
|
+
# This might be okay (integration tests, etc.) so just warn
|
|
403
|
+
pass # Don't fail, just note
|
|
404
|
+
|
|
405
|
+
if violations:
|
|
406
|
+
pytest.fail(
|
|
407
|
+
f"\\n\\nFound {len(violations)} location violations:\\n\\n" +
|
|
408
|
+
"\\n\\n".join(violations[:10]) +
|
|
409
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
410
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-TESTER-CONV-0082: RED convention rejects tests not in layer structure
|
|
3
|
+
|
|
4
|
+
Test that red.convention.yaml validates tests are in correct layer directories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
RED_CONVENTION = REPO_ROOT / "atdd" / "tester" / "conventions" / "red.convention.yaml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.tester
|
|
17
|
+
def test_rejects_non_layered_python_tests():
|
|
18
|
+
"""
|
|
19
|
+
SPEC-TESTER-CONV-0082: RED convention rejects Python tests not in layer structure.
|
|
20
|
+
|
|
21
|
+
Given: Test file generated outside layer directory
|
|
22
|
+
When: Validating test structure
|
|
23
|
+
Then: Convention validation fails with error message
|
|
24
|
+
"""
|
|
25
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
26
|
+
|
|
27
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
28
|
+
convention = yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
31
|
+
assert structure is not None, "Convention must define test structure"
|
|
32
|
+
|
|
33
|
+
python_config = structure.get('python', {})
|
|
34
|
+
|
|
35
|
+
# Check for validation rules
|
|
36
|
+
validation = python_config.get('validation') or convention.get('validation', {})
|
|
37
|
+
|
|
38
|
+
# Should reject tests without layer directory
|
|
39
|
+
assert 'require_layer_directory' in validation or \
|
|
40
|
+
validation.get('enforce_layer_structure') == True, \
|
|
41
|
+
"Convention must enforce layer directory requirement"
|
|
42
|
+
|
|
43
|
+
# Check for error message configuration
|
|
44
|
+
assert 'error_messages' in validation or 'messages' in validation, \
|
|
45
|
+
"Convention must define error messages for violations"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.tester
|
|
49
|
+
def test_rejects_non_layered_flutter_tests():
|
|
50
|
+
"""
|
|
51
|
+
SPEC-TESTER-CONV-0082: RED convention rejects Flutter tests not in layer structure.
|
|
52
|
+
|
|
53
|
+
Given: Flutter test file outside layer directory
|
|
54
|
+
When: Validating test structure
|
|
55
|
+
Then: Convention validation fails
|
|
56
|
+
"""
|
|
57
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
58
|
+
|
|
59
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
60
|
+
convention = yaml.safe_load(f)
|
|
61
|
+
|
|
62
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
63
|
+
flutter_config = structure.get('flutter') or structure.get('dart', {})
|
|
64
|
+
|
|
65
|
+
validation = flutter_config.get('validation') or convention.get('validation', {})
|
|
66
|
+
|
|
67
|
+
# Should reject tests without layer directory
|
|
68
|
+
assert 'require_layer_directory' in validation or \
|
|
69
|
+
validation.get('enforce_layer_structure') == True, \
|
|
70
|
+
"Convention must enforce layer directory requirement for Flutter"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.tester
|
|
74
|
+
def test_rejects_non_layered_supabase_tests():
|
|
75
|
+
"""
|
|
76
|
+
SPEC-TESTER-CONV-0082: RED convention rejects Supabase tests not in layer structure.
|
|
77
|
+
|
|
78
|
+
Given: Supabase test file outside layer directory
|
|
79
|
+
When: Validating test structure
|
|
80
|
+
Then: Convention validation fails
|
|
81
|
+
"""
|
|
82
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
83
|
+
|
|
84
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
85
|
+
convention = yaml.safe_load(f)
|
|
86
|
+
|
|
87
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
88
|
+
supabase_config = structure.get('supabase') or structure.get('typescript', {})
|
|
89
|
+
|
|
90
|
+
validation = supabase_config.get('validation') or convention.get('validation', {})
|
|
91
|
+
|
|
92
|
+
# Should reject tests without layer directory
|
|
93
|
+
assert 'require_layer_directory' in validation or \
|
|
94
|
+
validation.get('enforce_layer_structure') == True, \
|
|
95
|
+
"Convention must enforce layer directory requirement for Supabase"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-TESTER-CONV-0079: RED convention defines 4-layer test structure for Python
|
|
3
|
+
|
|
4
|
+
Test that red.convention.yaml enforces 4-layer test structure for Python.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
RED_CONVENTION = REPO_ROOT / "atdd" / "tester" / "conventions" / "red.convention.yaml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.tester
|
|
17
|
+
def test_red_defines_python_layer_structure():
|
|
18
|
+
"""
|
|
19
|
+
SPEC-TESTER-CONV-0079: RED convention defines 4-layer test structure for Python.
|
|
20
|
+
|
|
21
|
+
Given: red.convention.yaml exists
|
|
22
|
+
When: Reading Python test structure configuration
|
|
23
|
+
Then: Convention specifies test structure python/{wagon}/{feature}/tests/{layer}/
|
|
24
|
+
"""
|
|
25
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
26
|
+
|
|
27
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
28
|
+
convention = yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
# Check for layer structure definition
|
|
31
|
+
assert 'layer_structure' in convention or 'test_structure' in convention, \
|
|
32
|
+
"Convention must define layer_structure or test_structure"
|
|
33
|
+
|
|
34
|
+
# Get structure config
|
|
35
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
36
|
+
|
|
37
|
+
# Check Python-specific configuration
|
|
38
|
+
assert 'python' in structure, "Convention must define Python test structure"
|
|
39
|
+
|
|
40
|
+
python_config = structure['python']
|
|
41
|
+
|
|
42
|
+
# Verify 4 layers are defined
|
|
43
|
+
assert 'layers' in python_config, "Python config must define layers"
|
|
44
|
+
layers = python_config['layers']
|
|
45
|
+
|
|
46
|
+
expected_layers = {'presentation', 'application', 'domain', 'integration'}
|
|
47
|
+
assert set(layers) == expected_layers, \
|
|
48
|
+
f"Layers must be {expected_layers}, got {set(layers)}"
|
|
49
|
+
|
|
50
|
+
# Verify test path pattern
|
|
51
|
+
assert 'test_path_pattern' in python_config, "Python config must define test_path_pattern"
|
|
52
|
+
pattern = python_config['test_path_pattern']
|
|
53
|
+
|
|
54
|
+
assert '{wagon}' in pattern, "Pattern must include {wagon} placeholder"
|
|
55
|
+
assert '{feature}' in pattern, "Pattern must include {feature} placeholder"
|
|
56
|
+
assert '{layer}' in pattern, "Pattern must include {layer} placeholder"
|
|
57
|
+
assert 'tests' in pattern, "Pattern must include 'tests' directory"
|
|
58
|
+
|
|
59
|
+
# Verify it matches expected pattern
|
|
60
|
+
expected_pattern = "python/{wagon}/{feature}/tests/{layer}/"
|
|
61
|
+
assert pattern == expected_pattern or pattern.startswith("python/{wagon}/{feature}/tests/{layer}"), \
|
|
62
|
+
f"Pattern should be {expected_pattern}, got {pattern}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.tester
|
|
66
|
+
def test_red_creates_layer_directories():
|
|
67
|
+
"""
|
|
68
|
+
SPEC-TESTER-CONV-0079: Test path generation creates layer directories automatically.
|
|
69
|
+
|
|
70
|
+
Given: red.convention.yaml with layer structure
|
|
71
|
+
When: Generating test paths
|
|
72
|
+
Then: Layer directories are created automatically
|
|
73
|
+
"""
|
|
74
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
75
|
+
|
|
76
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
77
|
+
convention = yaml.safe_load(f)
|
|
78
|
+
|
|
79
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
80
|
+
assert structure is not None, "Convention must define test structure"
|
|
81
|
+
|
|
82
|
+
python_config = structure.get('python', {})
|
|
83
|
+
|
|
84
|
+
# Check for auto-create configuration
|
|
85
|
+
assert 'auto_create_directories' in python_config or \
|
|
86
|
+
python_config.get('behavior', {}).get('create_layer_dirs') == True, \
|
|
87
|
+
"Convention must specify that layer directories are auto-created"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-TESTER-CONV-0081: RED convention defines 4-layer test structure for Supabase
|
|
3
|
+
|
|
4
|
+
Test that red.convention.yaml enforces 4-layer test structure for Supabase.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
RED_CONVENTION = REPO_ROOT / "atdd" / "tester" / "conventions" / "red.convention.yaml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.tester
|
|
17
|
+
def test_red_defines_supabase_layer_structure():
|
|
18
|
+
"""
|
|
19
|
+
SPEC-TESTER-CONV-0081: RED convention defines 4-layer test structure for Supabase.
|
|
20
|
+
|
|
21
|
+
Given: Supabase structure is supabase/functions/{wagon}/{feature}/
|
|
22
|
+
When: Reading Supabase test structure configuration
|
|
23
|
+
Then: Convention specifies test structure supabase/functions/{wagon}/{feature}/tests/{layer}/
|
|
24
|
+
"""
|
|
25
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
26
|
+
|
|
27
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
28
|
+
convention = yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
31
|
+
assert structure is not None, "Convention must define test structure"
|
|
32
|
+
|
|
33
|
+
# Check Supabase/TypeScript-specific configuration
|
|
34
|
+
assert 'supabase' in structure or 'typescript' in structure, \
|
|
35
|
+
"Convention must define Supabase/TypeScript test structure"
|
|
36
|
+
|
|
37
|
+
supabase_config = structure.get('supabase') or structure.get('typescript')
|
|
38
|
+
|
|
39
|
+
# Verify 4 layers are defined
|
|
40
|
+
assert 'layers' in supabase_config, "Supabase config must define layers"
|
|
41
|
+
layers = supabase_config['layers']
|
|
42
|
+
|
|
43
|
+
expected_layers = {'presentation', 'application', 'domain', 'integration'}
|
|
44
|
+
assert set(layers) == expected_layers, \
|
|
45
|
+
f"Layers must be {expected_layers}, got {set(layers)}"
|
|
46
|
+
|
|
47
|
+
# Verify test path pattern
|
|
48
|
+
assert 'test_path_pattern' in supabase_config, "Supabase config must define test_path_pattern"
|
|
49
|
+
pattern = supabase_config['test_path_pattern']
|
|
50
|
+
|
|
51
|
+
assert '{wagon}' in pattern, "Pattern must include {wagon} placeholder"
|
|
52
|
+
assert '{feature}' in pattern, "Pattern must include {feature} placeholder"
|
|
53
|
+
assert '{layer}' in pattern, "Pattern must include {layer} placeholder"
|
|
54
|
+
assert 'tests' in pattern, "Pattern must include 'tests' directory"
|
|
55
|
+
assert 'supabase/functions' in pattern or 'functions' in pattern, \
|
|
56
|
+
"Pattern must reference Supabase functions directory"
|
|
57
|
+
|
|
58
|
+
# Verify it matches expected pattern
|
|
59
|
+
expected_pattern = "supabase/functions/{wagon}/{feature}/tests/{layer}/"
|
|
60
|
+
assert pattern == expected_pattern or \
|
|
61
|
+
pattern.startswith("supabase/functions/{wagon}/{feature}/tests/{layer}"), \
|
|
62
|
+
f"Pattern should be {expected_pattern}, got {pattern}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.tester
|
|
66
|
+
def test_http_tests_in_presentation():
|
|
67
|
+
"""
|
|
68
|
+
SPEC-TESTER-CONV-0081: HTTP handler tests go in presentation layer.
|
|
69
|
+
|
|
70
|
+
Given: Supabase layer structure
|
|
71
|
+
When: Determining layer for HTTP handler tests
|
|
72
|
+
Then: HTTP tests are placed in presentation layer
|
|
73
|
+
"""
|
|
74
|
+
assert RED_CONVENTION.exists(), "red.convention.yaml must exist"
|
|
75
|
+
|
|
76
|
+
with open(RED_CONVENTION, 'r') as f:
|
|
77
|
+
convention = yaml.safe_load(f)
|
|
78
|
+
|
|
79
|
+
structure = convention.get('layer_structure') or convention.get('test_structure')
|
|
80
|
+
supabase_config = structure.get('supabase') or structure.get('typescript', {})
|
|
81
|
+
|
|
82
|
+
# Check for layer mapping rules
|
|
83
|
+
layer_mapping = supabase_config.get('layer_mapping') or supabase_config.get('test_type_layers')
|
|
84
|
+
|
|
85
|
+
if layer_mapping:
|
|
86
|
+
# HTTP/controller tests should map to presentation
|
|
87
|
+
assert layer_mapping.get('http') == 'presentation' or \
|
|
88
|
+
layer_mapping.get('controller') == 'presentation' or \
|
|
89
|
+
'http' in layer_mapping.get('presentation', []), \
|
|
90
|
+
"HTTP handler tests must be in presentation layer"
|