atdd 0.4.5__py3-none-any.whl → 0.4.7__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/cli.py +71 -12
- atdd/coach/commands/inventory.py +91 -3
- atdd/coach/commands/registry.py +475 -202
- atdd/coach/utils/config.py +131 -0
- atdd/coach/utils/train_spec_phase.py +97 -0
- atdd/coach/validators/shared_fixtures.py +68 -1
- atdd/coach/validators/test_train_registry.py +189 -0
- atdd/coder/validators/test_train_infrastructure.py +236 -2
- atdd/planner/schemas/train.schema.json +125 -2
- atdd/planner/validators/test_train_validation.py +667 -2
- atdd/tester/validators/test_train_backend_e2e.py +371 -0
- atdd/tester/validators/test_train_frontend_e2e.py +292 -0
- atdd/tester/validators/test_train_frontend_python.py +282 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/METADATA +1 -1
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/RECORD +19 -13
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/WHEEL +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/entry_points.txt +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/licenses/LICENSE +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Test train infrastructure validation (SESSION-12).
|
|
2
|
+
Test train infrastructure validation (SESSION-12 + Train First-Class Spec v0.6).
|
|
3
3
|
|
|
4
4
|
Validates conventions from:
|
|
5
5
|
- atdd/coder/conventions/train.convention.yaml
|
|
@@ -13,6 +13,11 @@ Enforces:
|
|
|
13
13
|
- E2E tests use production TrainRunner
|
|
14
14
|
- Station Master pattern in game.py
|
|
15
15
|
|
|
16
|
+
Train First-Class Spec v0.6 additions:
|
|
17
|
+
- SPEC-TRAIN-VAL-0031: Backend runner paths
|
|
18
|
+
- SPEC-TRAIN-VAL-0032: Frontend code allowed roots
|
|
19
|
+
- SPEC-TRAIN-VAL-0033: FastAPI template enforcement
|
|
20
|
+
|
|
16
21
|
Rationale:
|
|
17
22
|
Trains are production orchestration, not test infrastructure (SESSION-12).
|
|
18
23
|
These audits ensure the train composition root pattern is correctly implemented.
|
|
@@ -21,11 +26,18 @@ These audits ensure the train composition root pattern is correctly implemented.
|
|
|
21
26
|
import pytest
|
|
22
27
|
import ast
|
|
23
28
|
import re
|
|
29
|
+
import yaml
|
|
24
30
|
from pathlib import Path
|
|
25
|
-
from typing import List, Dict, Set, Tuple
|
|
31
|
+
from typing import List, Dict, Set, Tuple, Any
|
|
26
32
|
|
|
27
33
|
import atdd
|
|
28
34
|
from atdd.coach.utils.repo import find_repo_root
|
|
35
|
+
from atdd.coach.utils.train_spec_phase import (
|
|
36
|
+
TrainSpecPhase,
|
|
37
|
+
should_enforce,
|
|
38
|
+
emit_phase_warning
|
|
39
|
+
)
|
|
40
|
+
from atdd.coach.utils.config import get_train_config
|
|
29
41
|
|
|
30
42
|
|
|
31
43
|
# Path constants
|
|
@@ -459,3 +471,225 @@ def test_no_wagon_to_wagon_imports():
|
|
|
459
471
|
"\n\nWagons must communicate via contracts only, not direct imports\n"
|
|
460
472
|
"See: atdd/coder/conventions/boundaries.convention.yaml"
|
|
461
473
|
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ============================================================================
|
|
477
|
+
# TRAIN FIRST-CLASS SPEC v0.6 VALIDATORS
|
|
478
|
+
# ============================================================================
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _get_all_train_ids() -> List[str]:
|
|
482
|
+
"""Get all train IDs from registry."""
|
|
483
|
+
train_ids = []
|
|
484
|
+
trains_file = REPO_ROOT / "plan" / "_trains.yaml"
|
|
485
|
+
|
|
486
|
+
if trains_file.exists():
|
|
487
|
+
with open(trains_file) as f:
|
|
488
|
+
data = yaml.safe_load(f)
|
|
489
|
+
|
|
490
|
+
for theme_key, categories in data.get("trains", {}).items():
|
|
491
|
+
if isinstance(categories, dict):
|
|
492
|
+
for category_key, trains_list in categories.items():
|
|
493
|
+
if isinstance(trains_list, list):
|
|
494
|
+
for train in trains_list:
|
|
495
|
+
train_id = train.get("train_id")
|
|
496
|
+
if train_id:
|
|
497
|
+
train_ids.append(train_id)
|
|
498
|
+
|
|
499
|
+
return train_ids
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_backend_runner_paths():
|
|
503
|
+
"""
|
|
504
|
+
SPEC-TRAIN-VAL-0031: Backend runner paths validation.
|
|
505
|
+
|
|
506
|
+
Given: Train infrastructure
|
|
507
|
+
When: Checking runner file locations
|
|
508
|
+
Then: Runners exist at python/trains/runner.py or python/trains/<train_id>/runner.py
|
|
509
|
+
|
|
510
|
+
Section 9: Backend Runner Paths
|
|
511
|
+
"""
|
|
512
|
+
train_config = get_train_config(REPO_ROOT)
|
|
513
|
+
allowed_paths = train_config.get("backend_runner_paths", [
|
|
514
|
+
"python/trains/runner.py",
|
|
515
|
+
"python/trains/{train_id}/runner.py"
|
|
516
|
+
])
|
|
517
|
+
|
|
518
|
+
# Check for main runner
|
|
519
|
+
main_runner = TRAINS_DIR / "runner.py"
|
|
520
|
+
if not main_runner.exists():
|
|
521
|
+
if should_enforce(TrainSpecPhase.BACKEND_ENFORCEMENT):
|
|
522
|
+
pytest.fail(
|
|
523
|
+
f"Main TrainRunner not found at {main_runner}\n"
|
|
524
|
+
"Expected: python/trains/runner.py"
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
emit_phase_warning(
|
|
528
|
+
"SPEC-TRAIN-VAL-0031",
|
|
529
|
+
f"Main TrainRunner not found at {main_runner}",
|
|
530
|
+
TrainSpecPhase.BACKEND_ENFORCEMENT
|
|
531
|
+
)
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
# Check for train-specific runners if trains have custom runners
|
|
535
|
+
train_ids = _get_all_train_ids()
|
|
536
|
+
custom_runners = []
|
|
537
|
+
|
|
538
|
+
for train_id in train_ids:
|
|
539
|
+
custom_runner = TRAINS_DIR / train_id / "runner.py"
|
|
540
|
+
if custom_runner.exists():
|
|
541
|
+
custom_runners.append((train_id, custom_runner))
|
|
542
|
+
|
|
543
|
+
# Validate custom runners extend base TrainRunner
|
|
544
|
+
for train_id, runner_path in custom_runners:
|
|
545
|
+
with open(runner_path, 'r', encoding='utf-8') as f:
|
|
546
|
+
content = f.read()
|
|
547
|
+
|
|
548
|
+
if "TrainRunner" not in content:
|
|
549
|
+
if should_enforce(TrainSpecPhase.BACKEND_ENFORCEMENT):
|
|
550
|
+
pytest.fail(
|
|
551
|
+
f"Custom runner at {runner_path} does not reference TrainRunner\n"
|
|
552
|
+
"Custom runners should extend or use the base TrainRunner"
|
|
553
|
+
)
|
|
554
|
+
else:
|
|
555
|
+
emit_phase_warning(
|
|
556
|
+
"SPEC-TRAIN-VAL-0031",
|
|
557
|
+
f"Custom runner {train_id}/runner.py should reference TrainRunner",
|
|
558
|
+
TrainSpecPhase.BACKEND_ENFORCEMENT
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def test_frontend_code_allowed_roots():
|
|
563
|
+
"""
|
|
564
|
+
SPEC-TRAIN-VAL-0032: Frontend code in allowed root directories.
|
|
565
|
+
|
|
566
|
+
Given: Frontend (web) code files
|
|
567
|
+
When: Checking file locations
|
|
568
|
+
Then: Code is in allowed roots (web/src/, web/components/, web/pages/)
|
|
569
|
+
|
|
570
|
+
Section 10: Frontend Code Allowed Roots
|
|
571
|
+
"""
|
|
572
|
+
train_config = get_train_config(REPO_ROOT)
|
|
573
|
+
allowed_roots = train_config.get("frontend_allowed_roots", [
|
|
574
|
+
"web/src/",
|
|
575
|
+
"web/components/",
|
|
576
|
+
"web/pages/"
|
|
577
|
+
])
|
|
578
|
+
|
|
579
|
+
web_dir = REPO_ROOT / "web"
|
|
580
|
+
if not web_dir.exists():
|
|
581
|
+
pytest.skip("No web/ directory found")
|
|
582
|
+
|
|
583
|
+
# Find all TypeScript/JavaScript files in web/
|
|
584
|
+
code_files = []
|
|
585
|
+
for pattern in ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]:
|
|
586
|
+
code_files.extend(web_dir.glob(pattern))
|
|
587
|
+
|
|
588
|
+
# Exclude test files, node_modules, and build directories
|
|
589
|
+
code_files = [
|
|
590
|
+
f for f in code_files
|
|
591
|
+
if "node_modules" not in str(f)
|
|
592
|
+
and ".next" not in str(f)
|
|
593
|
+
and "dist" not in str(f)
|
|
594
|
+
and not f.name.endswith(".test.ts")
|
|
595
|
+
and not f.name.endswith(".test.tsx")
|
|
596
|
+
and not f.name.endswith(".spec.ts")
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
violations = []
|
|
600
|
+
for code_file in code_files:
|
|
601
|
+
rel_path = code_file.relative_to(REPO_ROOT)
|
|
602
|
+
in_allowed_root = any(str(rel_path).startswith(root) for root in allowed_roots)
|
|
603
|
+
|
|
604
|
+
# Also allow e2e/ directory for tests (already excluded above but be explicit)
|
|
605
|
+
in_e2e = str(rel_path).startswith("web/e2e/")
|
|
606
|
+
|
|
607
|
+
if not in_allowed_root and not in_e2e:
|
|
608
|
+
# Check if it's a config file at root level (allow those)
|
|
609
|
+
is_config = code_file.parent == web_dir and code_file.suffix in [".js", ".ts"]
|
|
610
|
+
if not is_config:
|
|
611
|
+
violations.append(str(rel_path))
|
|
612
|
+
|
|
613
|
+
if violations and len(violations) > 10:
|
|
614
|
+
if should_enforce(TrainSpecPhase.FULL_ENFORCEMENT):
|
|
615
|
+
pytest.fail(
|
|
616
|
+
f"Frontend code outside allowed roots ({len(violations)} files):\n " +
|
|
617
|
+
"\n ".join(violations[:10]) +
|
|
618
|
+
f"\n ... and {len(violations) - 10} more" +
|
|
619
|
+
f"\n\nAllowed roots: {allowed_roots}"
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
emit_phase_warning(
|
|
623
|
+
"SPEC-TRAIN-VAL-0032",
|
|
624
|
+
f"{len(violations)} frontend files outside allowed roots",
|
|
625
|
+
TrainSpecPhase.FULL_ENFORCEMENT
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_fastapi_template_enforcement():
|
|
630
|
+
"""
|
|
631
|
+
SPEC-TRAIN-VAL-0033: FastAPI template enforcement when configured.
|
|
632
|
+
|
|
633
|
+
Given: .atdd/config.yaml with enforce_fastapi_template=true
|
|
634
|
+
When: Checking API endpoint files
|
|
635
|
+
Then: Endpoints follow FastAPI template conventions
|
|
636
|
+
|
|
637
|
+
Section 11: FastAPI Template Enforcement
|
|
638
|
+
"""
|
|
639
|
+
train_config = get_train_config(REPO_ROOT)
|
|
640
|
+
|
|
641
|
+
if not train_config.get("enforce_fastapi_template", False):
|
|
642
|
+
pytest.skip("FastAPI template enforcement not enabled in config")
|
|
643
|
+
|
|
644
|
+
# Look for FastAPI app files
|
|
645
|
+
python_dir = REPO_ROOT / "python"
|
|
646
|
+
if not python_dir.exists():
|
|
647
|
+
pytest.skip("No python/ directory found")
|
|
648
|
+
|
|
649
|
+
# Find files that define FastAPI apps
|
|
650
|
+
fastapi_files = []
|
|
651
|
+
for py_file in python_dir.rglob("*.py"):
|
|
652
|
+
if "__pycache__" in str(py_file):
|
|
653
|
+
continue
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
with open(py_file, 'r', encoding='utf-8') as f:
|
|
657
|
+
content = f.read()
|
|
658
|
+
|
|
659
|
+
if "FastAPI" in content and ("app = FastAPI" in content or "router = APIRouter" in content):
|
|
660
|
+
fastapi_files.append(py_file)
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
if not fastapi_files:
|
|
665
|
+
pytest.skip("No FastAPI app files found")
|
|
666
|
+
|
|
667
|
+
# Check template conventions
|
|
668
|
+
violations = []
|
|
669
|
+
for api_file in fastapi_files:
|
|
670
|
+
with open(api_file, 'r', encoding='utf-8') as f:
|
|
671
|
+
content = f.read()
|
|
672
|
+
|
|
673
|
+
# Check for required template elements
|
|
674
|
+
required_elements = [
|
|
675
|
+
("response_model", "Endpoints should use response_model parameter"),
|
|
676
|
+
("HTTPException", "Endpoints should use HTTPException for errors"),
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
for element, description in required_elements:
|
|
680
|
+
if element not in content:
|
|
681
|
+
violations.append(f"{api_file.name}: {description}")
|
|
682
|
+
|
|
683
|
+
if violations:
|
|
684
|
+
if should_enforce(TrainSpecPhase.FULL_ENFORCEMENT):
|
|
685
|
+
pytest.fail(
|
|
686
|
+
f"FastAPI template violations:\n " + "\n ".join(violations) +
|
|
687
|
+
"\n\nSee: train.convention.yaml for FastAPI template requirements"
|
|
688
|
+
)
|
|
689
|
+
else:
|
|
690
|
+
for violation in violations:
|
|
691
|
+
emit_phase_warning(
|
|
692
|
+
"SPEC-TRAIN-VAL-0033",
|
|
693
|
+
violation,
|
|
694
|
+
TrainSpecPhase.FULL_ENFORCEMENT
|
|
695
|
+
)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"properties": {
|
|
17
17
|
"train_id": {
|
|
18
18
|
"type": "string",
|
|
19
|
-
"pattern": "
|
|
19
|
+
"pattern": "^\\d{4}-[a-z0-9-]+$",
|
|
20
20
|
"description": "Train identifier with hierarchical numbering: {theme}{category}{variation}-{kebab-case-name} (e.g., 0001-auth-session-standard, 2301-empty-input-file)"
|
|
21
21
|
},
|
|
22
22
|
"title": {
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
"minLength": 10,
|
|
30
30
|
"description": "Concise description of the train's purpose and workflow"
|
|
31
31
|
},
|
|
32
|
+
"path": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Canonical path to the train YAML file relative to repo root"
|
|
35
|
+
},
|
|
36
|
+
"file": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "DEPRECATED: Use 'path' instead. Alias for backward compatibility",
|
|
39
|
+
"deprecated": true
|
|
40
|
+
},
|
|
32
41
|
"themes": {
|
|
33
42
|
"type": "array",
|
|
34
43
|
"description": "Theme categories this train belongs to",
|
|
@@ -49,12 +58,17 @@
|
|
|
49
58
|
]
|
|
50
59
|
}
|
|
51
60
|
},
|
|
61
|
+
"primary_wagon": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
64
|
+
"description": "The primary wagon responsible for this train's core functionality"
|
|
65
|
+
},
|
|
52
66
|
"dependencies": {
|
|
53
67
|
"type": "array",
|
|
54
68
|
"description": "Other trains that must run before this train",
|
|
55
69
|
"items": {
|
|
56
70
|
"type": "string",
|
|
57
|
-
"pattern": "^train
|
|
71
|
+
"pattern": "^train:\\d{4}-[a-z0-9-]+$",
|
|
58
72
|
"description": "Train reference in format: train:{train_id}"
|
|
59
73
|
}
|
|
60
74
|
},
|
|
@@ -68,6 +82,115 @@
|
|
|
68
82
|
"description": "Participant reference: wagon:{wagon-id}, user:{role}, system:{source}"
|
|
69
83
|
}
|
|
70
84
|
},
|
|
85
|
+
"test": {
|
|
86
|
+
"oneOf": [
|
|
87
|
+
{
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Single backend test file path"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "array",
|
|
93
|
+
"items": { "type": "string" },
|
|
94
|
+
"description": "List of backend test file paths"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"backend": {
|
|
100
|
+
"oneOf": [
|
|
101
|
+
{ "type": "string" },
|
|
102
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
103
|
+
],
|
|
104
|
+
"description": "Backend test file path(s)"
|
|
105
|
+
},
|
|
106
|
+
"frontend": {
|
|
107
|
+
"oneOf": [
|
|
108
|
+
{ "type": "string" },
|
|
109
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
110
|
+
],
|
|
111
|
+
"description": "Frontend (web) test file path(s)"
|
|
112
|
+
},
|
|
113
|
+
"frontend_python": {
|
|
114
|
+
"oneOf": [
|
|
115
|
+
{ "type": "string" },
|
|
116
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
117
|
+
],
|
|
118
|
+
"description": "Frontend Python (Streamlit) test file path(s)"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"description": "Test files organized by platform"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"description": "Test file references for this train (string, array, or object with backend/frontend/frontend_python)"
|
|
126
|
+
},
|
|
127
|
+
"code": {
|
|
128
|
+
"oneOf": [
|
|
129
|
+
{
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Single backend implementation file path"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"type": "array",
|
|
135
|
+
"items": { "type": "string" },
|
|
136
|
+
"description": "List of backend implementation file paths"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"backend": {
|
|
142
|
+
"oneOf": [
|
|
143
|
+
{ "type": "string" },
|
|
144
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
145
|
+
],
|
|
146
|
+
"description": "Backend implementation file path(s)"
|
|
147
|
+
},
|
|
148
|
+
"frontend": {
|
|
149
|
+
"oneOf": [
|
|
150
|
+
{ "type": "string" },
|
|
151
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
152
|
+
],
|
|
153
|
+
"description": "Frontend (web) implementation file path(s)"
|
|
154
|
+
},
|
|
155
|
+
"frontend_python": {
|
|
156
|
+
"oneOf": [
|
|
157
|
+
{ "type": "string" },
|
|
158
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
159
|
+
],
|
|
160
|
+
"description": "Frontend Python (Streamlit) implementation file path(s)"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"additionalProperties": false,
|
|
164
|
+
"description": "Implementation files organized by platform"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"description": "Implementation file references for this train (string, array, or object with backend/frontend/frontend_python)"
|
|
168
|
+
},
|
|
169
|
+
"expectations": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"backend": {
|
|
173
|
+
"type": "boolean",
|
|
174
|
+
"description": "Whether backend implementation is expected"
|
|
175
|
+
},
|
|
176
|
+
"frontend": {
|
|
177
|
+
"type": "boolean",
|
|
178
|
+
"description": "Whether frontend (web) implementation is expected"
|
|
179
|
+
},
|
|
180
|
+
"frontend_python": {
|
|
181
|
+
"type": "boolean",
|
|
182
|
+
"description": "Whether frontend Python (Streamlit) implementation is expected"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"additionalProperties": false,
|
|
186
|
+
"description": "Explicit expectations for which platforms this train should be implemented on"
|
|
187
|
+
},
|
|
188
|
+
"status": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"enum": ["planned", "tested", "implemented"],
|
|
191
|
+
"default": "planned",
|
|
192
|
+
"description": "Current implementation status of the train"
|
|
193
|
+
},
|
|
71
194
|
"sequence": {
|
|
72
195
|
"type": "array",
|
|
73
196
|
"description": "Ordered steps, loops, and routes defining the train workflow",
|