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,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "WMBT (What Must Be True) Schema",
|
|
4
|
+
"description": "Schema for What Must Be True statements that define measurable outcomes",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["urn", "step", "direction", "dimension", "object_of_control", "lens"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"urn": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^wmbt:[a-z][a-z0-9-]*:[DLPCEMYRK][0-9]{3}$",
|
|
12
|
+
"description": "WMBT URN: wmbt:{wagon}:{step_coded_id} (e.g., wmbt:burn-timebank:E001) - step-coded sequence"
|
|
13
|
+
},
|
|
14
|
+
"step": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["define", "locate", "prepare", "confirm", "execute", "monitor", "modify", "resolve", "conclude"],
|
|
17
|
+
"description": "JTBD decomposition step indicating where in the job flow this outcome matters"
|
|
18
|
+
},
|
|
19
|
+
"direction": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"enum": ["minimize", "maximize", "increase", "decrease"],
|
|
22
|
+
"description": "Optimization direction indicating how to improve the outcome"
|
|
23
|
+
},
|
|
24
|
+
"dimension": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": ["time", "effort", "likelihood", "frequency", "quantity", "financial value"],
|
|
27
|
+
"description": "Measurement dimension - exactly one per WMBT to maintain focus"
|
|
28
|
+
},
|
|
29
|
+
"object_of_control": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
32
|
+
"minLength": 2,
|
|
33
|
+
"description": "Descriptive of what is being controlled or measured"
|
|
34
|
+
},
|
|
35
|
+
"context_clarifier": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^[a-z].{1,250}$",
|
|
38
|
+
"description": "Detailed situational phrase specifying when/where/how this applies (one line maximum)"
|
|
39
|
+
},
|
|
40
|
+
"lens": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"pattern": "^(functional|emotional|social)\\.[a-z][a-z0-9-]*$",
|
|
43
|
+
"description": "Specific lens aspect reference (e.g., functional.efficiency, emotional.trust, social.belong)"
|
|
44
|
+
},
|
|
45
|
+
"statement": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"minLength": 10,
|
|
48
|
+
"description": "Human-readable statement: direction + dimension + object_of_control + context_clarifier"
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
"appendices": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"description": "Supplementary files specific to this WMBT scenario",
|
|
54
|
+
"items": {
|
|
55
|
+
"$ref": "appendix.schema.json#/definitions/appendix_reference"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform tests: Draft Wagon Registry Validation.
|
|
3
|
+
|
|
4
|
+
Validates draft wagons in plan/_wagons.yaml registry for coherence with:
|
|
5
|
+
- Existing wagon manifests (prevent duplicates)
|
|
6
|
+
- Contract/telemetry references (check artifact resolution)
|
|
7
|
+
- Traceability (cross-reference validation)
|
|
8
|
+
- Implementation status (manifest vs draft)
|
|
9
|
+
|
|
10
|
+
Tests help agents make better decisions when encountering references to non-existent wagons
|
|
11
|
+
by checking the registry first before assuming external services.
|
|
12
|
+
"""
|
|
13
|
+
import pytest
|
|
14
|
+
import yaml
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from atdd.coach.validators.shared_fixtures import PLAN_DIR
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="module")
|
|
20
|
+
def wagon_registry():
|
|
21
|
+
"""Load plan/_wagons.yaml registry."""
|
|
22
|
+
registry_path = PLAN_DIR / "_wagons.yaml"
|
|
23
|
+
if not registry_path.exists():
|
|
24
|
+
pytest.skip(f"Wagon registry not found: {registry_path}")
|
|
25
|
+
|
|
26
|
+
with open(registry_path) as f:
|
|
27
|
+
data = yaml.safe_load(f)
|
|
28
|
+
|
|
29
|
+
return data.get("wagons", [])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture(scope="module")
|
|
33
|
+
def implemented_wagon_slugs(wagon_manifests):
|
|
34
|
+
"""Extract wagon slugs that have manifests (implemented)."""
|
|
35
|
+
slugs = set()
|
|
36
|
+
for manifest_path, manifest in wagon_manifests:
|
|
37
|
+
slug = manifest.get("wagon", "")
|
|
38
|
+
if slug:
|
|
39
|
+
slugs.add(slug)
|
|
40
|
+
return slugs
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="module")
|
|
44
|
+
def draft_wagons(wagon_registry, implemented_wagon_slugs):
|
|
45
|
+
"""Filter wagons from registry that are drafts (no manifest/path)."""
|
|
46
|
+
drafts = []
|
|
47
|
+
for wagon in wagon_registry:
|
|
48
|
+
slug = wagon.get("wagon", "")
|
|
49
|
+
# Draft wagons don't have 'manifest' or 'path' fields
|
|
50
|
+
has_manifest = wagon.get("manifest") or wagon.get("path")
|
|
51
|
+
is_implemented = slug in implemented_wagon_slugs
|
|
52
|
+
|
|
53
|
+
if not has_manifest and not is_implemented:
|
|
54
|
+
drafts.append(wagon)
|
|
55
|
+
|
|
56
|
+
return drafts
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture(scope="module")
|
|
60
|
+
def all_registry_wagon_slugs(wagon_registry):
|
|
61
|
+
"""Get all wagon slugs from registry (both draft and implemented)."""
|
|
62
|
+
return {wagon.get("wagon", "") for wagon in wagon_registry if wagon.get("wagon")}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.platform
|
|
66
|
+
def test_draft_wagons_are_valid_yaml(draft_wagons):
|
|
67
|
+
"""
|
|
68
|
+
SPEC-PLATFORM-REGISTRY-0001: Draft wagons have valid structure
|
|
69
|
+
|
|
70
|
+
Given: Draft wagons in plan/_wagons.yaml
|
|
71
|
+
When: Checking basic structure
|
|
72
|
+
Then: Each draft has required fields: wagon, description, theme, subject
|
|
73
|
+
"""
|
|
74
|
+
required_fields = ["wagon", "description", "theme", "subject", "context", "action", "goal", "outcome"]
|
|
75
|
+
|
|
76
|
+
errors = []
|
|
77
|
+
for draft in draft_wagons:
|
|
78
|
+
wagon_slug = draft.get("wagon", "UNKNOWN")
|
|
79
|
+
for field in required_fields:
|
|
80
|
+
if field not in draft:
|
|
81
|
+
errors.append(
|
|
82
|
+
f"Draft wagon '{wagon_slug}' missing required field: {field}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if errors:
|
|
86
|
+
pytest.fail("\n".join(errors))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.platform
|
|
90
|
+
def test_draft_wagons_not_duplicated_in_manifests(draft_wagons, implemented_wagon_slugs):
|
|
91
|
+
"""
|
|
92
|
+
SPEC-PLATFORM-REGISTRY-0002: Draft wagons don't have manifests
|
|
93
|
+
|
|
94
|
+
Given: Draft wagons in registry
|
|
95
|
+
When: Checking against implemented wagon manifests
|
|
96
|
+
Then: Draft wagon slugs should NOT have manifest files
|
|
97
|
+
(If they do, they're implemented, not draft)
|
|
98
|
+
"""
|
|
99
|
+
errors = []
|
|
100
|
+
for draft in draft_wagons:
|
|
101
|
+
slug = draft.get("wagon", "")
|
|
102
|
+
if slug in implemented_wagon_slugs:
|
|
103
|
+
errors.append(
|
|
104
|
+
f"Wagon '{slug}' is in registry as draft but has manifest file - "
|
|
105
|
+
f"should add 'manifest' and 'path' fields to registry entry"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if errors:
|
|
109
|
+
pytest.fail("\n\n".join(errors))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.platform
|
|
113
|
+
def test_registry_produce_artifacts_follow_convention(wagon_registry):
|
|
114
|
+
"""
|
|
115
|
+
SPEC-PLATFORM-REGISTRY-0003: Registry produce artifacts follow artifact-naming convention v2.1
|
|
116
|
+
|
|
117
|
+
Given: All wagons in registry (draft + implemented)
|
|
118
|
+
When: Checking produce artifact names
|
|
119
|
+
Then: All artifacts follow pattern: {theme}(:{category})*:{aspect}(.{variant})?
|
|
120
|
+
Supports unlimited hierarchical depth with colons (e.g., commons:ux:foundations)
|
|
121
|
+
Supports optional variant with dot (e.g., match:dilemma.paired)
|
|
122
|
+
"""
|
|
123
|
+
import re
|
|
124
|
+
# Pattern per artifact-naming.convention.yaml v2.1:
|
|
125
|
+
# {theme}(:{category})*:{aspect}(.{variant})?
|
|
126
|
+
# - theme: required (1 segment)
|
|
127
|
+
# - categories: optional (0+ segments with colons)
|
|
128
|
+
# - aspect: required (1 segment)
|
|
129
|
+
# - variant: optional (1 segment with dot)
|
|
130
|
+
artifact_pattern = re.compile(r"^[a-z][a-z0-9-]+:[a-z][a-z0-9-]+(:[a-z][a-z0-9-]+)*(\.[a-z][a-z0-9-]+)?$")
|
|
131
|
+
|
|
132
|
+
errors = []
|
|
133
|
+
for wagon in wagon_registry:
|
|
134
|
+
wagon_slug = wagon.get("wagon", "UNKNOWN")
|
|
135
|
+
produce_items = wagon.get("produce", [])
|
|
136
|
+
|
|
137
|
+
for idx, item in enumerate(produce_items):
|
|
138
|
+
name = item.get("name", "")
|
|
139
|
+
if not artifact_pattern.match(name):
|
|
140
|
+
errors.append(
|
|
141
|
+
f"Wagon '{wagon_slug}' produce[{idx}] has invalid artifact name: '{name}'\n"
|
|
142
|
+
f" Expected pattern: {{theme}}(:{{category}})*:{{aspect}}(.{{variant}})?\n"
|
|
143
|
+
f" Examples: commons:ux:foundations, match:dilemma.paired"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if errors:
|
|
147
|
+
pytest.fail("\n\n".join(errors))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.platform
|
|
151
|
+
def test_registry_consume_references_valid_wagons(wagon_registry, all_registry_wagon_slugs):
|
|
152
|
+
"""
|
|
153
|
+
SPEC-PLATFORM-REGISTRY-0004: Registry consume references are coherent
|
|
154
|
+
|
|
155
|
+
Given: All wagons in registry
|
|
156
|
+
When: Checking consume 'from' references
|
|
157
|
+
Then: wagon:slug references resolve to wagons in registry OR
|
|
158
|
+
References use valid patterns: system:*, appendix:*, internal
|
|
159
|
+
"""
|
|
160
|
+
errors = []
|
|
161
|
+
|
|
162
|
+
for wagon in wagon_registry:
|
|
163
|
+
wagon_slug = wagon.get("wagon", "UNKNOWN")
|
|
164
|
+
consume_items = wagon.get("consume", [])
|
|
165
|
+
|
|
166
|
+
for idx, item in enumerate(consume_items):
|
|
167
|
+
from_ref = item.get("from", "")
|
|
168
|
+
if not from_ref:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Check pattern validity
|
|
172
|
+
if from_ref.startswith("wagon:"):
|
|
173
|
+
referenced_wagon = from_ref.split(":", 1)[1]
|
|
174
|
+
if referenced_wagon not in all_registry_wagon_slugs:
|
|
175
|
+
errors.append(
|
|
176
|
+
f"Wagon '{wagon_slug}' consume[{idx}] references unknown wagon: '{referenced_wagon}'\n"
|
|
177
|
+
f" Reference: {from_ref}\n"
|
|
178
|
+
f" Artifact: {item.get('name', 'UNKNOWN')}\n"
|
|
179
|
+
f" Hint: Check if wagon exists in registry or should be system:* reference"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
elif from_ref.startswith("system:"):
|
|
183
|
+
# System references are valid
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
elif from_ref.startswith("appendix:"):
|
|
187
|
+
# Appendix references are valid
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
elif from_ref == "internal":
|
|
191
|
+
# Internal reference is valid
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
else:
|
|
195
|
+
errors.append(
|
|
196
|
+
f"Wagon '{wagon_slug}' consume[{idx}] has invalid 'from' pattern: '{from_ref}'\n"
|
|
197
|
+
f" Expected: wagon:slug, system:service, appendix:type, or internal"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if errors:
|
|
201
|
+
pytest.fail("\n\n".join(errors))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@pytest.mark.platform
|
|
205
|
+
def test_registry_produce_artifacts_have_consumers(wagon_registry):
|
|
206
|
+
"""
|
|
207
|
+
SPEC-PLATFORM-REGISTRY-0005: Registry produce artifacts are consumed
|
|
208
|
+
|
|
209
|
+
Given: All wagons in registry
|
|
210
|
+
When: Checking produce artifacts against all consume references
|
|
211
|
+
Then: Each produce artifact should be consumed by at least one wagon
|
|
212
|
+
(Orphaned artifacts should be flagged as warnings)
|
|
213
|
+
|
|
214
|
+
Note: This is a soft validation - some artifacts may be consumed externally
|
|
215
|
+
"""
|
|
216
|
+
# Build produce artifact registry
|
|
217
|
+
produced_artifacts = {} # artifact_name -> wagon_slug
|
|
218
|
+
for wagon in wagon_registry:
|
|
219
|
+
wagon_slug = wagon.get("wagon", "")
|
|
220
|
+
for item in wagon.get("produce", []):
|
|
221
|
+
artifact_name = item.get("name", "")
|
|
222
|
+
if artifact_name:
|
|
223
|
+
if artifact_name not in produced_artifacts:
|
|
224
|
+
produced_artifacts[artifact_name] = []
|
|
225
|
+
produced_artifacts[artifact_name].append(wagon_slug)
|
|
226
|
+
|
|
227
|
+
# Build consume artifact set
|
|
228
|
+
consumed_artifacts = set()
|
|
229
|
+
for wagon in wagon_registry:
|
|
230
|
+
for item in wagon.get("consume", []):
|
|
231
|
+
artifact_name = item.get("name", "")
|
|
232
|
+
if artifact_name:
|
|
233
|
+
consumed_artifacts.add(artifact_name)
|
|
234
|
+
|
|
235
|
+
# Find orphaned artifacts
|
|
236
|
+
warnings = []
|
|
237
|
+
for artifact_name, producers in produced_artifacts.items():
|
|
238
|
+
if artifact_name not in consumed_artifacts:
|
|
239
|
+
producers_str = ", ".join(producers)
|
|
240
|
+
warnings.append(
|
|
241
|
+
f"Artifact '{artifact_name}' produced by [{producers_str}] "
|
|
242
|
+
f"but not consumed by any wagon (may be external/endpoint)"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Report warnings (not failures) - informational only
|
|
246
|
+
if warnings:
|
|
247
|
+
print(f"\n\n⚠️ Orphaned Artifacts (may be intentional for external consumption):")
|
|
248
|
+
for warning in warnings:
|
|
249
|
+
print(f" • {warning}")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest.mark.platform
|
|
253
|
+
def test_draft_wagon_contract_coherence(draft_wagons):
|
|
254
|
+
"""
|
|
255
|
+
SPEC-PLATFORM-REGISTRY-0006: Draft wagon contract references are coherent
|
|
256
|
+
|
|
257
|
+
Given: Draft wagons in registry
|
|
258
|
+
When: Checking contract/telemetry URN references
|
|
259
|
+
Then: URNs follow expected pattern: contract:domain:resource
|
|
260
|
+
Telemetry follows: telemetry:domain:resource
|
|
261
|
+
|
|
262
|
+
Note: This doesn't validate filesystem resolution (draft wagons don't have contracts yet)
|
|
263
|
+
Just validates URN format coherence
|
|
264
|
+
"""
|
|
265
|
+
import re
|
|
266
|
+
contract_pattern = re.compile(r"^contract:[a-z]+:[a-z][a-z0-9-]+(\.[a-z][a-z0-9-]+)?$")
|
|
267
|
+
telemetry_pattern = re.compile(r"^telemetry:[a-z]+:[a-z][a-z0-9-]+(\.[a-z][a-z0-9-]+)?$")
|
|
268
|
+
|
|
269
|
+
errors = []
|
|
270
|
+
|
|
271
|
+
for draft in draft_wagons:
|
|
272
|
+
wagon_slug = draft.get("wagon", "UNKNOWN")
|
|
273
|
+
|
|
274
|
+
# Check produce items
|
|
275
|
+
for idx, item in enumerate(draft.get("produce", [])):
|
|
276
|
+
# Contract URN
|
|
277
|
+
if "contract" in item and item["contract"]:
|
|
278
|
+
contract_urn = item["contract"]
|
|
279
|
+
if not contract_pattern.match(contract_urn):
|
|
280
|
+
errors.append(
|
|
281
|
+
f"Draft wagon '{wagon_slug}' produce[{idx}] has invalid contract URN: '{contract_urn}'\n"
|
|
282
|
+
f" Expected pattern: contract:domain:resource[.category]"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Telemetry URN
|
|
286
|
+
if "telemetry" in item and item["telemetry"]:
|
|
287
|
+
telemetry_urn = item["telemetry"]
|
|
288
|
+
if not telemetry_pattern.match(telemetry_urn):
|
|
289
|
+
errors.append(
|
|
290
|
+
f"Draft wagon '{wagon_slug}' produce[{idx}] has invalid telemetry URN: '{telemetry_urn}'\n"
|
|
291
|
+
f" Expected pattern: telemetry:domain:resource[.category]"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if errors:
|
|
295
|
+
pytest.fail("\n\n".join(errors))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.mark.platform
|
|
299
|
+
def test_registry_wagon_slugs_are_unique(wagon_registry):
|
|
300
|
+
"""
|
|
301
|
+
SPEC-PLATFORM-REGISTRY-0007: Wagon slugs in registry are unique
|
|
302
|
+
|
|
303
|
+
Given: All wagons in registry
|
|
304
|
+
When: Checking wagon slugs
|
|
305
|
+
Then: Each slug appears only once
|
|
306
|
+
"""
|
|
307
|
+
slug_counts = {}
|
|
308
|
+
for wagon in wagon_registry:
|
|
309
|
+
slug = wagon.get("wagon", "")
|
|
310
|
+
if slug:
|
|
311
|
+
slug_counts[slug] = slug_counts.get(slug, 0) + 1
|
|
312
|
+
|
|
313
|
+
duplicates = {slug: count for slug, count in slug_counts.items() if count > 1}
|
|
314
|
+
|
|
315
|
+
if duplicates:
|
|
316
|
+
errors = []
|
|
317
|
+
for slug, count in duplicates.items():
|
|
318
|
+
errors.append(f"Wagon slug '{slug}' appears {count} times in registry")
|
|
319
|
+
pytest.fail("\n".join(errors))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@pytest.mark.platform
|
|
323
|
+
def test_registry_has_all_implemented_wagons(wagon_registry, implemented_wagon_slugs):
|
|
324
|
+
"""
|
|
325
|
+
SPEC-PLATFORM-REGISTRY-0008: All implemented wagons are in registry
|
|
326
|
+
|
|
327
|
+
Given: Wagon manifests in plan/*/
|
|
328
|
+
When: Checking against registry
|
|
329
|
+
Then: All wagon manifest slugs should be in registry
|
|
330
|
+
(Registry is the source of truth)
|
|
331
|
+
"""
|
|
332
|
+
registry_slugs = {wagon.get("wagon", "") for wagon in wagon_registry}
|
|
333
|
+
|
|
334
|
+
missing_from_registry = implemented_wagon_slugs - registry_slugs
|
|
335
|
+
|
|
336
|
+
if missing_from_registry:
|
|
337
|
+
errors = [
|
|
338
|
+
f"Wagon '{slug}' has manifest but is NOT in registry plan/_wagons.yaml"
|
|
339
|
+
for slug in sorted(missing_from_registry)
|
|
340
|
+
]
|
|
341
|
+
pytest.fail(
|
|
342
|
+
"Implemented wagons missing from registry:\n" +
|
|
343
|
+
"\n".join(f" • {e}" for e in errors)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.platform
|
|
348
|
+
def test_registry_implemented_wagons_have_path_and_manifest(wagon_registry, implemented_wagon_slugs):
|
|
349
|
+
"""
|
|
350
|
+
SPEC-PLATFORM-REGISTRY-0009: Implemented wagons have manifest/path fields
|
|
351
|
+
|
|
352
|
+
Given: Wagons in registry that have manifests
|
|
353
|
+
When: Checking registry entries
|
|
354
|
+
Then: Registry entry should have 'manifest' and 'path' fields
|
|
355
|
+
These fields help distinguish implemented vs draft wagons
|
|
356
|
+
"""
|
|
357
|
+
errors = []
|
|
358
|
+
|
|
359
|
+
for wagon in wagon_registry:
|
|
360
|
+
slug = wagon.get("wagon", "")
|
|
361
|
+
if slug in implemented_wagon_slugs:
|
|
362
|
+
if not wagon.get("manifest"):
|
|
363
|
+
errors.append(
|
|
364
|
+
f"Implemented wagon '{slug}' missing 'manifest' field in registry\n"
|
|
365
|
+
f" Expected: manifest: plan/{slug.replace('-', '_')}/_{slug.replace('-', '_')}.yaml"
|
|
366
|
+
)
|
|
367
|
+
if not wagon.get("path"):
|
|
368
|
+
errors.append(
|
|
369
|
+
f"Implemented wagon '{slug}' missing 'path' field in registry\n"
|
|
370
|
+
f" Expected: path: plan/{slug.replace('-', '_')}/"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if errors:
|
|
374
|
+
pytest.fail("\n\n".join(errors))
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform tests: Cross-reference validation.
|
|
3
|
+
|
|
4
|
+
Validates that cross-references between wagons, trains, and artifacts are coherent.
|
|
5
|
+
Tests ensure that consume references point to valid produce artifacts.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from typing import Dict, Set, List, Tuple, Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.platform
|
|
12
|
+
@pytest.mark.e2e
|
|
13
|
+
def test_wagon_consume_references_valid_produce_or_external(wagon_manifests):
|
|
14
|
+
"""
|
|
15
|
+
SPEC-PLATFORM-REFS-0001: Wagon consume references point to valid sources
|
|
16
|
+
|
|
17
|
+
Given: Wagon consume items with 'from' field
|
|
18
|
+
When: Checking consume references
|
|
19
|
+
Then: Each 'from' reference either:
|
|
20
|
+
- Points to another wagon's produce (wagon:slug format)
|
|
21
|
+
- Points to external system (system:external)
|
|
22
|
+
- Points to appendix (appendix:type)
|
|
23
|
+
- Is omitted (defaults to inferred/external)
|
|
24
|
+
"""
|
|
25
|
+
# Build produce registry: {artifact_name: [wagon_slugs]}
|
|
26
|
+
produce_registry: Dict[str, List[str]] = {}
|
|
27
|
+
wagon_slugs: Set[str] = set()
|
|
28
|
+
|
|
29
|
+
for path, manifest in wagon_manifests:
|
|
30
|
+
wagon_slug = manifest.get("wagon", "")
|
|
31
|
+
wagon_slugs.add(wagon_slug)
|
|
32
|
+
|
|
33
|
+
for produce_item in manifest.get("produce", []):
|
|
34
|
+
artifact_name = produce_item.get("name", "")
|
|
35
|
+
if artifact_name:
|
|
36
|
+
produce_registry.setdefault(artifact_name, []).append(wagon_slug)
|
|
37
|
+
|
|
38
|
+
# Validate consume references
|
|
39
|
+
for path, manifest in wagon_manifests:
|
|
40
|
+
wagon_slug = manifest.get("wagon", "")
|
|
41
|
+
|
|
42
|
+
for consume_item in manifest.get("consume", []):
|
|
43
|
+
from_ref = consume_item.get("from", "")
|
|
44
|
+
|
|
45
|
+
# Skip if from is not specified (defaults to external/inferred)
|
|
46
|
+
if not from_ref:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# Valid patterns:
|
|
50
|
+
# - wagon:slug (reference to another wagon)
|
|
51
|
+
# - system:external (external dependency)
|
|
52
|
+
# - appendix:type (appendix artifact)
|
|
53
|
+
if from_ref.startswith("wagon:"):
|
|
54
|
+
referenced_wagon = from_ref.split(":", 1)[1]
|
|
55
|
+
assert referenced_wagon in wagon_slugs, \
|
|
56
|
+
f"Wagon {wagon_slug} at {path} consumes from unknown wagon: {referenced_wagon}"
|
|
57
|
+
|
|
58
|
+
elif from_ref.startswith("system:"):
|
|
59
|
+
# System references are allowed (e.g., system:external)
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
elif from_ref.startswith("appendix:"):
|
|
63
|
+
# Appendix references are allowed
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
else:
|
|
67
|
+
# Unknown pattern - should match wagon:, system:, or appendix:
|
|
68
|
+
pytest.fail(
|
|
69
|
+
f"Wagon {wagon_slug} at {path} has invalid 'from' reference: {from_ref}\n"
|
|
70
|
+
f" Expected format: wagon:slug, system:external, or appendix:type"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.platform
|
|
75
|
+
def test_no_circular_dependencies_simple(wagon_manifests):
|
|
76
|
+
"""
|
|
77
|
+
SPEC-PLATFORM-REFS-0002: No direct circular dependencies between wagons
|
|
78
|
+
|
|
79
|
+
Given: Wagon consume to produce graph
|
|
80
|
+
When: Checking for circular dependencies
|
|
81
|
+
Then: No wagon directly consumes its own produce
|
|
82
|
+
(Advanced cycle detection in separate test)
|
|
83
|
+
"""
|
|
84
|
+
for path, manifest in wagon_manifests:
|
|
85
|
+
wagon_slug = manifest.get("wagon", "")
|
|
86
|
+
|
|
87
|
+
for consume_item in manifest.get("consume", []):
|
|
88
|
+
from_ref = consume_item.get("from", "")
|
|
89
|
+
|
|
90
|
+
if from_ref.startswith("wagon:"):
|
|
91
|
+
referenced_wagon = from_ref.split(":", 1)[1]
|
|
92
|
+
assert referenced_wagon != wagon_slug, \
|
|
93
|
+
f"Wagon {wagon_slug} at {path} has circular dependency (consumes from itself)"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.platform
|
|
97
|
+
def test_trains_reference_valid_wagons(trains_registry, wagon_manifests):
|
|
98
|
+
"""
|
|
99
|
+
SPEC-PLATFORM-REFS-0003: Train participants reference existing wagons
|
|
100
|
+
|
|
101
|
+
Given: Train definitions in plan/_trains/ (theme-grouped registry)
|
|
102
|
+
When: Checking train participant references
|
|
103
|
+
Then: All referenced wagons exist in wagon registry
|
|
104
|
+
"""
|
|
105
|
+
# Build wagon slug set
|
|
106
|
+
wagon_slugs = {manifest.get("wagon", "") for _, manifest in wagon_manifests}
|
|
107
|
+
|
|
108
|
+
# Check each train's participants (theme-grouped structure)
|
|
109
|
+
for theme, trains in trains_registry.items():
|
|
110
|
+
if not trains:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
for train in trains:
|
|
114
|
+
train_id = train.get("train_id", "")
|
|
115
|
+
train_path = train.get("path", "")
|
|
116
|
+
|
|
117
|
+
# Load individual train file if path exists
|
|
118
|
+
if train_path:
|
|
119
|
+
import yaml
|
|
120
|
+
from pathlib import Path
|
|
121
|
+
train_file = Path(__file__).resolve().parents[4] / train_path
|
|
122
|
+
|
|
123
|
+
if train_file.exists():
|
|
124
|
+
with open(train_file) as f:
|
|
125
|
+
train_data = yaml.safe_load(f)
|
|
126
|
+
|
|
127
|
+
# Check participants if present
|
|
128
|
+
participants = train_data.get("participants", [])
|
|
129
|
+
for participant in participants:
|
|
130
|
+
# Handle both formats: string ("wagon:slug") or object ({wagon: "slug"})
|
|
131
|
+
if isinstance(participant, str):
|
|
132
|
+
# String format: "wagon:slug" or "system:user"
|
|
133
|
+
if participant.startswith("wagon:"):
|
|
134
|
+
wagon_ref = participant.split(":", 1)[1]
|
|
135
|
+
else:
|
|
136
|
+
# Skip non-wagon participants (system:*, user:*, etc.)
|
|
137
|
+
continue
|
|
138
|
+
else:
|
|
139
|
+
# Object format: {wagon: "slug"}
|
|
140
|
+
wagon_ref = participant.get("wagon", "")
|
|
141
|
+
|
|
142
|
+
if wagon_ref:
|
|
143
|
+
assert wagon_ref in wagon_slugs, \
|
|
144
|
+
f"Train {train_id} (theme: {theme}) references unknown wagon: {wagon_ref}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pytest.mark.platform
|
|
148
|
+
def test_produce_and_consume_artifact_names_are_coherent(wagon_manifests):
|
|
149
|
+
"""
|
|
150
|
+
SPEC-PLATFORM-REFS-0004: Consumed artifacts exist in produce registry
|
|
151
|
+
|
|
152
|
+
Given: Wagon consume items without explicit 'from' field
|
|
153
|
+
When: Artifact name is used to infer source
|
|
154
|
+
Then: Artifact name should match a produced artifact somewhere
|
|
155
|
+
OR be a known external/appendix pattern
|
|
156
|
+
"""
|
|
157
|
+
# Build produce artifact name registry
|
|
158
|
+
produce_names: Set[str] = set()
|
|
159
|
+
|
|
160
|
+
for _, manifest in wagon_manifests:
|
|
161
|
+
for produce_item in manifest.get("produce", []):
|
|
162
|
+
artifact_name = produce_item.get("name", "")
|
|
163
|
+
if artifact_name:
|
|
164
|
+
produce_names.add(artifact_name)
|
|
165
|
+
|
|
166
|
+
# Check consume references
|
|
167
|
+
warnings = []
|
|
168
|
+
|
|
169
|
+
for path, manifest in wagon_manifests:
|
|
170
|
+
wagon_slug = manifest.get("wagon", "")
|
|
171
|
+
|
|
172
|
+
for consume_item in manifest.get("consume", []):
|
|
173
|
+
artifact_name = consume_item.get("name", "")
|
|
174
|
+
from_ref = consume_item.get("from", "")
|
|
175
|
+
|
|
176
|
+
# Skip if from is explicitly set to external/appendix/system
|
|
177
|
+
if from_ref and (
|
|
178
|
+
from_ref.startswith("system:") or
|
|
179
|
+
from_ref.startswith("appendix:")
|
|
180
|
+
):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# Check if artifact name exists in produce registry
|
|
184
|
+
if artifact_name and artifact_name not in produce_names:
|
|
185
|
+
# Special patterns that are allowed even if not produced
|
|
186
|
+
if any(artifact_name.startswith(prefix) for prefix in [
|
|
187
|
+
"appendix:", "system:", "external:"
|
|
188
|
+
]):
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
warnings.append(
|
|
192
|
+
f"Wagon {wagon_slug} at {path} consumes artifact '{artifact_name}' "
|
|
193
|
+
f"which is not produced by any wagon"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Report warnings if any
|
|
197
|
+
if warnings:
|
|
198
|
+
pytest.skip(
|
|
199
|
+
f"Found {len(warnings)} orphaned consume references:\n" +
|
|
200
|
+
"\n".join(f" - {w}" for w in warnings[:5]) +
|
|
201
|
+
(f"\n ... and {len(warnings) - 5} more" if len(warnings) > 5 else "")
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.platform
|
|
206
|
+
def test_wagon_to_field_references_valid_destinations(wagon_manifests):
|
|
207
|
+
"""
|
|
208
|
+
SPEC-PLATFORM-REFS-0005: Produce 'to' field references valid destinations
|
|
209
|
+
|
|
210
|
+
Given: Wagon produce items with 'to' field
|
|
211
|
+
When: Checking destination references
|
|
212
|
+
Then: Each 'to' reference is either:
|
|
213
|
+
- 'external' (default)
|
|
214
|
+
- 'internal' (wagon-internal artifact)
|
|
215
|
+
- wagon:slug (specific wagon destination)
|
|
216
|
+
"""
|
|
217
|
+
wagon_slugs = {manifest.get("wagon", "") for _, manifest in wagon_manifests}
|
|
218
|
+
|
|
219
|
+
for path, manifest in wagon_manifests:
|
|
220
|
+
wagon_slug = manifest.get("wagon", "")
|
|
221
|
+
|
|
222
|
+
for produce_item in manifest.get("produce", []):
|
|
223
|
+
to_ref = produce_item.get("to", "external") # Default to external
|
|
224
|
+
|
|
225
|
+
# Valid patterns:
|
|
226
|
+
# - 'external' (public artifact)
|
|
227
|
+
# - 'internal' (wagon-internal)
|
|
228
|
+
# - wagon:slug (specific destination)
|
|
229
|
+
if to_ref in ["external", "internal"]:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if to_ref.startswith("wagon:"):
|
|
233
|
+
referenced_wagon = to_ref.split(":", 1)[1]
|
|
234
|
+
assert referenced_wagon in wagon_slugs, \
|
|
235
|
+
f"Wagon {wagon_slug} at {path} produces to unknown wagon: {referenced_wagon}"
|
|
236
|
+
else:
|
|
237
|
+
pytest.fail(
|
|
238
|
+
f"Wagon {wagon_slug} at {path} has invalid 'to' reference: {to_ref}\n"
|
|
239
|
+
f" Expected: 'external', 'internal', or wagon:slug"
|
|
240
|
+
)
|