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,634 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform tests: Telemetry directory structure validation.
|
|
3
|
+
|
|
4
|
+
Validates that telemetry/ follows the signal naming convention.
|
|
5
|
+
Tests ensure telemetry signal files match pattern: {signal-type}.{plane}[.{measure}].json
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import json
|
|
11
|
+
from jsonschema import validate, ValidationError
|
|
12
|
+
|
|
13
|
+
# Path constants
|
|
14
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
15
|
+
TELEMETRY_DIR = REPO_ROOT / "telemetry"
|
|
16
|
+
CONTRACTS_DIR = REPO_ROOT / "contracts"
|
|
17
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
18
|
+
META_SCHEMA_PATH = REPO_ROOT / "atdd" / "tester" / "schemas" / "telemetry.schema.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def telemetry_meta_schema():
|
|
23
|
+
"""Load telemetry meta-schema."""
|
|
24
|
+
if not META_SCHEMA_PATH.exists():
|
|
25
|
+
pytest.skip(f"Meta-schema not found: {META_SCHEMA_PATH}")
|
|
26
|
+
|
|
27
|
+
with open(META_SCHEMA_PATH) as f:
|
|
28
|
+
return json.load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_all_telemetry_signals():
|
|
32
|
+
"""Find telemetry signal files excluding tests/ directories."""
|
|
33
|
+
if not TELEMETRY_DIR.exists():
|
|
34
|
+
return []
|
|
35
|
+
return [
|
|
36
|
+
path for path in TELEMETRY_DIR.rglob("*.json")
|
|
37
|
+
if "tests" not in path.parts
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_plan_acceptance_urns():
|
|
42
|
+
"""Collect acceptance URNs from plan/ YAML files."""
|
|
43
|
+
if not PLAN_DIR.exists():
|
|
44
|
+
return set()
|
|
45
|
+
|
|
46
|
+
urns = set()
|
|
47
|
+
urn_pattern = re.compile(r"\\burn:\\s*(acc:[^\\s]+)")
|
|
48
|
+
|
|
49
|
+
for plan_path in PLAN_DIR.rglob("*.yaml"):
|
|
50
|
+
try:
|
|
51
|
+
content = plan_path.read_text()
|
|
52
|
+
except OSError:
|
|
53
|
+
continue
|
|
54
|
+
for match in urn_pattern.findall(content):
|
|
55
|
+
urns.add(match.strip())
|
|
56
|
+
|
|
57
|
+
return urns
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def collect_contract_urns():
|
|
61
|
+
"""Collect contract URNs from contract schemas."""
|
|
62
|
+
if not CONTRACTS_DIR.exists():
|
|
63
|
+
return set()
|
|
64
|
+
|
|
65
|
+
urns = set()
|
|
66
|
+
for contract_path in CONTRACTS_DIR.rglob("*.schema.json"):
|
|
67
|
+
try:
|
|
68
|
+
with open(contract_path) as f:
|
|
69
|
+
contract = json.load(f)
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
contract_id = contract.get("$id")
|
|
74
|
+
if contract_id:
|
|
75
|
+
urns.add(f"contract:{contract_id}")
|
|
76
|
+
|
|
77
|
+
return urns
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_placeholder_signal(signal_path, signal):
|
|
81
|
+
"""Return True if signal is a placeholder signal."""
|
|
82
|
+
if "placeholder" in signal_path.parts:
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
for key in ("description", "note"):
|
|
86
|
+
value = signal.get(key)
|
|
87
|
+
if isinstance(value, str) and "placeholder" in value.lower():
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.platform
|
|
94
|
+
def test_telemetry_directory_exists():
|
|
95
|
+
"""
|
|
96
|
+
SPEC-PLATFORM-TELEMETRY-0001: telemetry/ directory exists
|
|
97
|
+
|
|
98
|
+
Given: Repository root
|
|
99
|
+
When: Checking for telemetry/ directory
|
|
100
|
+
Then: telemetry/ directory exists
|
|
101
|
+
"""
|
|
102
|
+
assert TELEMETRY_DIR.exists(), \
|
|
103
|
+
f"telemetry/ directory does not exist at {TELEMETRY_DIR}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.platform
|
|
107
|
+
def test_telemetry_follows_theme_domain_pattern():
|
|
108
|
+
"""
|
|
109
|
+
SPEC-PLATFORM-TELEMETRY-0002: telemetry/ follows theme/domain pattern with aspect as filename prefix
|
|
110
|
+
|
|
111
|
+
Given: telemetry/ directory structure
|
|
112
|
+
When: Checking directory hierarchy and file naming
|
|
113
|
+
Then: Structure follows telemetry/{theme}/{domain}/ pattern
|
|
114
|
+
Aspect is filename prefix: {aspect}.{type}.{plane}[.{measure}].json
|
|
115
|
+
Mirrors contracts/{theme}/{domain}/{aspect}.schema.json structure
|
|
116
|
+
"""
|
|
117
|
+
if not TELEMETRY_DIR.exists():
|
|
118
|
+
pytest.skip(f"telemetry/ directory does not exist")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
name_pattern = re.compile(r"^[a-z][a-z0-9\-]*$")
|
|
122
|
+
|
|
123
|
+
for theme_dir in TELEMETRY_DIR.iterdir():
|
|
124
|
+
if not theme_dir.is_dir():
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
# Skip hidden directories and special files
|
|
128
|
+
if theme_dir.name.startswith(".") or theme_dir.name == "__pycache__" or theme_dir.name.startswith("_"):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Verify theme name follows pattern
|
|
132
|
+
assert name_pattern.match(theme_dir.name), \
|
|
133
|
+
f"Telemetry theme '{theme_dir.name}' doesn't match pattern (lowercase, hyphens only)"
|
|
134
|
+
|
|
135
|
+
# Check domain directories
|
|
136
|
+
for domain_dir in theme_dir.iterdir():
|
|
137
|
+
if not domain_dir.is_dir():
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if domain_dir.name.startswith(".") or domain_dir.name == "__pycache__" or domain_dir.name == "tests":
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Verify domain name follows pattern
|
|
144
|
+
assert name_pattern.match(domain_dir.name), \
|
|
145
|
+
f"Telemetry domain '{domain_dir.name}' in theme '{theme_dir.name}' " \
|
|
146
|
+
f"doesn't match pattern (lowercase, hyphens only)"
|
|
147
|
+
|
|
148
|
+
# Verify signal files have aspect prefix (no aspect subdirectories)
|
|
149
|
+
signal_files = list(domain_dir.glob("*.json"))
|
|
150
|
+
if signal_files:
|
|
151
|
+
# Check that files follow aspect.type.plane[.measure].json pattern
|
|
152
|
+
for signal_file in signal_files:
|
|
153
|
+
parts = signal_file.name.split('.')
|
|
154
|
+
assert len(parts) >= 4, \
|
|
155
|
+
f"Signal file '{signal_file.name}' doesn't follow aspect.type.plane[.measure].json pattern"
|
|
156
|
+
|
|
157
|
+
aspect, type_part = parts[0], parts[1]
|
|
158
|
+
assert name_pattern.match(aspect), \
|
|
159
|
+
f"Aspect '{aspect}' in '{signal_file.name}' doesn't match pattern"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.platform
|
|
163
|
+
def test_telemetry_signal_files_follow_naming_convention():
|
|
164
|
+
"""
|
|
165
|
+
SPEC-PLATFORM-TELEMETRY-0003: Signal files follow naming convention
|
|
166
|
+
|
|
167
|
+
Given: Telemetry signal files in telemetry/{domain}/{resource}/
|
|
168
|
+
When: Checking file naming pattern
|
|
169
|
+
Then: Files match pattern: {signal-type}.{plane}[.{measure}].json
|
|
170
|
+
signal-type: metric (with measure) or event (no measure)
|
|
171
|
+
plane: ui, ux, be, nw, db, st, tm, sc, au, fn, if
|
|
172
|
+
measure: optional for metrics (e.g., count, duration, bytes)
|
|
173
|
+
"""
|
|
174
|
+
if not TELEMETRY_DIR.exists():
|
|
175
|
+
pytest.skip(f"telemetry/ directory does not exist")
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Pattern: metric.{plane}.{measure}.json OR event.{plane}.json
|
|
179
|
+
# Planes: ui, ux, be, nw, db, st, tm, sc, au, fn, if
|
|
180
|
+
metric_pattern = re.compile(
|
|
181
|
+
r"^metric\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.[a-z][a-z0-9\-]*\.json$"
|
|
182
|
+
)
|
|
183
|
+
event_pattern = re.compile(
|
|
184
|
+
r"^event\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.json$"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
invalid_files = []
|
|
188
|
+
|
|
189
|
+
for theme_dir in TELEMETRY_DIR.iterdir():
|
|
190
|
+
if not theme_dir.is_dir() or theme_dir.name.startswith((".", "_")):
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
for domain_dir in theme_dir.iterdir():
|
|
194
|
+
if not domain_dir.is_dir() or domain_dir.name.startswith((".", "_")) or domain_dir.name == "tests":
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Check all JSON files in domain directory (not subdirectories)
|
|
198
|
+
for signal_file in domain_dir.glob("*.json"):
|
|
199
|
+
filename = signal_file.name
|
|
200
|
+
|
|
201
|
+
# Skip files in tests directory
|
|
202
|
+
if "tests" in signal_file.parts:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Files should follow: {aspect}.{type}.{plane}[.{measure}].json
|
|
206
|
+
# So we need to check parts after the aspect prefix
|
|
207
|
+
parts = filename.split('.')
|
|
208
|
+
if len(parts) < 4: # aspect.type.plane.json minimum
|
|
209
|
+
invalid_files.append(str(signal_file.relative_to(REPO_ROOT)))
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Check type.plane[.measure] portion (after aspect prefix)
|
|
213
|
+
type_plane_portion = '.'.join(parts[1:]) # Skip aspect
|
|
214
|
+
|
|
215
|
+
# Check if matches metric or event pattern
|
|
216
|
+
if not (metric_pattern.match(type_plane_portion) or event_pattern.match(type_plane_portion)):
|
|
217
|
+
invalid_files.append(
|
|
218
|
+
str(signal_file.relative_to(REPO_ROOT))
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if invalid_files:
|
|
222
|
+
pytest.fail(
|
|
223
|
+
f"Found {len(invalid_files)} telemetry files not following naming convention:\n" +
|
|
224
|
+
"\n".join(f" {f}" for f in invalid_files[:10]) +
|
|
225
|
+
(f"\n ... and {len(invalid_files) - 10} more" if len(invalid_files) > 10 else "") +
|
|
226
|
+
"\n\nExpected patterns:\n" +
|
|
227
|
+
" - metric.{plane}.{measure}.json (e.g., metric.ui.duration.json)\n" +
|
|
228
|
+
" - event.{plane}.json (e.g., event.ux.json)\n" +
|
|
229
|
+
" Planes: ui, ux, be, nw, db, st, tm, sc, au, fn, if"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pytest.mark.platform
|
|
234
|
+
def test_telemetry_directories_contain_signal_files():
|
|
235
|
+
"""
|
|
236
|
+
SPEC-PLATFORM-TELEMETRY-0004: Telemetry directories contain signal files
|
|
237
|
+
|
|
238
|
+
Given: telemetry/{theme}/{domain}/{aspect}/ subdirectories (mirroring contracts)
|
|
239
|
+
When: Checking directory contents
|
|
240
|
+
Then: Each aspect subdirectory contains *.json signal files
|
|
241
|
+
Directories are not empty
|
|
242
|
+
"""
|
|
243
|
+
if not TELEMETRY_DIR.exists():
|
|
244
|
+
pytest.skip(f"telemetry/ directory does not exist")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
empty_dirs = []
|
|
248
|
+
|
|
249
|
+
for theme_dir in TELEMETRY_DIR.iterdir():
|
|
250
|
+
if not theme_dir.is_dir() or theme_dir.name.startswith((".", "_")):
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
for domain_dir in theme_dir.iterdir():
|
|
254
|
+
if not domain_dir.is_dir() or domain_dir.name.startswith((".", "_")) or domain_dir.name == "tests":
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Check subdirectories (aspect directories) for signal files
|
|
258
|
+
# telemetry/commons/ux/ should contain subdirectories like foundations/, primitives/, etc.
|
|
259
|
+
for aspect_dir in domain_dir.iterdir():
|
|
260
|
+
if not aspect_dir.is_dir() or aspect_dir.name.startswith((".", "_")) or aspect_dir.name in ("tests", "atdd"):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Check if aspect directory has JSON signal files (excluding tests/)
|
|
264
|
+
json_files = [f for f in aspect_dir.glob("*.json") if "tests" not in f.parts]
|
|
265
|
+
|
|
266
|
+
if not json_files:
|
|
267
|
+
empty_dirs.append(str(aspect_dir.relative_to(REPO_ROOT)))
|
|
268
|
+
|
|
269
|
+
if empty_dirs:
|
|
270
|
+
pytest.fail(
|
|
271
|
+
f"Found {len(empty_dirs)} telemetry directories without signal files:\n" +
|
|
272
|
+
"\n".join(f" {d}" for d in empty_dirs[:10]) +
|
|
273
|
+
(f"\n ... and {len(empty_dirs) - 10} more" if len(empty_dirs) > 10 else "")
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@pytest.mark.platform
|
|
278
|
+
def test_metric_signals_have_measure_suffix():
|
|
279
|
+
"""
|
|
280
|
+
SPEC-PLATFORM-TELEMETRY-0005: Metric signals include measure suffix
|
|
281
|
+
|
|
282
|
+
Given: Telemetry signal files with type 'metric'
|
|
283
|
+
When: Checking file naming
|
|
284
|
+
Then: Metric files match pattern metric.{plane}.{measure}.json
|
|
285
|
+
Measure describes what is measured (e.g., count, duration, bytes)
|
|
286
|
+
"""
|
|
287
|
+
if not TELEMETRY_DIR.exists():
|
|
288
|
+
pytest.skip(f"telemetry/ directory does not exist")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
# Find all metric.* files
|
|
292
|
+
metric_files = list(TELEMETRY_DIR.rglob("metric.*.json"))
|
|
293
|
+
|
|
294
|
+
# Pattern with measure: metric.{plane}.{measure}.json
|
|
295
|
+
metric_with_measure = re.compile(
|
|
296
|
+
r"^metric\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.[a-z][a-z0-9\-]+\.json$"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
invalid_metrics = []
|
|
300
|
+
|
|
301
|
+
for metric_file in metric_files:
|
|
302
|
+
filename = metric_file.name
|
|
303
|
+
|
|
304
|
+
# Check if it has the measure component
|
|
305
|
+
if not metric_with_measure.match(filename):
|
|
306
|
+
invalid_metrics.append(str(metric_file.relative_to(REPO_ROOT)))
|
|
307
|
+
|
|
308
|
+
if invalid_metrics:
|
|
309
|
+
pytest.fail(
|
|
310
|
+
f"Found {len(invalid_metrics)} metric files without measure suffix:\n" +
|
|
311
|
+
"\n".join(f" {f}" for f in invalid_metrics[:10]) +
|
|
312
|
+
(f"\n ... and {len(invalid_metrics) - 10} more" if len(invalid_metrics) > 10 else "") +
|
|
313
|
+
"\n\nMetric files must include measure: metric.{plane}.{measure}.json"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@pytest.mark.platform
|
|
318
|
+
def test_event_signals_have_no_measure_suffix():
|
|
319
|
+
"""
|
|
320
|
+
SPEC-PLATFORM-TELEMETRY-0006: Event signals have no measure suffix
|
|
321
|
+
|
|
322
|
+
Given: Telemetry signal files with type 'event'
|
|
323
|
+
When: Checking file naming
|
|
324
|
+
Then: Event files match pattern event.{plane}.json (no measure)
|
|
325
|
+
"""
|
|
326
|
+
if not TELEMETRY_DIR.exists():
|
|
327
|
+
pytest.skip(f"telemetry/ directory does not exist")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# Find all event.* files
|
|
331
|
+
event_files = list(TELEMETRY_DIR.rglob("event.*.json"))
|
|
332
|
+
|
|
333
|
+
# Pattern without measure: event.{plane}.json
|
|
334
|
+
event_pattern = re.compile(
|
|
335
|
+
r"^event\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.json$"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
invalid_events = []
|
|
339
|
+
|
|
340
|
+
for event_file in event_files:
|
|
341
|
+
filename = event_file.name
|
|
342
|
+
|
|
343
|
+
# Check if it matches simple event pattern
|
|
344
|
+
if not event_pattern.match(filename):
|
|
345
|
+
invalid_events.append(str(event_file.relative_to(REPO_ROOT)))
|
|
346
|
+
|
|
347
|
+
if invalid_events:
|
|
348
|
+
pytest.fail(
|
|
349
|
+
f"Found {len(invalid_events)} event files with unexpected format:\n" +
|
|
350
|
+
"\n".join(f" {f}" for f in invalid_events[:10]) +
|
|
351
|
+
(f"\n ... and {len(invalid_events) - 10} more" if len(invalid_events) > 10 else "") +
|
|
352
|
+
"\n\nEvent files should match: event.{plane}.json (no measure suffix)"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@pytest.mark.platform
|
|
357
|
+
def test_telemetry_signals_validate_against_meta_schema(telemetry_meta_schema):
|
|
358
|
+
"""
|
|
359
|
+
SPEC-PLATFORM-TELEMETRY-0009: Telemetry signals validate against meta-schema
|
|
360
|
+
|
|
361
|
+
Given: Telemetry signal files
|
|
362
|
+
When: Validating against atdd/tester/schemas/telemetry.schema.json
|
|
363
|
+
Then: All telemetry signals pass meta-schema validation
|
|
364
|
+
"""
|
|
365
|
+
signal_files = find_all_telemetry_signals()
|
|
366
|
+
|
|
367
|
+
if not signal_files:
|
|
368
|
+
pytest.skip("No telemetry signal files found")
|
|
369
|
+
|
|
370
|
+
validation_errors = []
|
|
371
|
+
|
|
372
|
+
for signal_path in signal_files:
|
|
373
|
+
try:
|
|
374
|
+
with open(signal_path) as f:
|
|
375
|
+
signal = json.load(f)
|
|
376
|
+
validate(instance=signal, schema=telemetry_meta_schema)
|
|
377
|
+
except ValidationError as exc:
|
|
378
|
+
validation_errors.append(
|
|
379
|
+
f"{signal_path.relative_to(REPO_ROOT)}: {exc.message}"
|
|
380
|
+
)
|
|
381
|
+
except json.JSONDecodeError as exc:
|
|
382
|
+
validation_errors.append(
|
|
383
|
+
f"{signal_path.relative_to(REPO_ROOT)}: Invalid JSON - {exc}"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if validation_errors:
|
|
387
|
+
pytest.fail(
|
|
388
|
+
f"Found {len(validation_errors)} telemetry validation errors:\n" +
|
|
389
|
+
"\n".join(f" {err}" for err in validation_errors[:10]) +
|
|
390
|
+
(f"\n ... and {len(validation_errors) - 10} more" if len(validation_errors) > 10 else "")
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@pytest.mark.platform
|
|
395
|
+
def test_telemetry_versions_follow_semver():
|
|
396
|
+
"""
|
|
397
|
+
SPEC-PLATFORM-TELEMETRY-0010: Telemetry versions follow semantic versioning
|
|
398
|
+
|
|
399
|
+
Given: Telemetry signal version fields
|
|
400
|
+
When: Checking version format
|
|
401
|
+
Then: Versions match pattern: MAJOR.MINOR.PATCH
|
|
402
|
+
"""
|
|
403
|
+
signal_files = find_all_telemetry_signals()
|
|
404
|
+
|
|
405
|
+
if not signal_files:
|
|
406
|
+
pytest.skip("No telemetry signal files found")
|
|
407
|
+
|
|
408
|
+
version_pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
409
|
+
invalid_versions = []
|
|
410
|
+
|
|
411
|
+
for signal_path in signal_files:
|
|
412
|
+
try:
|
|
413
|
+
with open(signal_path) as f:
|
|
414
|
+
signal = json.load(f)
|
|
415
|
+
except json.JSONDecodeError:
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
version = signal.get("version")
|
|
419
|
+
if not version or not version_pattern.match(version):
|
|
420
|
+
invalid_versions.append(
|
|
421
|
+
f"{signal_path.relative_to(REPO_ROOT)}: version '{version}'"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if invalid_versions:
|
|
425
|
+
pytest.fail(
|
|
426
|
+
f"Found {len(invalid_versions)} telemetry signals with invalid versions:\n" +
|
|
427
|
+
"\n".join(f" {err}" for err in invalid_versions[:10]) +
|
|
428
|
+
(f"\n ... and {len(invalid_versions) - 10} more" if len(invalid_versions) > 10 else "")
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@pytest.mark.platform
|
|
433
|
+
def test_telemetry_contract_references_exist():
|
|
434
|
+
"""
|
|
435
|
+
SPEC-PLATFORM-TELEMETRY-0011: Telemetry contract references point to existing contracts
|
|
436
|
+
|
|
437
|
+
Given: Telemetry signal files with artifact_ref fields
|
|
438
|
+
When: Resolving artifact_ref references
|
|
439
|
+
Then: All referenced contracts exist
|
|
440
|
+
"""
|
|
441
|
+
signal_files = find_all_telemetry_signals()
|
|
442
|
+
|
|
443
|
+
if not signal_files:
|
|
444
|
+
pytest.skip("No telemetry signal files found")
|
|
445
|
+
|
|
446
|
+
contract_urns = collect_contract_urns()
|
|
447
|
+
missing_refs = []
|
|
448
|
+
|
|
449
|
+
for signal_path in signal_files:
|
|
450
|
+
try:
|
|
451
|
+
with open(signal_path) as f:
|
|
452
|
+
signal = json.load(f)
|
|
453
|
+
except json.JSONDecodeError:
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
artifact_ref = signal.get("artifact_ref")
|
|
457
|
+
if artifact_ref and artifact_ref not in contract_urns:
|
|
458
|
+
missing_refs.append(
|
|
459
|
+
f"{signal_path.relative_to(REPO_ROOT)}: artifact_ref '{artifact_ref}' not found"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if missing_refs:
|
|
463
|
+
pytest.fail(
|
|
464
|
+
f"Found {len(missing_refs)} telemetry signals with broken contract references:\n" +
|
|
465
|
+
"\n".join(f" {err}" for err in missing_refs[:10]) +
|
|
466
|
+
(f"\n ... and {len(missing_refs) - 10} more" if len(missing_refs) > 10 else "")
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@pytest.mark.platform
|
|
471
|
+
def test_telemetry_acceptance_references_exist():
|
|
472
|
+
"""
|
|
473
|
+
SPEC-PLATFORM-TELEMETRY-0012: Telemetry acceptance_criteria reference existing criteria
|
|
474
|
+
|
|
475
|
+
Given: Telemetry signal files with acceptance_criteria arrays
|
|
476
|
+
When: Checking acceptance criteria files
|
|
477
|
+
Then: All referenced acceptance URNs exist in plan/ directories
|
|
478
|
+
"""
|
|
479
|
+
signal_files = find_all_telemetry_signals()
|
|
480
|
+
|
|
481
|
+
if not signal_files:
|
|
482
|
+
pytest.skip("No telemetry signal files found")
|
|
483
|
+
|
|
484
|
+
acceptance_urns = load_plan_acceptance_urns()
|
|
485
|
+
if not acceptance_urns:
|
|
486
|
+
pytest.skip("No acceptance URNs found in plan/")
|
|
487
|
+
|
|
488
|
+
urn_pattern = re.compile(
|
|
489
|
+
r"^acc:[a-z][a-z0-9_-]*:([DLPCEMYRK][0-9]{3}-(UNIT|HTTP|EVENT|WS|E2E|A11Y|VIS|METRIC|JOB|DB|SEC|LOAD|SCRIPT|WIDGET|GOLDEN|BLOC|INTEGRATION|RLS|EDGE|REALTIME|STORAGE)-[0-9]{3}(?:-[a-z0-9-]+)?|[A-Z][0-9]{3})$"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
missing = []
|
|
493
|
+
empty_criteria = []
|
|
494
|
+
|
|
495
|
+
for signal_path in signal_files:
|
|
496
|
+
try:
|
|
497
|
+
with open(signal_path) as f:
|
|
498
|
+
signal = json.load(f)
|
|
499
|
+
except json.JSONDecodeError:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
acceptance_criteria = signal.get("acceptance_criteria", [])
|
|
503
|
+
if not acceptance_criteria:
|
|
504
|
+
empty_criteria.append(signal_path)
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
for ref in acceptance_criteria:
|
|
508
|
+
if not urn_pattern.match(ref):
|
|
509
|
+
missing.append(
|
|
510
|
+
f"{signal_path.relative_to(REPO_ROOT)}: acceptance_criteria '{ref}' has invalid format"
|
|
511
|
+
)
|
|
512
|
+
continue
|
|
513
|
+
if ref not in acceptance_urns:
|
|
514
|
+
missing.append(
|
|
515
|
+
f"{signal_path.relative_to(REPO_ROOT)}: acceptance_criteria '{ref}' not found in plan/"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if empty_criteria:
|
|
519
|
+
print(
|
|
520
|
+
"Telemetry signals missing acceptance_criteria:\n" +
|
|
521
|
+
"\n".join(f" {p.relative_to(REPO_ROOT)}" for p in empty_criteria[:10]) +
|
|
522
|
+
(f"\n ... and {len(empty_criteria) - 10} more" if len(empty_criteria) > 10 else "")
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if missing:
|
|
526
|
+
pytest.fail(
|
|
527
|
+
f"Found {len(missing)} invalid acceptance references:\n" +
|
|
528
|
+
"\n".join(f" {err}" for err in missing[:10]) +
|
|
529
|
+
(f"\n ... and {len(missing) - 10} more" if len(missing) > 10 else "")
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@pytest.mark.platform
|
|
534
|
+
def test_no_duplicate_telemetry_ids():
|
|
535
|
+
"""
|
|
536
|
+
SPEC-PLATFORM-TELEMETRY-0013: Telemetry $id fields are unique
|
|
537
|
+
|
|
538
|
+
Given: All telemetry signal files in telemetry/
|
|
539
|
+
When: Collecting $id values
|
|
540
|
+
Then: No two signals have the same $id
|
|
541
|
+
"""
|
|
542
|
+
signal_files = find_all_telemetry_signals()
|
|
543
|
+
|
|
544
|
+
if not signal_files:
|
|
545
|
+
pytest.skip("No telemetry signal files found")
|
|
546
|
+
|
|
547
|
+
seen = {}
|
|
548
|
+
duplicates = {}
|
|
549
|
+
|
|
550
|
+
for signal_path in signal_files:
|
|
551
|
+
try:
|
|
552
|
+
with open(signal_path) as f:
|
|
553
|
+
signal = json.load(f)
|
|
554
|
+
except json.JSONDecodeError:
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
signal_id = signal.get("$id")
|
|
558
|
+
if not signal_id:
|
|
559
|
+
continue
|
|
560
|
+
if signal_id in seen:
|
|
561
|
+
duplicates.setdefault(signal_id, [seen[signal_id]]).append(signal_path)
|
|
562
|
+
else:
|
|
563
|
+
seen[signal_id] = signal_path
|
|
564
|
+
|
|
565
|
+
if duplicates:
|
|
566
|
+
lines = []
|
|
567
|
+
for signal_id, paths in duplicates.items():
|
|
568
|
+
lines.append(f"$id: \"{signal_id}\"")
|
|
569
|
+
for path in paths:
|
|
570
|
+
lines.append(f" - {path.relative_to(REPO_ROOT)}")
|
|
571
|
+
|
|
572
|
+
pytest.fail(
|
|
573
|
+
"Found duplicate telemetry IDs:\n" +
|
|
574
|
+
"\n".join(lines)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@pytest.mark.platform
|
|
579
|
+
def test_no_orphaned_telemetry_directories(telemetry_urns):
|
|
580
|
+
"""
|
|
581
|
+
SPEC-PLATFORM-TELEMETRY-0007: No orphaned telemetry directories
|
|
582
|
+
|
|
583
|
+
Given: Telemetry directories in telemetry/
|
|
584
|
+
When: Comparing to telemetry URNs from wagons
|
|
585
|
+
Then: Each telemetry/{theme}/{domain}/{aspect}/ has a corresponding URN
|
|
586
|
+
No orphaned directories that aren't referenced
|
|
587
|
+
"""
|
|
588
|
+
if not TELEMETRY_DIR.exists():
|
|
589
|
+
pytest.skip(f"telemetry/ directory does not exist")
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
# Build set of expected paths from URNs
|
|
593
|
+
expected_paths = set()
|
|
594
|
+
|
|
595
|
+
for urn in telemetry_urns:
|
|
596
|
+
parts = urn.split(":")
|
|
597
|
+
if len(parts) == 4:
|
|
598
|
+
_, theme, domain, aspect = parts
|
|
599
|
+
expected_paths.add(f"{theme}/{domain}/{aspect}")
|
|
600
|
+
|
|
601
|
+
# Check actual directory structure
|
|
602
|
+
orphaned = []
|
|
603
|
+
|
|
604
|
+
for theme_dir in TELEMETRY_DIR.iterdir():
|
|
605
|
+
if not theme_dir.is_dir() or theme_dir.name.startswith((".", "_")):
|
|
606
|
+
continue
|
|
607
|
+
|
|
608
|
+
theme_name = theme_dir.name
|
|
609
|
+
|
|
610
|
+
for domain_dir in theme_dir.iterdir():
|
|
611
|
+
if not domain_dir.is_dir() or domain_dir.name.startswith((".", "_")) or domain_dir.name == "tests":
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
domain_name = domain_dir.name
|
|
615
|
+
|
|
616
|
+
# Check signal files for aspects (aspects are filename prefixes now)
|
|
617
|
+
signal_files = [f for f in domain_dir.glob("*.json") if "tests" not in f.parts]
|
|
618
|
+
|
|
619
|
+
for signal_file in signal_files:
|
|
620
|
+
# Extract aspect from filename (first part before .)
|
|
621
|
+
aspect_name = signal_file.name.split('.')[0]
|
|
622
|
+
path = f"{theme_name}/{domain_name}/{aspect_name}"
|
|
623
|
+
|
|
624
|
+
# Check if this path is referenced by a URN
|
|
625
|
+
if path not in expected_paths:
|
|
626
|
+
orphaned.append(path)
|
|
627
|
+
|
|
628
|
+
if orphaned:
|
|
629
|
+
pytest.skip(
|
|
630
|
+
f"Found {len(orphaned)} telemetry directories without corresponding URNs:\n" +
|
|
631
|
+
"\n".join(f" telemetry/{d}" for d in orphaned[:10]) +
|
|
632
|
+
(f"\n ... and {len(orphaned) - 10} more" if len(orphaned) > 10 else "") +
|
|
633
|
+
"\n (This may be expected for legacy or future telemetry)"
|
|
634
|
+
)
|