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.
Files changed (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.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))