Skip to content

Commit 70f5a57

Browse files
Enforce planning docs command contract
Refs #213
1 parent 9cfae27 commit 70f5a57

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

scripts/validate_planning_docs.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
]
4747
WORKFLOW_TRIGGER_EVENTS = ("push", "pull_request")
4848
CANONICAL_PLANNING_DOCS_TEST_PATH = "tests/planning_docs/**"
49+
CANONICAL_PLANNING_DOCS_TEST_COMMAND = "uv run --with pytest python -m pytest tests/planning_docs"
4950
STALE_PLANNING_DOCS_TEST_PATH = "tests/planningdocs/**"
51+
STALE_PLANNING_DOCS_TEST_COMMAND_PATH = "tests/planningdocs"
5052
CLEAN_HELP_DESCRIPTION = "Remove root generated search index and backend dist."
5153
STALE_WORK_ENGINE_HELP_PHRASE = "work-engine dist"
5254
TASK_TEMPLATE_SECTIONS = [
@@ -409,7 +411,6 @@ def validate_workflow(repo_root: Path) -> list[str]:
409411
"workflow_dispatch:",
410412
"permissions:",
411413
"contents: read",
412-
"uv run --with pytest python -m pytest tests/planning_docs",
413414
"uv run --with pytest python -m pytest tests/infra",
414415
"npm --prefix backend run validate:docs-links",
415416
]
@@ -420,6 +421,10 @@ def validate_workflow(repo_root: Path) -> list[str]:
420421
if protected_path not in text:
421422
violations.append(f"{_repo_path(repo_root, workflow)}: path filter missing {protected_path}")
422423
workflow_label = _repo_path(repo_root, workflow)
424+
violations.extend(
425+
f"{workflow_label}: {violation}"
426+
for violation in validate_workflow_execution_command(text)
427+
)
423428
violations.extend(
424429
f"{workflow_label}: {violation}"
425430
for violation in validate_workflow_trigger_paths(text)
@@ -444,6 +449,28 @@ def validate_workflow(repo_root: Path) -> list[str]:
444449
return violations
445450

446451

452+
def validate_workflow_execution_command(workflow_text: str) -> list[str]:
453+
"""Require the planning-doc test step to use the exact canonical command."""
454+
run_commands = [
455+
match.group("command").strip()
456+
for match in re.finditer(
457+
r"(?m)^\s*run:\s*(?P<command>\S.*?)\s*$", workflow_text
458+
)
459+
]
460+
violations: list[str] = []
461+
if any(STALE_PLANNING_DOCS_TEST_COMMAND_PATH in command for command in run_commands):
462+
violations.append(
463+
"planning docs test command must not use the retired path "
464+
f"{STALE_PLANNING_DOCS_TEST_COMMAND_PATH}"
465+
)
466+
if CANONICAL_PLANNING_DOCS_TEST_COMMAND not in run_commands:
467+
violations.append(
468+
"planning docs test command must be exactly "
469+
f"{CANONICAL_PLANNING_DOCS_TEST_COMMAND}"
470+
)
471+
return violations
472+
473+
447474
def validate_workflow_trigger_paths(workflow_text: str) -> list[str]:
448475
"""Validate the planning workflow's protected-path contract without YAML dependencies."""
449476
try:

tests/planning_docs/test_validate_planning_docs_workflow_triggers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,35 @@ def test_current_workflow_trigger_paths_are_canonical_and_consistent():
5555
assert validate_planning_docs.validate_workflow_trigger_paths(WORKFLOW_TEXT) == []
5656

5757

58+
def test_current_workflow_execution_command_is_canonical():
59+
assert validate_planning_docs.validate_workflow_execution_command(WORKFLOW_TEXT) == []
60+
61+
62+
@pytest.mark.parametrize(
63+
("replacement", "expected_violation"),
64+
[
65+
(
66+
"uv run --with pytest python -m pytest tests/planningdocs",
67+
"planning docs test command must not use the retired path tests/planningdocs",
68+
),
69+
(
70+
"uv run --with pytest python -m pytest tests/planning_docs_extra",
71+
"planning docs test command must be exactly uv run --with pytest python -m pytest tests/planning_docs",
72+
),
73+
],
74+
)
75+
def test_execution_command_drift_fails_closed(replacement, expected_violation):
76+
modified = WORKFLOW_TEXT.replace(
77+
validate_planning_docs.CANONICAL_PLANNING_DOCS_TEST_COMMAND,
78+
replacement,
79+
1,
80+
)
81+
82+
violations = validate_planning_docs.validate_workflow_execution_command(modified)
83+
84+
assert expected_violation in violations
85+
86+
5887
@pytest.mark.parametrize("event", ["push", "pull_request"])
5988
def test_missing_canonical_path_fails_closed_for_each_event(event):
6089
modified = _remove_event_path(

0 commit comments

Comments
 (0)