atdd 0.2.1__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 +6 -0
- atdd/__main__.py +4 -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.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- atdd-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test train infrastructure validation (SESSION-12).
|
|
3
|
+
|
|
4
|
+
Validates conventions from:
|
|
5
|
+
- atdd/coder/conventions/train.convention.yaml
|
|
6
|
+
- atdd/coder/conventions/boundaries.convention.yaml
|
|
7
|
+
- atdd/coder/conventions/refactor.convention.yaml
|
|
8
|
+
|
|
9
|
+
Enforces:
|
|
10
|
+
- Train infrastructure exists (python/trains/)
|
|
11
|
+
- Wagons implement run_train() for train mode
|
|
12
|
+
- Contract validator is real (not mock)
|
|
13
|
+
- E2E tests use production TrainRunner
|
|
14
|
+
- Station Master pattern in game.py
|
|
15
|
+
|
|
16
|
+
Rationale:
|
|
17
|
+
Trains are production orchestration, not test infrastructure (SESSION-12).
|
|
18
|
+
These audits ensure the train composition root pattern is correctly implemented.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
import ast
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import List, Dict, Set, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Path constants
|
|
29
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
30
|
+
TRAINS_DIR = REPO_ROOT / "python" / "trains"
|
|
31
|
+
WAGONS_DIR = REPO_ROOT / "python"
|
|
32
|
+
GAME_PY = REPO_ROOT / "python" / "game.py"
|
|
33
|
+
E2E_CONFTEST = REPO_ROOT / "e2e" / "conftest.py"
|
|
34
|
+
CONTRACT_VALIDATOR = REPO_ROOT / "e2e" / "shared" / "fixtures" / "contract_validator.py"
|
|
35
|
+
TRAIN_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "train.convention.yaml"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_wagons() -> List[Path]:
|
|
39
|
+
"""Find all wagon.py files."""
|
|
40
|
+
wagons = []
|
|
41
|
+
for wagon_file in WAGONS_DIR.glob("*/wagon.py"):
|
|
42
|
+
# Skip trains directory
|
|
43
|
+
if "trains" in wagon_file.parts:
|
|
44
|
+
continue
|
|
45
|
+
wagons.append(wagon_file)
|
|
46
|
+
return wagons
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def has_run_train_function(file_path: Path) -> Tuple[bool, str]:
|
|
50
|
+
"""
|
|
51
|
+
Check if wagon.py has run_train() function.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
(has_function, implementation_type)
|
|
55
|
+
implementation_type: "function", "method", or "none"
|
|
56
|
+
"""
|
|
57
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
58
|
+
content = f.read()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
tree = ast.parse(content)
|
|
62
|
+
|
|
63
|
+
# Check for module-level function
|
|
64
|
+
for node in ast.walk(tree):
|
|
65
|
+
if isinstance(node, ast.FunctionDef) and node.name == "run_train":
|
|
66
|
+
# Check if it's at module level (not inside a class)
|
|
67
|
+
if isinstance(node, ast.FunctionDef):
|
|
68
|
+
return (True, "function")
|
|
69
|
+
|
|
70
|
+
# Check for class method
|
|
71
|
+
for node in ast.walk(tree):
|
|
72
|
+
if isinstance(node, ast.ClassDef):
|
|
73
|
+
for item in node.body:
|
|
74
|
+
if isinstance(item, ast.FunctionDef) and item.name == "run_train":
|
|
75
|
+
return (True, "method")
|
|
76
|
+
|
|
77
|
+
return (False, "none")
|
|
78
|
+
|
|
79
|
+
except SyntaxError:
|
|
80
|
+
return (False, "none")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_imports_from_file(file_path: Path) -> Set[str]:
|
|
84
|
+
"""Extract all import statements from a file."""
|
|
85
|
+
imports = set()
|
|
86
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
87
|
+
for line in f:
|
|
88
|
+
# Match: from X import Y or import X
|
|
89
|
+
if 'import' in line and not line.strip().startswith('#'):
|
|
90
|
+
imports.add(line.strip())
|
|
91
|
+
return imports
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ============================================================================
|
|
95
|
+
# TRAIN INFRASTRUCTURE TESTS
|
|
96
|
+
# ============================================================================
|
|
97
|
+
|
|
98
|
+
def test_trains_directory_exists():
|
|
99
|
+
"""Train infrastructure must exist at python/trains/."""
|
|
100
|
+
assert TRAINS_DIR.exists(), (
|
|
101
|
+
f"Train infrastructure directory not found: {TRAINS_DIR}\n"
|
|
102
|
+
"Expected: python/trains/\n"
|
|
103
|
+
"See: atdd/coder/conventions/train.convention.yaml"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert TRAINS_DIR.is_dir(), f"{TRAINS_DIR} exists but is not a directory"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_train_infrastructure_files_exist():
|
|
110
|
+
"""
|
|
111
|
+
Train infrastructure files must exist.
|
|
112
|
+
|
|
113
|
+
Required files:
|
|
114
|
+
- python/trains/__init__.py
|
|
115
|
+
- python/trains/runner.py (TrainRunner class)
|
|
116
|
+
- python/trains/models.py (TrainSpec, TrainResult, Cargo)
|
|
117
|
+
"""
|
|
118
|
+
required_files = {
|
|
119
|
+
"__init__.py": "Package initialization",
|
|
120
|
+
"runner.py": "TrainRunner class",
|
|
121
|
+
"models.py": "Data models (TrainSpec, TrainResult, Cargo)"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
missing_files = []
|
|
125
|
+
for filename, description in required_files.items():
|
|
126
|
+
file_path = TRAINS_DIR / filename
|
|
127
|
+
if not file_path.exists():
|
|
128
|
+
missing_files.append((filename, description))
|
|
129
|
+
|
|
130
|
+
if missing_files:
|
|
131
|
+
pytest.fail(
|
|
132
|
+
f"\nMissing {len(missing_files)} train infrastructure files:\n\n" +
|
|
133
|
+
"\n".join(f" python/trains/{name}\n Purpose: {desc}"
|
|
134
|
+
for name, desc in missing_files) +
|
|
135
|
+
"\n\nSee: atdd/coder/conventions/train.convention.yaml::train_structure"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_train_runner_class_exists():
|
|
140
|
+
"""TrainRunner class must exist in python/trains/runner.py."""
|
|
141
|
+
runner_file = TRAINS_DIR / "runner.py"
|
|
142
|
+
|
|
143
|
+
assert runner_file.exists(), f"runner.py not found: {runner_file}"
|
|
144
|
+
|
|
145
|
+
with open(runner_file, 'r', encoding='utf-8') as f:
|
|
146
|
+
content = f.read()
|
|
147
|
+
|
|
148
|
+
# Check for TrainRunner class
|
|
149
|
+
assert "class TrainRunner" in content, (
|
|
150
|
+
"TrainRunner class not found in python/trains/runner.py\n"
|
|
151
|
+
"Expected: class TrainRunner with execute() method"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Check for key methods
|
|
155
|
+
required_methods = ["__init__", "execute", "_execute_step"]
|
|
156
|
+
missing_methods = [m for m in required_methods if f"def {m}" not in content]
|
|
157
|
+
|
|
158
|
+
if missing_methods:
|
|
159
|
+
pytest.fail(
|
|
160
|
+
f"\nTrainRunner missing required methods: {', '.join(missing_methods)}\n"
|
|
161
|
+
"Expected methods: __init__, execute, _execute_step"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_train_models_exist():
|
|
166
|
+
"""Train data models must exist in python/trains/models.py."""
|
|
167
|
+
models_file = TRAINS_DIR / "models.py"
|
|
168
|
+
|
|
169
|
+
assert models_file.exists(), f"models.py not found: {models_file}"
|
|
170
|
+
|
|
171
|
+
with open(models_file, 'r', encoding='utf-8') as f:
|
|
172
|
+
content = f.read()
|
|
173
|
+
|
|
174
|
+
# Check for required models
|
|
175
|
+
required_models = ["TrainSpec", "TrainStep", "TrainResult", "Cargo"]
|
|
176
|
+
missing_models = [m for m in required_models if f"class {m}" not in content]
|
|
177
|
+
|
|
178
|
+
if missing_models:
|
|
179
|
+
pytest.fail(
|
|
180
|
+
f"\nMissing train models: {', '.join(missing_models)}\n"
|
|
181
|
+
"Expected in python/trains/models.py:\n"
|
|
182
|
+
" - TrainSpec: Parsed train definition\n"
|
|
183
|
+
" - TrainStep: Single step in sequence\n"
|
|
184
|
+
" - TrainResult: Execution result\n"
|
|
185
|
+
" - Cargo: Artifacts passed between wagons"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ============================================================================
|
|
190
|
+
# WAGON TRAIN MODE TESTS
|
|
191
|
+
# ============================================================================
|
|
192
|
+
|
|
193
|
+
def test_wagons_implement_run_train():
|
|
194
|
+
"""
|
|
195
|
+
Wagons must implement run_train() to participate in train orchestration.
|
|
196
|
+
|
|
197
|
+
Expected signature:
|
|
198
|
+
def run_train(inputs: Dict[str, Any], timing: Dict[str, float] = None) -> Dict[str, Any]
|
|
199
|
+
|
|
200
|
+
Can be either:
|
|
201
|
+
- Module-level function: def run_train(...)
|
|
202
|
+
- Class method: class XxxWagon: def run_train(self, ...)
|
|
203
|
+
"""
|
|
204
|
+
wagons = find_wagons()
|
|
205
|
+
|
|
206
|
+
assert len(wagons) > 0, "No wagons found in python/ directory"
|
|
207
|
+
|
|
208
|
+
missing_run_train = []
|
|
209
|
+
for wagon_file in wagons:
|
|
210
|
+
has_function, impl_type = has_run_train_function(wagon_file)
|
|
211
|
+
if not has_function:
|
|
212
|
+
wagon_name = wagon_file.parent.name
|
|
213
|
+
missing_run_train.append(wagon_name)
|
|
214
|
+
|
|
215
|
+
# Allow some wagons to not have run_train yet (partial migration)
|
|
216
|
+
# But key wagons from SESSION-12 must have it
|
|
217
|
+
required_wagons = ["pace_dilemmas", "supply_fragments", "juggle_domains", "resolve_dilemmas"]
|
|
218
|
+
missing_required = [w for w in required_wagons if w in missing_run_train]
|
|
219
|
+
|
|
220
|
+
if missing_required:
|
|
221
|
+
pytest.fail(
|
|
222
|
+
f"\nCritical wagons missing run_train() implementation:\n\n" +
|
|
223
|
+
"\n".join(f" python/{name}/wagon.py" for name in missing_required) +
|
|
224
|
+
"\n\nExpected signature:\n"
|
|
225
|
+
" def run_train(inputs: Dict[str, Any], timing: Dict[str, float] = None) -> Dict[str, Any]\n"
|
|
226
|
+
"\nSee: atdd/coder/conventions/train.convention.yaml::wagon_train_interface"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ============================================================================
|
|
231
|
+
# STATION MASTER TESTS (game.py)
|
|
232
|
+
# ============================================================================
|
|
233
|
+
|
|
234
|
+
def test_game_py_imports_train_runner():
|
|
235
|
+
"""game.py must import TrainRunner (Station Master pattern)."""
|
|
236
|
+
assert GAME_PY.exists(), f"game.py not found: {GAME_PY}"
|
|
237
|
+
|
|
238
|
+
imports = extract_imports_from_file(GAME_PY)
|
|
239
|
+
|
|
240
|
+
has_train_import = any("trains.runner import TrainRunner" in imp for imp in imports)
|
|
241
|
+
|
|
242
|
+
assert has_train_import, (
|
|
243
|
+
"game.py must import TrainRunner\n"
|
|
244
|
+
"Expected: from trains.runner import TrainRunner\n"
|
|
245
|
+
"See: atdd/coder/conventions/train.convention.yaml::station_master"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_game_py_has_journey_map():
|
|
250
|
+
"""game.py must have JOURNEY_MAP routing actions to trains."""
|
|
251
|
+
with open(GAME_PY, 'r', encoding='utf-8') as f:
|
|
252
|
+
content = f.read()
|
|
253
|
+
|
|
254
|
+
assert "JOURNEY_MAP" in content, (
|
|
255
|
+
"game.py must define JOURNEY_MAP dictionary\n"
|
|
256
|
+
"Expected: JOURNEY_MAP = {'action': 'train_id', ...}\n"
|
|
257
|
+
"See: atdd/coder/conventions/train.convention.yaml::station_master"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_game_py_has_train_execution_endpoint():
|
|
262
|
+
"""game.py must have /trains/execute endpoint."""
|
|
263
|
+
with open(GAME_PY, 'r', encoding='utf-8') as f:
|
|
264
|
+
content = f.read()
|
|
265
|
+
|
|
266
|
+
has_endpoint = '"/trains/execute"' in content or "'/trains/execute'" in content
|
|
267
|
+
|
|
268
|
+
assert has_endpoint, (
|
|
269
|
+
"game.py must have /trains/execute endpoint\n"
|
|
270
|
+
"Expected: @app.post('/trains/execute')\n"
|
|
271
|
+
"See: atdd/coder/conventions/train.convention.yaml::station_master"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ============================================================================
|
|
276
|
+
# E2E TEST INFRASTRUCTURE TESTS
|
|
277
|
+
# ============================================================================
|
|
278
|
+
|
|
279
|
+
def test_e2e_conftest_uses_production_train_runner():
|
|
280
|
+
"""
|
|
281
|
+
E2E conftest must use production TrainRunner, not mocks.
|
|
282
|
+
|
|
283
|
+
This ensures tests validate production orchestration (zero drift).
|
|
284
|
+
"""
|
|
285
|
+
assert E2E_CONFTEST.exists(), f"E2E conftest not found: {E2E_CONFTEST}"
|
|
286
|
+
|
|
287
|
+
imports = extract_imports_from_file(E2E_CONFTEST)
|
|
288
|
+
|
|
289
|
+
# Should import from trains.runner (production)
|
|
290
|
+
has_production_import = any("trains.runner import TrainRunner" in imp for imp in imports)
|
|
291
|
+
|
|
292
|
+
# Should NOT import mock
|
|
293
|
+
has_mock_import = any("mock_train_runner" in imp for imp in imports)
|
|
294
|
+
|
|
295
|
+
assert has_production_import, (
|
|
296
|
+
"E2E conftest must import production TrainRunner\n"
|
|
297
|
+
"Expected: from trains.runner import TrainRunner\n"
|
|
298
|
+
"Found: Mock import still present\n"
|
|
299
|
+
"See: atdd/coder/conventions/train.convention.yaml::testing_pattern"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
assert not has_mock_import, (
|
|
303
|
+
"E2E conftest should NOT use MockTrainRunner\n"
|
|
304
|
+
"Remove: from e2e.shared.fixtures.mock_train_runner import MockTrainRunner\n"
|
|
305
|
+
"Use production TrainRunner instead"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_contract_validator_is_real():
|
|
310
|
+
"""
|
|
311
|
+
Contract validator must be real JSON schema validator, not mock.
|
|
312
|
+
|
|
313
|
+
Real validator uses jsonschema library for contract validation.
|
|
314
|
+
"""
|
|
315
|
+
assert CONTRACT_VALIDATOR.exists(), f"Contract validator not found: {CONTRACT_VALIDATOR}"
|
|
316
|
+
|
|
317
|
+
with open(CONTRACT_VALIDATOR, 'r', encoding='utf-8') as f:
|
|
318
|
+
content = f.read()
|
|
319
|
+
|
|
320
|
+
# Check for real implementation
|
|
321
|
+
has_jsonschema = "import jsonschema" in content or "from jsonschema import" in content
|
|
322
|
+
has_validate_method = "def validate(" in content
|
|
323
|
+
|
|
324
|
+
assert has_jsonschema, (
|
|
325
|
+
"Contract validator must use jsonschema library\n"
|
|
326
|
+
"Expected: import jsonschema\n"
|
|
327
|
+
"File: e2e/shared/fixtures/contract_validator.py"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
assert has_validate_method, (
|
|
331
|
+
"Contract validator must have validate() method\n"
|
|
332
|
+
"Expected: def validate(self, artifact, schema_path)\n"
|
|
333
|
+
"File: e2e/shared/fixtures/contract_validator.py"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Check it's not a mock
|
|
337
|
+
is_mock = "Mock" in content and "mock" in content.lower()
|
|
338
|
+
|
|
339
|
+
assert not is_mock, (
|
|
340
|
+
"Contract validator appears to be a mock\n"
|
|
341
|
+
"Replace with real JSON schema validation\n"
|
|
342
|
+
"See: atdd/coder/conventions/train.convention.yaml::cargo_pattern"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_e2e_conftest_uses_real_contract_validator():
|
|
347
|
+
"""E2E conftest must use real ContractValidator, not mock."""
|
|
348
|
+
imports = extract_imports_from_file(E2E_CONFTEST)
|
|
349
|
+
|
|
350
|
+
# Should import real validator
|
|
351
|
+
has_real_import = any("contract_validator import ContractValidator" in imp
|
|
352
|
+
and "mock" not in imp.lower()
|
|
353
|
+
for imp in imports)
|
|
354
|
+
|
|
355
|
+
# Should NOT import mock
|
|
356
|
+
has_mock_import = any("mock_contract_validator" in imp for imp in imports)
|
|
357
|
+
|
|
358
|
+
assert has_real_import, (
|
|
359
|
+
"E2E conftest must import real ContractValidator\n"
|
|
360
|
+
"Expected: from e2e.shared.fixtures.contract_validator import ContractValidator\n"
|
|
361
|
+
"File: e2e/conftest.py"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
assert not has_mock_import, (
|
|
365
|
+
"E2E conftest should NOT use MockContractValidator\n"
|
|
366
|
+
"Remove: from e2e.shared.fixtures.mock_contract_validator import MockContractValidator\n"
|
|
367
|
+
"Use real ContractValidator instead"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ============================================================================
|
|
372
|
+
# CONVENTION DOCUMENTATION TESTS
|
|
373
|
+
# ============================================================================
|
|
374
|
+
|
|
375
|
+
def test_train_convention_exists():
|
|
376
|
+
"""Train convention file must exist."""
|
|
377
|
+
assert TRAIN_CONVENTION.exists(), (
|
|
378
|
+
f"Train convention not found: {TRAIN_CONVENTION}\n"
|
|
379
|
+
"Expected: atdd/coder/conventions/train.convention.yaml"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_train_convention_documents_key_patterns():
|
|
384
|
+
"""
|
|
385
|
+
Train convention must document key implementation patterns.
|
|
386
|
+
|
|
387
|
+
Required sections:
|
|
388
|
+
- composition_hierarchy (with train level)
|
|
389
|
+
- wagon_train_mode (run_train signature)
|
|
390
|
+
- cargo_pattern (artifact flow)
|
|
391
|
+
- station_master (game.py pattern)
|
|
392
|
+
- testing_pattern (E2E tests)
|
|
393
|
+
"""
|
|
394
|
+
with open(TRAIN_CONVENTION, 'r', encoding='utf-8') as f:
|
|
395
|
+
content = f.read()
|
|
396
|
+
|
|
397
|
+
required_sections = [
|
|
398
|
+
"composition_hierarchy",
|
|
399
|
+
"wagon_train_mode",
|
|
400
|
+
"cargo_pattern",
|
|
401
|
+
"station_master",
|
|
402
|
+
"testing_pattern"
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
missing_sections = [s for s in required_sections if s not in content]
|
|
406
|
+
|
|
407
|
+
if missing_sections:
|
|
408
|
+
pytest.fail(
|
|
409
|
+
f"\nTrain convention missing required sections:\n\n" +
|
|
410
|
+
"\n".join(f" - {section}" for section in missing_sections) +
|
|
411
|
+
f"\n\nFile: {TRAIN_CONVENTION}\n"
|
|
412
|
+
"See: SESSION-12 implementation plan for required documentation"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ============================================================================
|
|
417
|
+
# BOUNDARY ENFORCEMENT TESTS
|
|
418
|
+
# ============================================================================
|
|
419
|
+
|
|
420
|
+
def test_no_wagon_to_wagon_imports():
|
|
421
|
+
"""
|
|
422
|
+
Wagons must NOT import from other wagons.
|
|
423
|
+
|
|
424
|
+
Enforces boundary pattern: wagons communicate via contracts only.
|
|
425
|
+
This is checked in wagon.py files specifically (not all python files).
|
|
426
|
+
"""
|
|
427
|
+
wagons = find_wagons()
|
|
428
|
+
violations = []
|
|
429
|
+
|
|
430
|
+
for wagon_file in wagons:
|
|
431
|
+
wagon_name = wagon_file.parent.name
|
|
432
|
+
|
|
433
|
+
with open(wagon_file, 'r', encoding='utf-8') as f:
|
|
434
|
+
content = f.read()
|
|
435
|
+
|
|
436
|
+
# Find imports from other wagons
|
|
437
|
+
for other_wagon in wagons:
|
|
438
|
+
other_name = other_wagon.parent.name
|
|
439
|
+
if other_name == wagon_name:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
# Check for imports like: from other_wagon.xxx import
|
|
443
|
+
pattern = f"from {other_name}\\."
|
|
444
|
+
if re.search(pattern, content):
|
|
445
|
+
violations.append((wagon_name, other_name, wagon_file))
|
|
446
|
+
|
|
447
|
+
if violations:
|
|
448
|
+
pytest.fail(
|
|
449
|
+
f"\nFound {len(violations)} wagon boundary violations:\n\n" +
|
|
450
|
+
"\n".join(f" {wagon} imports from {other}\n File: {file}"
|
|
451
|
+
for wagon, other, file in violations) +
|
|
452
|
+
"\n\nWagons must communicate via contracts only, not direct imports\n"
|
|
453
|
+
"See: atdd/coder/conventions/boundaries.convention.yaml"
|
|
454
|
+
)
|