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
|
File without changes
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared fixtures for platform tests.
|
|
3
|
+
|
|
4
|
+
Provides schemas, file discovery, and validation utilities for E2E platform tests.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import yaml
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, List, Tuple
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Path constants
|
|
14
|
+
# File is at atdd/coach/audits/shared_fixtures.py, so go up 3 levels to reach repo root
|
|
15
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
16
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
17
|
+
ATDD_DIR = REPO_ROOT / "atdd"
|
|
18
|
+
CONTRACTS_DIR = REPO_ROOT / "contracts"
|
|
19
|
+
TELEMETRY_DIR = REPO_ROOT / "telemetry"
|
|
20
|
+
WEB_DIR = REPO_ROOT / "web"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Schema fixtures - Planner schemas
|
|
24
|
+
@pytest.fixture(scope="module")
|
|
25
|
+
def wagon_schema() -> Dict[str, Any]:
|
|
26
|
+
"""Load wagon.schema.json for validation."""
|
|
27
|
+
with open(ATDD_DIR / "planner/schemas/wagon.schema.json") as f:
|
|
28
|
+
return json.load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(scope="module")
|
|
32
|
+
def wmbt_schema() -> Dict[str, Any]:
|
|
33
|
+
"""Load wmbt.schema.json for validation."""
|
|
34
|
+
with open(ATDD_DIR / "planner/schemas/wmbt.schema.json") as f:
|
|
35
|
+
return json.load(f)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(scope="module")
|
|
39
|
+
def feature_schema() -> Dict[str, Any]:
|
|
40
|
+
"""Load feature.schema.json for validation."""
|
|
41
|
+
with open(ATDD_DIR / "planner/schemas/feature.schema.json") as f:
|
|
42
|
+
return json.load(f)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture(scope="module")
|
|
46
|
+
def acceptance_schema() -> Dict[str, Any]:
|
|
47
|
+
"""Load acceptance.schema.json for validation."""
|
|
48
|
+
with open(ATDD_DIR / "planner/schemas/acceptance.schema.json") as f:
|
|
49
|
+
return json.load(f)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Schema fixtures - Tester schemas
|
|
53
|
+
@pytest.fixture(scope="module")
|
|
54
|
+
def telemetry_signal_schema() -> Dict[str, Any]:
|
|
55
|
+
"""Load telemetry_signal.schema.json for validation."""
|
|
56
|
+
schema_path = ATDD_DIR / "tester/schemas/telemetry_signal.schema.json"
|
|
57
|
+
if schema_path.exists():
|
|
58
|
+
with open(schema_path) as f:
|
|
59
|
+
return json.load(f)
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture(scope="module")
|
|
64
|
+
def telemetry_tracking_manifest_schema() -> Dict[str, Any]:
|
|
65
|
+
"""Load telemetry_tracking_manifest.schema.json for validation."""
|
|
66
|
+
schema_path = ATDD_DIR / "tester/schemas/telemetry_tracking_manifest.schema.json"
|
|
67
|
+
if schema_path.exists():
|
|
68
|
+
with open(schema_path) as f:
|
|
69
|
+
return json.load(f)
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Generic schema loader
|
|
74
|
+
@pytest.fixture(scope="module")
|
|
75
|
+
def load_schema():
|
|
76
|
+
"""Factory fixture to load any schema by path."""
|
|
77
|
+
def _loader(agent: str, schema_name: str) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Load a schema from atdd/{agent}/schemas/{schema_name}.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
agent: Agent name (planner, tester, coach, coder)
|
|
83
|
+
schema_name: Schema filename (e.g., "wagon.schema.json")
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Parsed JSON schema
|
|
87
|
+
"""
|
|
88
|
+
schema_path = ATDD_DIR / agent / "schemas" / schema_name
|
|
89
|
+
if not schema_path.exists():
|
|
90
|
+
raise FileNotFoundError(f"Schema not found: {schema_path}")
|
|
91
|
+
with open(schema_path) as f:
|
|
92
|
+
return json.load(f)
|
|
93
|
+
return _loader
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# File discovery fixtures
|
|
97
|
+
@pytest.fixture(scope="module")
|
|
98
|
+
def wagon_manifests() -> List[Tuple[Path, Dict[str, Any]]]:
|
|
99
|
+
"""
|
|
100
|
+
Discover all wagon manifests in plan/.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of (path, manifest_data) tuples
|
|
104
|
+
"""
|
|
105
|
+
manifests = []
|
|
106
|
+
|
|
107
|
+
# Load from _wagons.yaml registry
|
|
108
|
+
wagons_file = PLAN_DIR / "_wagons.yaml"
|
|
109
|
+
if wagons_file.exists():
|
|
110
|
+
with open(wagons_file) as f:
|
|
111
|
+
wagons_data = yaml.safe_load(f)
|
|
112
|
+
for wagon_entry in wagons_data.get("wagons", []):
|
|
113
|
+
if "manifest" in wagon_entry:
|
|
114
|
+
manifest_path = REPO_ROOT / wagon_entry["manifest"]
|
|
115
|
+
if manifest_path.exists():
|
|
116
|
+
with open(manifest_path) as mf:
|
|
117
|
+
manifest_data = yaml.safe_load(mf)
|
|
118
|
+
manifests.append((manifest_path, manifest_data))
|
|
119
|
+
|
|
120
|
+
# Also discover individual wagon manifests (pattern: plan/*/_{wagon}.yaml)
|
|
121
|
+
for wagon_dir in PLAN_DIR.iterdir():
|
|
122
|
+
if wagon_dir.is_dir() and not wagon_dir.name.startswith("_"):
|
|
123
|
+
for manifest_file in wagon_dir.glob("_*.yaml"):
|
|
124
|
+
manifest_path = manifest_file
|
|
125
|
+
if manifest_path not in [m[0] for m in manifests]:
|
|
126
|
+
with open(manifest_path) as f:
|
|
127
|
+
manifest_data = yaml.safe_load(f)
|
|
128
|
+
manifests.append((manifest_path, manifest_data))
|
|
129
|
+
|
|
130
|
+
return manifests
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.fixture(scope="module")
|
|
134
|
+
def trains_registry() -> Dict[str, Any]:
|
|
135
|
+
"""
|
|
136
|
+
Load trains registry from plan/_trains.yaml.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Trains data organized by theme (e.g., {"commons": [...], "scenario": [...]})
|
|
140
|
+
or empty dict with all themes if file doesn't exist
|
|
141
|
+
"""
|
|
142
|
+
trains_file = PLAN_DIR / "_trains.yaml"
|
|
143
|
+
if trains_file.exists():
|
|
144
|
+
with open(trains_file) as f:
|
|
145
|
+
data = yaml.safe_load(f)
|
|
146
|
+
trains_data = data.get("trains", {})
|
|
147
|
+
|
|
148
|
+
# Flatten the nested structure
|
|
149
|
+
# Input: {"0-commons": {"00-commons-nominal": [train1, train2], ...}, ...}
|
|
150
|
+
# Output: {"commons": [train1, train2, ...], ...}
|
|
151
|
+
flattened = {}
|
|
152
|
+
for theme_key, categories in trains_data.items():
|
|
153
|
+
# Extract theme name (e.g., "0-commons" -> "commons")
|
|
154
|
+
theme = theme_key.split("-", 1)[1] if "-" in theme_key else theme_key
|
|
155
|
+
flattened[theme] = []
|
|
156
|
+
|
|
157
|
+
# Flatten all category lists into single theme list
|
|
158
|
+
if isinstance(categories, dict):
|
|
159
|
+
for category_key, trains_list in categories.items():
|
|
160
|
+
if isinstance(trains_list, list):
|
|
161
|
+
flattened[theme].extend(trains_list)
|
|
162
|
+
|
|
163
|
+
return flattened
|
|
164
|
+
|
|
165
|
+
# Return empty theme-grouped structure
|
|
166
|
+
return {
|
|
167
|
+
"commons": [],
|
|
168
|
+
"mechanic": [],
|
|
169
|
+
"scenario": [],
|
|
170
|
+
"match": [],
|
|
171
|
+
"sensory": [],
|
|
172
|
+
"player": [],
|
|
173
|
+
"league": [],
|
|
174
|
+
"audience": [],
|
|
175
|
+
"monetization": [],
|
|
176
|
+
"partnership": []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.fixture(scope="module")
|
|
181
|
+
def wagons_registry() -> Dict[str, Any]:
|
|
182
|
+
"""
|
|
183
|
+
Load wagons registry from plan/_wagons.yaml.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Wagons data or empty dict if file doesn't exist
|
|
187
|
+
"""
|
|
188
|
+
wagons_file = PLAN_DIR / "_wagons.yaml"
|
|
189
|
+
if wagons_file.exists():
|
|
190
|
+
with open(wagons_file) as f:
|
|
191
|
+
return yaml.safe_load(f)
|
|
192
|
+
return {"wagons": []}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# URN resolution fixtures
|
|
196
|
+
@pytest.fixture(scope="module")
|
|
197
|
+
def contract_urns(wagon_manifests: List[Tuple[Path, Dict[str, Any]]]) -> List[str]:
|
|
198
|
+
"""
|
|
199
|
+
Extract all contract URNs from wagon produce items.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of unique contract URNs (e.g., "contract:ux:foundations")
|
|
203
|
+
"""
|
|
204
|
+
urns = set()
|
|
205
|
+
for _, manifest in wagon_manifests:
|
|
206
|
+
for produce_item in manifest.get("produce", []):
|
|
207
|
+
contract = produce_item.get("contract")
|
|
208
|
+
if contract and contract is not None:
|
|
209
|
+
urns.add(contract)
|
|
210
|
+
return sorted(urns)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@pytest.fixture(scope="module")
|
|
214
|
+
def telemetry_urns(wagon_manifests: List[Tuple[Path, Dict[str, Any]]]) -> List[str]:
|
|
215
|
+
"""
|
|
216
|
+
Extract all telemetry URNs from wagon produce items.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of unique telemetry URNs (e.g., "telemetry:ux:foundations")
|
|
220
|
+
"""
|
|
221
|
+
urns = set()
|
|
222
|
+
for _, manifest in wagon_manifests:
|
|
223
|
+
for produce_item in manifest.get("produce", []):
|
|
224
|
+
telemetry = produce_item.get("telemetry")
|
|
225
|
+
if telemetry and telemetry is not None:
|
|
226
|
+
# Handle both string and list types
|
|
227
|
+
if isinstance(telemetry, list):
|
|
228
|
+
urns.update(telemetry)
|
|
229
|
+
else:
|
|
230
|
+
urns.add(telemetry)
|
|
231
|
+
return sorted(urns)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.fixture(scope="module")
|
|
235
|
+
def typescript_test_files() -> List[Path]:
|
|
236
|
+
"""
|
|
237
|
+
Discover all TypeScript test files in supabase/ and e2e/ directories.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of Path objects pointing to *.test.ts files
|
|
241
|
+
"""
|
|
242
|
+
ts_tests = []
|
|
243
|
+
|
|
244
|
+
# Search in supabase/functions/*/test/
|
|
245
|
+
supabase_dir = REPO_ROOT / "supabase"
|
|
246
|
+
if supabase_dir.exists():
|
|
247
|
+
ts_tests.extend(supabase_dir.rglob("*.test.ts"))
|
|
248
|
+
|
|
249
|
+
# Search in e2e/
|
|
250
|
+
e2e_dir = REPO_ROOT / "e2e"
|
|
251
|
+
if e2e_dir.exists():
|
|
252
|
+
ts_tests.extend(e2e_dir.rglob("*.test.ts"))
|
|
253
|
+
|
|
254
|
+
return sorted(ts_tests)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.fixture(scope="module")
|
|
258
|
+
def web_typescript_test_files() -> List[Path]:
|
|
259
|
+
"""
|
|
260
|
+
Discover all Preact TypeScript test files in web/tests/.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of Path objects pointing to *.test.ts and *.test.tsx files
|
|
264
|
+
"""
|
|
265
|
+
web_tests_dir = REPO_ROOT / "web" / "tests"
|
|
266
|
+
if not web_tests_dir.exists():
|
|
267
|
+
return []
|
|
268
|
+
|
|
269
|
+
ts_tests = []
|
|
270
|
+
ts_tests.extend(web_tests_dir.rglob("*.test.ts"))
|
|
271
|
+
ts_tests.extend(web_tests_dir.rglob("*.test.tsx"))
|
|
272
|
+
return sorted(ts_tests)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Helper functions
|
|
276
|
+
def parse_urn(urn: str) -> Tuple[str, str, str]:
|
|
277
|
+
"""
|
|
278
|
+
Parse URN into components.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
urn: URN string like "contract:ux:foundations"
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Tuple of (type, domain, resource)
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
>>> parse_urn("contract:ux:foundations")
|
|
288
|
+
("contract", "ux", "foundations")
|
|
289
|
+
"""
|
|
290
|
+
parts = urn.split(":")
|
|
291
|
+
if len(parts) != 3:
|
|
292
|
+
raise ValueError(f"Invalid URN format: {urn} (expected type:domain:resource)")
|
|
293
|
+
return tuple(parts)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_wagon_slug(manifest: Dict[str, Any]) -> str:
|
|
297
|
+
"""Extract wagon slug from manifest."""
|
|
298
|
+
return manifest.get("wagon", "")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_produce_names(manifest: Dict[str, Any]) -> List[str]:
|
|
302
|
+
"""Extract produce artifact names from manifest."""
|
|
303
|
+
return [item.get("name", "") for item in manifest.get("produce", [])]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_consume_names(manifest: Dict[str, Any]) -> List[str]:
|
|
307
|
+
"""Extract consume artifact names from manifest."""
|
|
308
|
+
return [item.get("name", "") for item in manifest.get("consume", [])]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# HTML Report Customization
|
|
312
|
+
def pytest_html_report_title(report):
|
|
313
|
+
"""Customize HTML report title."""
|
|
314
|
+
report.title = "Platform Validation Test Report"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def pytest_configure(config):
|
|
318
|
+
"""Add custom metadata to HTML report."""
|
|
319
|
+
config._metadata = {
|
|
320
|
+
"Project": "Wagons Platform",
|
|
321
|
+
"Test Suite": "Platform Validation",
|
|
322
|
+
"Environment": "Development",
|
|
323
|
+
"Python": "3.11",
|
|
324
|
+
"Pytest": "8.4.2",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def pytest_html_results_table_header(cells):
|
|
329
|
+
"""Customize HTML report table headers."""
|
|
330
|
+
cells.insert(2, '<th>Category</th>')
|
|
331
|
+
cells.insert(1, '<th class="sortable time" data-column-type="time">Duration</th>')
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def pytest_html_results_table_row(report, cells):
|
|
335
|
+
"""Customize HTML report table rows."""
|
|
336
|
+
# Add category based on test module
|
|
337
|
+
category = "Unknown"
|
|
338
|
+
if hasattr(report, 'nodeid'):
|
|
339
|
+
if 'wagons' in report.nodeid:
|
|
340
|
+
category = '📋 Schema'
|
|
341
|
+
elif 'cross_refs' in report.nodeid:
|
|
342
|
+
category = '🔗 References'
|
|
343
|
+
elif 'urn_resolution' in report.nodeid:
|
|
344
|
+
category = '🗺️ URN Resolution'
|
|
345
|
+
elif 'uniqueness' in report.nodeid:
|
|
346
|
+
category = '🎯 Uniqueness'
|
|
347
|
+
elif 'contracts_structure' in report.nodeid:
|
|
348
|
+
category = '📄 Contracts'
|
|
349
|
+
elif 'telemetry_structure' in report.nodeid:
|
|
350
|
+
category = '📊 Telemetry'
|
|
351
|
+
|
|
352
|
+
cells.insert(2, f'<td>{category}</td>')
|
|
353
|
+
cells.insert(1, f'<td class="col-duration">{getattr(report, "duration", 0):.2f}s</td>')
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def pytest_html_results_summary(prefix, summary, postfix):
|
|
357
|
+
"""Add custom summary to HTML report."""
|
|
358
|
+
prefix.extend([
|
|
359
|
+
'<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); '
|
|
360
|
+
'padding: 20px; border-radius: 8px; color: white; margin: 20px 0;">'
|
|
361
|
+
'<h2 style="margin: 0 0 10px 0;">🚀 Platform Validation Suite</h2>'
|
|
362
|
+
'<p style="margin: 0; opacity: 0.9;">E2E validation of repository data '
|
|
363
|
+
'against platform schemas and conventions.</p>'
|
|
364
|
+
'</div>'
|
|
365
|
+
])
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-COACH-UTILS-0290: Add features section and simplify WMBT counts in wagon registry
|
|
3
|
+
|
|
4
|
+
Tests enrichment of _wagons.yaml with features list and simplified WMBT totals.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
import yaml
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from tempfile import TemporaryDirectory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_add_features_and_simplify_wmbt(tmp_path):
|
|
13
|
+
"""
|
|
14
|
+
SPEC-COACH-UTILS-0290: Add features section and simplify WMBT counts in wagon registry
|
|
15
|
+
|
|
16
|
+
Given: _wagons.yaml exists with wagon entries
|
|
17
|
+
Each wagon entry may have full wmbt details or empty wmbt: {}
|
|
18
|
+
Individual wagon manifests contain features: array with URN objects
|
|
19
|
+
Individual wagon manifests contain wmbt.total count
|
|
20
|
+
When: Enriching _wagons.yaml with features and WMBT counts
|
|
21
|
+
Then: Each wagon entry in _wagons.yaml has a features: section
|
|
22
|
+
features: section contains array of feature URNs from wagon manifest
|
|
23
|
+
Wagons without features in manifest get empty features: []
|
|
24
|
+
wmbt: {} or wmbt detailed entries are replaced with total: N
|
|
25
|
+
total value comes from wagon manifest wmbt.total field
|
|
26
|
+
All other wagon fields remain unchanged
|
|
27
|
+
YAML structure and formatting preserved
|
|
28
|
+
"""
|
|
29
|
+
# Setup test directories
|
|
30
|
+
plan_dir = tmp_path / "plan"
|
|
31
|
+
plan_dir.mkdir()
|
|
32
|
+
|
|
33
|
+
# Create sample wagon manifests with features and wmbt.total
|
|
34
|
+
wagon_a_dir = plan_dir / "wagon_a"
|
|
35
|
+
wagon_a_dir.mkdir()
|
|
36
|
+
wagon_a_manifest = wagon_a_dir / "_wagon_a.yaml"
|
|
37
|
+
wagon_a_data = {
|
|
38
|
+
"wagon": "wagon-a",
|
|
39
|
+
"description": "Test wagon A",
|
|
40
|
+
"theme": "commons",
|
|
41
|
+
"subject": "system:test",
|
|
42
|
+
"context": "test",
|
|
43
|
+
"action": "test action",
|
|
44
|
+
"goal": "test goal",
|
|
45
|
+
"outcome": "test outcome",
|
|
46
|
+
"produce": [{"name": "test:artifact", "contract": None, "telemetry": None}],
|
|
47
|
+
"consume": [],
|
|
48
|
+
"features": [
|
|
49
|
+
{"urn": "feature:wagon-a.feature-one"},
|
|
50
|
+
{"urn": "feature:wagon-a.feature-two"}
|
|
51
|
+
],
|
|
52
|
+
"wmbt": {
|
|
53
|
+
"L001": "Test WMBT 1",
|
|
54
|
+
"P001": "Test WMBT 2",
|
|
55
|
+
"total": 2
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
with open(wagon_a_manifest, 'w') as f:
|
|
59
|
+
yaml.dump(wagon_a_data, f, default_flow_style=False, sort_keys=False)
|
|
60
|
+
|
|
61
|
+
# Create wagon B with no features
|
|
62
|
+
wagon_b_dir = plan_dir / "wagon_b"
|
|
63
|
+
wagon_b_dir.mkdir()
|
|
64
|
+
wagon_b_manifest = wagon_b_dir / "_wagon_b.yaml"
|
|
65
|
+
wagon_b_data = {
|
|
66
|
+
"wagon": "wagon-b",
|
|
67
|
+
"description": "Test wagon B",
|
|
68
|
+
"theme": "commons",
|
|
69
|
+
"subject": "system:test",
|
|
70
|
+
"context": "test",
|
|
71
|
+
"action": "test action",
|
|
72
|
+
"goal": "test goal",
|
|
73
|
+
"outcome": "test outcome",
|
|
74
|
+
"produce": [{"name": "test:artifact", "contract": None, "telemetry": None}],
|
|
75
|
+
"consume": [],
|
|
76
|
+
"wmbt": {
|
|
77
|
+
"total": 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
with open(wagon_b_manifest, 'w') as f:
|
|
81
|
+
yaml.dump(wagon_b_data, f, default_flow_style=False, sort_keys=False)
|
|
82
|
+
|
|
83
|
+
# Create _wagons.yaml with wagons that need enrichment
|
|
84
|
+
wagons_file = plan_dir / "_wagons.yaml"
|
|
85
|
+
wagons_data = {
|
|
86
|
+
"wagons": [
|
|
87
|
+
{
|
|
88
|
+
"wagon": "wagon-a",
|
|
89
|
+
"description": "Test wagon A",
|
|
90
|
+
"theme": "commons",
|
|
91
|
+
"subject": "system:test",
|
|
92
|
+
"context": "test",
|
|
93
|
+
"action": "test action",
|
|
94
|
+
"goal": "test goal",
|
|
95
|
+
"outcome": "test outcome",
|
|
96
|
+
"produce": [{"name": "test:artifact", "to": "external"}],
|
|
97
|
+
"consume": [],
|
|
98
|
+
"wmbt": {
|
|
99
|
+
"L001": "Test WMBT 1",
|
|
100
|
+
"P001": "Test WMBT 2"
|
|
101
|
+
},
|
|
102
|
+
"total": 2,
|
|
103
|
+
"manifest": "plan/wagon_a/_wagon_a.yaml",
|
|
104
|
+
"path": "plan/wagon_a/"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"wagon": "wagon-b",
|
|
108
|
+
"description": "Test wagon B",
|
|
109
|
+
"theme": "commons",
|
|
110
|
+
"subject": "system:test",
|
|
111
|
+
"context": "test",
|
|
112
|
+
"action": "test action",
|
|
113
|
+
"goal": "test goal",
|
|
114
|
+
"outcome": "test outcome",
|
|
115
|
+
"produce": [{"name": "test:artifact", "to": "external"}],
|
|
116
|
+
"consume": [],
|
|
117
|
+
"wmbt": {},
|
|
118
|
+
"total": 0,
|
|
119
|
+
"manifest": "plan/wagon_b/_wagon_b.yaml",
|
|
120
|
+
"path": "plan/wagon_b/"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
with open(wagons_file, 'w') as f:
|
|
125
|
+
yaml.dump(wagons_data, f, default_flow_style=False, sort_keys=False)
|
|
126
|
+
|
|
127
|
+
# Import and call the enrichment via RegistryBuilder
|
|
128
|
+
from atdd.coach.commands.registry import RegistryBuilder
|
|
129
|
+
|
|
130
|
+
# Create builder and enrich registry
|
|
131
|
+
builder = RegistryBuilder(tmp_path)
|
|
132
|
+
builder.enrich_wagon_registry()
|
|
133
|
+
|
|
134
|
+
# Load the enriched _wagons.yaml
|
|
135
|
+
with open(wagons_file, 'r') as f:
|
|
136
|
+
enriched_data = yaml.safe_load(f)
|
|
137
|
+
|
|
138
|
+
# Assertions
|
|
139
|
+
wagons = enriched_data["wagons"]
|
|
140
|
+
|
|
141
|
+
# Check wagon-a
|
|
142
|
+
wagon_a = next(w for w in wagons if w["wagon"] == "wagon-a")
|
|
143
|
+
assert "features" in wagon_a, "wagon-a should have features section"
|
|
144
|
+
assert wagon_a["features"] == [
|
|
145
|
+
{"urn": "feature:wagon-a.feature-one"},
|
|
146
|
+
{"urn": "feature:wagon-a.feature-two"}
|
|
147
|
+
], "wagon-a features should match manifest"
|
|
148
|
+
assert "wmbt" in wagon_a, "wagon-a should have wmbt object"
|
|
149
|
+
assert wagon_a["wmbt"]["total"] == 2, "wagon-a wmbt.total should be 2"
|
|
150
|
+
assert wagon_a["wmbt"]["coverage"] == 0, "wagon-a wmbt.coverage should be 0"
|
|
151
|
+
assert "L001" not in wagon_a["wmbt"], "wagon-a wmbt should not have detailed entries"
|
|
152
|
+
assert "total" not in wagon_a or wagon_a.get("total") is None, "wagon-a should not have root-level total field"
|
|
153
|
+
|
|
154
|
+
# Check wagon-b
|
|
155
|
+
wagon_b = next(w for w in wagons if w["wagon"] == "wagon-b")
|
|
156
|
+
assert "features" in wagon_b, "wagon-b should have features section"
|
|
157
|
+
assert wagon_b["features"] == [], "wagon-b should have empty features list"
|
|
158
|
+
assert "wmbt" in wagon_b, "wagon-b should have wmbt object"
|
|
159
|
+
assert wagon_b["wmbt"]["total"] == 0, "wagon-b wmbt.total should be 0"
|
|
160
|
+
assert wagon_b["wmbt"]["coverage"] == 0, "wagon-b wmbt.coverage should be 0"
|
|
161
|
+
assert "total" not in wagon_b or wagon_b.get("total") is None, "wagon-b should not have root-level total field"
|
|
162
|
+
|
|
163
|
+
# Check other fields remain unchanged
|
|
164
|
+
assert wagon_a["description"] == "Test wagon A"
|
|
165
|
+
assert wagon_a["manifest"] == "plan/wagon_a/_wagon_a.yaml"
|
|
166
|
+
assert wagon_b["description"] == "Test wagon B"
|
|
167
|
+
assert wagon_b["manifest"] == "plan/wagon_b/_wagon_b.yaml"
|