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,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform tests: WMBT consistency validation.
|
|
3
|
+
|
|
4
|
+
Validates that WMBT references in wagon manifests and feature files match
|
|
5
|
+
the actual WMBT YAML files present in the wagon directory.
|
|
6
|
+
|
|
7
|
+
Source of truth: WMBT YAML files in plan/{wagon}/*.yaml
|
|
8
|
+
"""
|
|
9
|
+
import pytest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Set, List, Tuple
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
# Path constants
|
|
15
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
16
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def wmbt_files():
|
|
21
|
+
"""
|
|
22
|
+
Discover all WMBT files in plan/{wagon}/ directories.
|
|
23
|
+
|
|
24
|
+
Returns: Dict[wagon_slug, Set[wmbt_code]]
|
|
25
|
+
e.g., {"generate-identifiers": {"L001", "L002", "P001", "C001", ...}}
|
|
26
|
+
"""
|
|
27
|
+
wmbt_map: Dict[str, Set[str]] = {}
|
|
28
|
+
|
|
29
|
+
# Pattern: plan/{wagon_dir}/{STEP_CODE}{NNN}.yaml
|
|
30
|
+
# Where STEP_CODE is one of: D, L, P, C, E, M, Y, R, K
|
|
31
|
+
# And NNN is 001-999
|
|
32
|
+
|
|
33
|
+
for wagon_dir in PLAN_DIR.iterdir():
|
|
34
|
+
if not wagon_dir.is_dir():
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# Directory name uses underscores, convert to kebab-case slug
|
|
38
|
+
dir_name = wagon_dir.name
|
|
39
|
+
wagon_slug = dir_name.replace("_", "-")
|
|
40
|
+
|
|
41
|
+
wmbt_codes: Set[str] = set()
|
|
42
|
+
|
|
43
|
+
# Find all WMBT files matching the pattern
|
|
44
|
+
for yaml_file in wagon_dir.glob("*.yaml"):
|
|
45
|
+
filename = yaml_file.stem # e.g., "L001", "C005"
|
|
46
|
+
|
|
47
|
+
# Check if it matches WMBT pattern: {STEP_CODE}{NNN}
|
|
48
|
+
if len(filename) == 4 and filename[0] in "DLPCEMYRK" and filename[1:].isdigit():
|
|
49
|
+
wmbt_codes.add(filename)
|
|
50
|
+
|
|
51
|
+
if wmbt_codes:
|
|
52
|
+
wmbt_map[wagon_slug] = wmbt_codes
|
|
53
|
+
|
|
54
|
+
return wmbt_map
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def feature_files():
|
|
59
|
+
"""
|
|
60
|
+
Discover all feature files in plan/{wagon}/features/*.yaml.
|
|
61
|
+
|
|
62
|
+
Returns: List[Tuple[wagon_slug, feature_path, feature_data]]
|
|
63
|
+
"""
|
|
64
|
+
features: List[Tuple[str, Path, Dict]] = []
|
|
65
|
+
|
|
66
|
+
for wagon_dir in PLAN_DIR.iterdir():
|
|
67
|
+
if not wagon_dir.is_dir():
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# Directory name uses underscores, convert to kebab-case slug
|
|
71
|
+
dir_name = wagon_dir.name
|
|
72
|
+
wagon_slug = dir_name.replace("_", "-")
|
|
73
|
+
|
|
74
|
+
features_dir = wagon_dir / "features"
|
|
75
|
+
|
|
76
|
+
if features_dir.exists() and features_dir.is_dir():
|
|
77
|
+
for feature_file in features_dir.glob("*.yaml"):
|
|
78
|
+
try:
|
|
79
|
+
with open(feature_file) as f:
|
|
80
|
+
feature_data = yaml.safe_load(f)
|
|
81
|
+
if feature_data:
|
|
82
|
+
features.append((wagon_slug, feature_file, feature_data))
|
|
83
|
+
except Exception as e:
|
|
84
|
+
pytest.fail(f"Failed to load feature file {feature_file}: {e}")
|
|
85
|
+
|
|
86
|
+
return features
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.platform
|
|
90
|
+
@pytest.mark.e2e
|
|
91
|
+
def test_wagon_manifest_wmbt_codes_exist_as_files(wagon_manifests, wmbt_files):
|
|
92
|
+
"""
|
|
93
|
+
SPEC-PLATFORM-WMBT-0001: Wagon manifest WMBT codes must exist as YAML files
|
|
94
|
+
|
|
95
|
+
Given: Wagon manifest with wmbt section listing codes
|
|
96
|
+
When: Checking if WMBT files exist
|
|
97
|
+
Then: Each WMBT code in manifest has corresponding {CODE}.yaml file
|
|
98
|
+
in the wagon's directory
|
|
99
|
+
|
|
100
|
+
Source of Truth: WMBT YAML files in plan/{wagon}/{CODE}.yaml
|
|
101
|
+
"""
|
|
102
|
+
errors = []
|
|
103
|
+
|
|
104
|
+
for path, manifest in wagon_manifests:
|
|
105
|
+
wagon_slug = manifest.get("wagon", "")
|
|
106
|
+
wmbt_section = manifest.get("wmbt", {})
|
|
107
|
+
|
|
108
|
+
if not wmbt_section:
|
|
109
|
+
# No WMBTs declared - skip
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Get actual WMBT files for this wagon
|
|
113
|
+
actual_wmbts = wmbt_files.get(wagon_slug, set())
|
|
114
|
+
|
|
115
|
+
# Check each WMBT code in manifest
|
|
116
|
+
for wmbt_code, statement in wmbt_section.items():
|
|
117
|
+
# Skip metadata fields
|
|
118
|
+
if wmbt_code in ("total", "coverage"):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if wmbt_code not in actual_wmbts:
|
|
122
|
+
errors.append(
|
|
123
|
+
f"Wagon '{wagon_slug}' declares WMBT '{wmbt_code}' in manifest, "
|
|
124
|
+
f"but file plan/{wagon_slug}/{wmbt_code}.yaml does not exist. "
|
|
125
|
+
f"Available WMBTs: {sorted(actual_wmbts)}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if errors:
|
|
129
|
+
pytest.fail("\n".join(errors))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.platform
|
|
133
|
+
@pytest.mark.e2e
|
|
134
|
+
def test_wmbt_files_declared_in_wagon_manifest(wagon_manifests, wmbt_files):
|
|
135
|
+
"""
|
|
136
|
+
SPEC-PLATFORM-WMBT-0002: All WMBT files must be declared in wagon manifest
|
|
137
|
+
|
|
138
|
+
Given: WMBT YAML files in plan/{wagon}/ directory
|
|
139
|
+
When: Checking wagon manifest wmbt section
|
|
140
|
+
Then: Each WMBT file has corresponding entry in manifest wmbt section
|
|
141
|
+
|
|
142
|
+
Source of Truth: WMBT YAML files in plan/{wagon}/{CODE}.yaml
|
|
143
|
+
"""
|
|
144
|
+
errors = []
|
|
145
|
+
|
|
146
|
+
for wagon_slug, actual_wmbts in wmbt_files.items():
|
|
147
|
+
# Find corresponding wagon manifest
|
|
148
|
+
wagon_manifest = None
|
|
149
|
+
for path, manifest in wagon_manifests:
|
|
150
|
+
if manifest.get("wagon") == wagon_slug:
|
|
151
|
+
wagon_manifest = manifest
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if not wagon_manifest:
|
|
155
|
+
# Wagon manifest not found - skip (other tests will catch this)
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
wmbt_section = wagon_manifest.get("wmbt", {})
|
|
159
|
+
declared_wmbts = set(k for k in wmbt_section.keys() if k not in ("total", "coverage"))
|
|
160
|
+
|
|
161
|
+
# Check if all file WMBTs are declared
|
|
162
|
+
undeclared = actual_wmbts - declared_wmbts
|
|
163
|
+
|
|
164
|
+
if undeclared:
|
|
165
|
+
errors.append(
|
|
166
|
+
f"Wagon '{wagon_slug}' has WMBT files {sorted(undeclared)} "
|
|
167
|
+
f"but they are not declared in the manifest wmbt section. "
|
|
168
|
+
f"Declared WMBTs: {sorted(declared_wmbts)}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if errors:
|
|
172
|
+
pytest.fail("\n".join(errors))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.platform
|
|
176
|
+
@pytest.mark.e2e
|
|
177
|
+
def test_feature_acceptance_criteria_match_wmbt_files(feature_files, wmbt_files):
|
|
178
|
+
"""
|
|
179
|
+
SPEC-PLATFORM-WMBT-0003: Feature acceptance_criteria codes must match actual WMBT files
|
|
180
|
+
|
|
181
|
+
Given: Feature files with acceptance_criteria section
|
|
182
|
+
When: Checking acceptance criteria URNs
|
|
183
|
+
Then: Each acceptance criteria code references an existing WMBT file
|
|
184
|
+
|
|
185
|
+
Source of Truth: WMBT YAML files in plan/{wagon}/{CODE}.yaml
|
|
186
|
+
|
|
187
|
+
Note: Acceptance criteria URNs follow pattern: acc:{wagon}:{WMBT_CODE}-{TEST_TYPE}-{NNN}
|
|
188
|
+
We extract {WMBT_CODE} and verify it exists as a file
|
|
189
|
+
"""
|
|
190
|
+
errors = []
|
|
191
|
+
|
|
192
|
+
for wagon_slug, feature_path, feature_data in feature_files:
|
|
193
|
+
acceptance_criteria = feature_data.get("acceptance_criteria", {})
|
|
194
|
+
|
|
195
|
+
if not acceptance_criteria:
|
|
196
|
+
# No acceptance criteria - skip
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Get actual WMBT files for this wagon
|
|
200
|
+
actual_wmbts = wmbt_files.get(wagon_slug, set())
|
|
201
|
+
|
|
202
|
+
# Check each acceptance criterion
|
|
203
|
+
for criterion_key, criterion_data in acceptance_criteria.items():
|
|
204
|
+
urn = criterion_data.get("urn", "")
|
|
205
|
+
|
|
206
|
+
if not urn:
|
|
207
|
+
errors.append(
|
|
208
|
+
f"Feature '{feature_path.name}' in wagon '{wagon_slug}' "
|
|
209
|
+
f"has acceptance criterion '{criterion_key}' without URN"
|
|
210
|
+
)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Parse URN: acc:{wagon}:{WMBT_CODE}-{TEST_TYPE}-{NNN}
|
|
214
|
+
# Example: acc:generate-identifiers:L001-UNIT-001
|
|
215
|
+
# Extract WMBT_CODE: L001
|
|
216
|
+
parts = urn.split(":")
|
|
217
|
+
if len(parts) < 3:
|
|
218
|
+
errors.append(
|
|
219
|
+
f"Feature '{feature_path.name}' in wagon '{wagon_slug}' "
|
|
220
|
+
f"has malformed acceptance criterion URN: {urn}"
|
|
221
|
+
)
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
# Get the code part: "L001-UNIT-001"
|
|
225
|
+
code_part = parts[2]
|
|
226
|
+
|
|
227
|
+
# Extract WMBT code (before first hyphen): "L001"
|
|
228
|
+
wmbt_code = code_part.split("-")[0]
|
|
229
|
+
|
|
230
|
+
# Verify this WMBT file exists
|
|
231
|
+
if wmbt_code not in actual_wmbts:
|
|
232
|
+
errors.append(
|
|
233
|
+
f"Feature '{feature_path.name}' in wagon '{wagon_slug}' "
|
|
234
|
+
f"references WMBT '{wmbt_code}' (from URN: {urn}), "
|
|
235
|
+
f"but file plan/{wagon_slug}/{wmbt_code}.yaml does not exist. "
|
|
236
|
+
f"Available WMBTs: {sorted(actual_wmbts)}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if errors:
|
|
240
|
+
pytest.fail("\n".join(errors))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@pytest.mark.platform
|
|
244
|
+
@pytest.mark.e2e
|
|
245
|
+
def test_wmbt_file_urns_match_expected_pattern(wmbt_files):
|
|
246
|
+
"""
|
|
247
|
+
SPEC-PLATFORM-WMBT-0004: WMBT files must have URNs matching their filename
|
|
248
|
+
|
|
249
|
+
Given: WMBT YAML file at plan/{wagon}/{CODE}.yaml
|
|
250
|
+
When: Reading the file's URN field
|
|
251
|
+
Then: URN must be wmbt:{wagon}:{CODE}
|
|
252
|
+
|
|
253
|
+
Source of Truth: WMBT YAML files in plan/{wagon}/{CODE}.yaml
|
|
254
|
+
"""
|
|
255
|
+
errors = []
|
|
256
|
+
|
|
257
|
+
for wagon_slug, wmbt_codes in wmbt_files.items():
|
|
258
|
+
# Convert kebab-case slug to underscore directory name
|
|
259
|
+
dir_name = wagon_slug.replace("-", "_")
|
|
260
|
+
wagon_dir = PLAN_DIR / dir_name
|
|
261
|
+
|
|
262
|
+
for wmbt_code in wmbt_codes:
|
|
263
|
+
wmbt_file = wagon_dir / f"{wmbt_code}.yaml"
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
with open(wmbt_file) as f:
|
|
267
|
+
wmbt_data = yaml.safe_load(f)
|
|
268
|
+
|
|
269
|
+
if not wmbt_data:
|
|
270
|
+
errors.append(
|
|
271
|
+
f"WMBT file {wmbt_file} is empty or invalid YAML"
|
|
272
|
+
)
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
# Check URN
|
|
276
|
+
urn = wmbt_data.get("urn", "")
|
|
277
|
+
expected_urn = f"wmbt:{wagon_slug}:{wmbt_code}"
|
|
278
|
+
|
|
279
|
+
if urn != expected_urn:
|
|
280
|
+
errors.append(
|
|
281
|
+
f"WMBT file {wmbt_file} has URN '{urn}', "
|
|
282
|
+
f"but expected '{expected_urn}' based on filename"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
errors.append(
|
|
287
|
+
f"Failed to read WMBT file {wmbt_file}: {e}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if errors:
|
|
291
|
+
pytest.fail("\n".join(errors))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@pytest.mark.platform
|
|
295
|
+
@pytest.mark.e2e
|
|
296
|
+
def test_wmbt_count_matches_actual_files(wagon_manifests, wmbt_files):
|
|
297
|
+
"""
|
|
298
|
+
SPEC-PLATFORM-WMBT-0005: Wagon manifest 'total' field must match actual WMBT count
|
|
299
|
+
|
|
300
|
+
Given: Wagon manifest with wmbt.total field
|
|
301
|
+
When: Counting actual WMBT files in wagon directory
|
|
302
|
+
Then: wmbt.total equals the count of WMBT files
|
|
303
|
+
|
|
304
|
+
Source of Truth: WMBT YAML files in plan/{wagon}/{CODE}.yaml
|
|
305
|
+
"""
|
|
306
|
+
errors = []
|
|
307
|
+
|
|
308
|
+
for path, manifest in wagon_manifests:
|
|
309
|
+
wagon_slug = manifest.get("wagon", "")
|
|
310
|
+
wmbt_section = manifest.get("wmbt", {})
|
|
311
|
+
|
|
312
|
+
if not wmbt_section:
|
|
313
|
+
# No WMBTs - skip
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
declared_total = wmbt_section.get("total", 0)
|
|
317
|
+
actual_wmbts = wmbt_files.get(wagon_slug, set())
|
|
318
|
+
actual_count = len(actual_wmbts)
|
|
319
|
+
|
|
320
|
+
if declared_total != actual_count:
|
|
321
|
+
errors.append(
|
|
322
|
+
f"Wagon '{wagon_slug}' declares wmbt.total={declared_total}, "
|
|
323
|
+
f"but has {actual_count} actual WMBT files: {sorted(actual_wmbts)}"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if errors:
|
|
327
|
+
pytest.fail("\n".join(errors))
|