claude-dev-env 4.1.0 → 4.3.0
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.
- package/.agents/skills/_shared/pr-loop/scripts/durable_post_lint.py +242 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py +106 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint.py +332 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py +25 -0
- package/.agents/skills/everything-search/SKILL.md +18 -3
- package/.agents/skills/everything-search/scripts/everything_search.py +204 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/__init__.py +1 -0
- package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +172 -0
- package/.agents/skills/issue-tracker/SKILL.md +6 -0
- package/.agents/skills/pull-request/SKILL.md +218 -0
- package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +61 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +276 -0
- package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +49 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +398 -0
- package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
- package/.agents/skills/test-runner/SKILL.md +69 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
- package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/install.mjs +9 -1
- package/bin/install.prune.test.mjs +149 -0
- package/bin/install.test.mjs +24 -7
- package/hooks/blocking/pii_prevention_blocker.py +32 -51
- package/hooks/blocking/test_bash_post_call_dispatcher.py +5 -28
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
- package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
- package/hooks/gh_post_body_texts.py +195 -0
- package/hooks/hooks.json +0 -15
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +5 -16
- package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +7 -20
- package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
- package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
- package/hooks/observability/test_failure_recorder.py +1 -1
- package/hooks/pyproject.toml +1 -1
- package/hooks/test_gh_post_body_texts.py +79 -0
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +31 -20
- package/rules/filesystem-search.md +5 -5
- package/rules/gh-cli-conventions.md +23 -13
- package/rules/git-workflow.md +21 -8
- package/scripts/policy_lint/adapter_configuration.py +53 -12
- package/scripts/repository_checks/config/constants.py +0 -5
- package/scripts/tests/test_adapter_configuration.py +54 -0
- package/hooks/_gh_pr_author_swap_utils.py +0 -1225
- package/hooks/blocking/conventional_pr_title_gate.py +0 -443
- package/hooks/blocking/es_exe_path_rewriter.py +0 -159
- package/hooks/blocking/gh_body_arg_blocker.py +0 -176
- package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
- package/hooks/blocking/gh_pr_author_restore.py +0 -105
- package/hooks/blocking/pr_description_writer_gate.py +0 -256
- package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
- package/hooks/blocking/test_es_exe_path_rewriter.py +0 -447
- package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
- package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
- package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
- package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
- package/hooks/blocking/test_preflight_check.py +0 -343
- package/hooks/blocking/test_test_preflight_check.py +0 -80
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
- package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
- package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
- package/hooks/hooks_constants/path_rewriter_constants.py +0 -13
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
- package/hooks/hooks_constants/test_path_rewriter_constants.py +0 -57
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
- package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
- package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
- package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
- package/hooks/test__gh_pr_author_swap_utils.py +0 -333
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Focused tests for explicit test-runner preflight helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
13
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
15
|
+
|
|
16
|
+
preflight_checks = importlib.import_module("preflight_checks")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_check_server_health_reports_unhealthy_response(
|
|
20
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
21
|
+
) -> None:
|
|
22
|
+
completed_process = subprocess.CompletedProcess(
|
|
23
|
+
args=["curl"], returncode=0, stdout="503", stderr=""
|
|
24
|
+
)
|
|
25
|
+
monkeypatch.setattr(
|
|
26
|
+
preflight_checks.subprocess,
|
|
27
|
+
"run",
|
|
28
|
+
lambda *_arguments, **_keywords: completed_process,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert preflight_checks.check_server_health("http://localhost:3000") == (
|
|
32
|
+
"BLOCKED: Server at http://localhost:3000 is not healthy (HTTP 503). "
|
|
33
|
+
"Fix the server before running tests."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_check_django_database_requires_db_sqlite3(tmp_path: Path) -> None:
|
|
38
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
assert preflight_checks.check_django_database(tmp_path) == (
|
|
41
|
+
f"BLOCKED: No database file (db.sqlite3) found in {tmp_path}. "
|
|
42
|
+
"Run migrations before running tests."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_extract_port_from_url_uses_explicit_or_default_port() -> None:
|
|
47
|
+
assert preflight_checks.extract_port_from_url("http://localhost:9000") == "9000"
|
|
48
|
+
assert preflight_checks.extract_port_from_url("http://localhost") == "8000"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_check_test_db_flag_requires_detected_server_flag(
|
|
52
|
+
tmp_path: Path,
|
|
53
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
54
|
+
) -> None:
|
|
55
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
56
|
+
monkeypatch.setattr(
|
|
57
|
+
preflight_checks,
|
|
58
|
+
"_read_process_listing",
|
|
59
|
+
lambda: "python manage.py runserver",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert preflight_checks.check_test_db_flag("http://localhost:8000", tmp_path) == (
|
|
63
|
+
"BLOCKED: Django server on port 8000 is not running with --test-db. "
|
|
64
|
+
"Restart with: python manage.py runserver --test-db 0.0.0.0:8000"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_check_test_db_flag_ignores_test_database_flag_on_another_port(
|
|
69
|
+
tmp_path: Path,
|
|
70
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
71
|
+
) -> None:
|
|
72
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
73
|
+
monkeypatch.setattr(
|
|
74
|
+
preflight_checks,
|
|
75
|
+
"_read_process_listing",
|
|
76
|
+
lambda: (
|
|
77
|
+
"python manage.py runserver --test-db 9000\n8000 python manage.py runserver"
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert preflight_checks.check_test_db_flag("http://localhost:8000", tmp_path) == (
|
|
82
|
+
"BLOCKED: Django server on port 8000 is not running with --test-db. "
|
|
83
|
+
"Restart with: python manage.py runserver --test-db 0.0.0.0:8000"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_check_test_db_flag_skips_value_taking_runserver_options(
|
|
88
|
+
tmp_path: Path,
|
|
89
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
90
|
+
) -> None:
|
|
91
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
92
|
+
monkeypatch.setattr(
|
|
93
|
+
preflight_checks,
|
|
94
|
+
"_read_process_listing",
|
|
95
|
+
lambda: "python manage.py runserver --settings project.settings 8000",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
assert preflight_checks.check_test_db_flag("http://localhost:8000", tmp_path) == (
|
|
99
|
+
"BLOCKED: Django server on port 8000 is not running with --test-db. "
|
|
100
|
+
"Restart with: python manage.py runserver --test-db 0.0.0.0:8000"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_check_test_db_flag_accepts_value_taking_options_with_test_db(
|
|
105
|
+
tmp_path: Path,
|
|
106
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
107
|
+
) -> None:
|
|
108
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
109
|
+
monkeypatch.setattr(
|
|
110
|
+
preflight_checks,
|
|
111
|
+
"_read_process_listing",
|
|
112
|
+
lambda: "python manage.py runserver --settings project.settings --test-db 8000",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert (
|
|
116
|
+
preflight_checks.check_test_db_flag("http://localhost:8000", tmp_path) is None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_check_runserver_port_conflicts_reports_other_worktree(
|
|
121
|
+
tmp_path: Path,
|
|
122
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
123
|
+
) -> None:
|
|
124
|
+
project_root = tmp_path / "project"
|
|
125
|
+
other_project_root = tmp_path / "other-project"
|
|
126
|
+
project_root.mkdir()
|
|
127
|
+
other_project_root.mkdir()
|
|
128
|
+
monkeypatch.setattr(
|
|
129
|
+
preflight_checks,
|
|
130
|
+
"_get_runserver_processes_on_port",
|
|
131
|
+
lambda _port: [(1, str(project_root)), (2, str(other_project_root))],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert (
|
|
135
|
+
preflight_checks.check_runserver_port_conflicts(
|
|
136
|
+
"http://localhost:8000", project_root
|
|
137
|
+
)
|
|
138
|
+
== f"BLOCKED: Multiple Django runserver processes are bound to port 8000 across worktrees: {other_project_root.resolve()}. Stop stale servers first."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_build_frontend_runs_build_then_collectstatic(
|
|
143
|
+
tmp_path: Path,
|
|
144
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
145
|
+
) -> None:
|
|
146
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
147
|
+
(tmp_path / "frontend").mkdir()
|
|
148
|
+
all_calls: list[tuple[list[str], Path]] = []
|
|
149
|
+
|
|
150
|
+
def run_process(
|
|
151
|
+
all_arguments: list[str], **keyword_arguments: object
|
|
152
|
+
) -> subprocess.CompletedProcess[str]:
|
|
153
|
+
all_calls.append((all_arguments, keyword_arguments["cwd"]))
|
|
154
|
+
return subprocess.CompletedProcess(
|
|
155
|
+
args=all_arguments, returncode=0, stdout="", stderr=""
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
monkeypatch.setattr(preflight_checks.subprocess, "run", run_process)
|
|
159
|
+
|
|
160
|
+
assert preflight_checks.build_frontend(tmp_path) is None
|
|
161
|
+
assert all_calls == [
|
|
162
|
+
(["npm", "run", "build"], tmp_path / "frontend"),
|
|
163
|
+
(["python", "manage.py", "collectstatic", "--noinput"], tmp_path),
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_build_frontend_skips_collectstatic_without_manage_py(
|
|
168
|
+
tmp_path: Path,
|
|
169
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
170
|
+
) -> None:
|
|
171
|
+
(tmp_path / "frontend").mkdir()
|
|
172
|
+
all_calls: list[list[str]] = []
|
|
173
|
+
|
|
174
|
+
def run_process(
|
|
175
|
+
all_arguments: list[str], **_keyword_arguments: object
|
|
176
|
+
) -> subprocess.CompletedProcess[str]:
|
|
177
|
+
all_calls.append(all_arguments)
|
|
178
|
+
return subprocess.CompletedProcess(
|
|
179
|
+
args=all_arguments, returncode=0, stdout="", stderr=""
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
monkeypatch.setattr(preflight_checks.subprocess, "run", run_process)
|
|
183
|
+
|
|
184
|
+
assert preflight_checks.build_frontend(tmp_path) is None
|
|
185
|
+
assert all_calls == [["npm", "run", "build"]]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Focused behavior tests for the explicit test runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
14
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
16
|
+
|
|
17
|
+
run_tests = importlib.import_module("run_tests")
|
|
18
|
+
|
|
19
|
+
FRONTEND_COMMANDS = [
|
|
20
|
+
["ps", "aux"],
|
|
21
|
+
["npm", "run", "build"],
|
|
22
|
+
["python", "manage.py", "collectstatic", "--noinput"],
|
|
23
|
+
[
|
|
24
|
+
"curl",
|
|
25
|
+
"-s",
|
|
26
|
+
"-o",
|
|
27
|
+
"/dev/null",
|
|
28
|
+
"-w",
|
|
29
|
+
"%{http_code}",
|
|
30
|
+
"--max-time",
|
|
31
|
+
"2",
|
|
32
|
+
"http://localhost:3000",
|
|
33
|
+
],
|
|
34
|
+
["playwright", "test"],
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _create_django_project(project_root: Path, *, has_database: bool) -> None:
|
|
39
|
+
project_root.mkdir()
|
|
40
|
+
(project_root / "manage.py").write_text(
|
|
41
|
+
"import sys\n\nif __name__ == '__main__':\n sys.exit(0)\n",
|
|
42
|
+
encoding="utf-8",
|
|
43
|
+
)
|
|
44
|
+
if has_database:
|
|
45
|
+
(project_root / "db.sqlite3").write_bytes(b"database")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _completed_process(
|
|
49
|
+
all_arguments: list[str],
|
|
50
|
+
return_code: int = 0,
|
|
51
|
+
stdout_text: str = "",
|
|
52
|
+
) -> subprocess.CompletedProcess[str]:
|
|
53
|
+
return subprocess.CompletedProcess(
|
|
54
|
+
args=all_arguments,
|
|
55
|
+
returncode=return_code,
|
|
56
|
+
stdout=stdout_text,
|
|
57
|
+
stderr="",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_recording_process(
|
|
62
|
+
all_calls: list[tuple[list[str], dict[str, object]]],
|
|
63
|
+
) -> Callable[..., subprocess.CompletedProcess[str]]:
|
|
64
|
+
def run_process(
|
|
65
|
+
all_arguments: list[str],
|
|
66
|
+
**keyword_arguments: object,
|
|
67
|
+
) -> subprocess.CompletedProcess[str]:
|
|
68
|
+
all_calls.append((all_arguments, keyword_arguments))
|
|
69
|
+
stdout_text = "200" if all_arguments[0] == "curl" else ""
|
|
70
|
+
return _completed_process(all_arguments, stdout_text=stdout_text)
|
|
71
|
+
|
|
72
|
+
return run_process
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_select_runner_recognizes_supported_forms() -> None:
|
|
76
|
+
assert run_tests.select_runner(["pytest", "tests"]).name == "pytest"
|
|
77
|
+
assert run_tests.select_runner(["python", "-m", "pytest"]).name == "pytest"
|
|
78
|
+
assert run_tests.select_runner(["npx", "playwright", "test"]).is_playwright
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_extract_target_url_reads_explicit_base_url() -> None:
|
|
82
|
+
assert (
|
|
83
|
+
run_tests.extract_target_url(
|
|
84
|
+
["playwright", "test", "--base-url", "http://127.0.0.1:9000"], True
|
|
85
|
+
)
|
|
86
|
+
== "http://127.0.0.1:9000"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_run_preflight_skips_plain_pytest_without_django(
|
|
91
|
+
tmp_path: Path,
|
|
92
|
+
) -> None:
|
|
93
|
+
selected_runner = run_tests.select_runner(["python", "-m", "pytest"])
|
|
94
|
+
assert (
|
|
95
|
+
run_tests.run_preflight(selected_runner, tmp_path, ["python", "-m", "pytest"])
|
|
96
|
+
is None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_run_child_process_returns_child_status(
|
|
101
|
+
tmp_path: Path,
|
|
102
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
103
|
+
) -> None:
|
|
104
|
+
child_process = _completed_process(["pytest"], return_code=6)
|
|
105
|
+
monkeypatch.setattr(
|
|
106
|
+
run_tests.subprocess, "run", lambda *_args, **_kwargs: child_process
|
|
107
|
+
)
|
|
108
|
+
assert run_tests.run_child_process(["pytest"], tmp_path) == 6
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_main_runs_plain_pytest_without_django_preflight(
|
|
112
|
+
tmp_path: Path,
|
|
113
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
114
|
+
) -> None:
|
|
115
|
+
child_process = _completed_process(["python", "-m", "pytest", "tests"], 7)
|
|
116
|
+
all_calls: list[dict[str, object]] = []
|
|
117
|
+
|
|
118
|
+
def run_child(
|
|
119
|
+
all_arguments: list[str],
|
|
120
|
+
**keyword_arguments: object,
|
|
121
|
+
) -> subprocess.CompletedProcess[str]:
|
|
122
|
+
all_calls.append({"arguments": all_arguments, **keyword_arguments})
|
|
123
|
+
return child_process
|
|
124
|
+
|
|
125
|
+
monkeypatch.setattr(run_tests.subprocess, "run", run_child)
|
|
126
|
+
exit_code = run_tests.main(
|
|
127
|
+
["--project", str(tmp_path), "--", "python", "-m", "pytest", "tests"]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert exit_code == 7
|
|
131
|
+
assert all_calls == [
|
|
132
|
+
{
|
|
133
|
+
"arguments": ["python", "-m", "pytest", "tests"],
|
|
134
|
+
"cwd": tmp_path.resolve(),
|
|
135
|
+
"check": False,
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_missing_django_database_stops_before_child_launch(
|
|
141
|
+
tmp_path: Path,
|
|
142
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
143
|
+
) -> None:
|
|
144
|
+
project_root = tmp_path / "django-project"
|
|
145
|
+
_create_django_project(project_root, has_database=False)
|
|
146
|
+
all_calls: list[list[str]] = []
|
|
147
|
+
|
|
148
|
+
def record_launch(all_arguments: list[str], **_keyword_arguments: object) -> None:
|
|
149
|
+
all_calls.append(all_arguments)
|
|
150
|
+
|
|
151
|
+
monkeypatch.setattr(run_tests.subprocess, "run", record_launch)
|
|
152
|
+
exit_code = run_tests.main(
|
|
153
|
+
["--project", str(project_root), "--", "python", "-m", "pytest"]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert exit_code != 0
|
|
157
|
+
assert all_calls == []
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_django_pytest_passes_server_check_and_returns_child_status(
|
|
161
|
+
tmp_path: Path,
|
|
162
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
163
|
+
) -> None:
|
|
164
|
+
project_root = tmp_path / "django-project"
|
|
165
|
+
_create_django_project(project_root, has_database=True)
|
|
166
|
+
all_calls: list[tuple[list[str], dict[str, object]]] = []
|
|
167
|
+
|
|
168
|
+
monkeypatch.setattr(
|
|
169
|
+
run_tests.subprocess, "run", _build_recording_process(all_calls)
|
|
170
|
+
)
|
|
171
|
+
exit_code = run_tests.main(
|
|
172
|
+
["--project", str(project_root), "--", "python", "-m", "pytest", "tests"]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
assert exit_code == 0
|
|
176
|
+
assert all_calls[-1][0] == ["python", "-m", "pytest", "tests"]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_playwright_server_failure_stops_before_child_launch(
|
|
180
|
+
tmp_path: Path,
|
|
181
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
182
|
+
) -> None:
|
|
183
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
184
|
+
all_calls: list[list[str]] = []
|
|
185
|
+
|
|
186
|
+
def run_process(
|
|
187
|
+
all_arguments: list[str],
|
|
188
|
+
**_keyword_arguments: object,
|
|
189
|
+
) -> subprocess.CompletedProcess[str]:
|
|
190
|
+
all_calls.append(all_arguments)
|
|
191
|
+
return _completed_process(all_arguments, stdout_text="000")
|
|
192
|
+
|
|
193
|
+
monkeypatch.setattr(run_tests.subprocess, "run", run_process)
|
|
194
|
+
exit_code = run_tests.main(
|
|
195
|
+
[
|
|
196
|
+
"--project",
|
|
197
|
+
str(tmp_path),
|
|
198
|
+
"--",
|
|
199
|
+
"npx",
|
|
200
|
+
"playwright",
|
|
201
|
+
"test",
|
|
202
|
+
"--base-url",
|
|
203
|
+
"http://127.0.0.1:1",
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert exit_code != 0
|
|
208
|
+
assert [each_call[0] for each_call in all_calls] == ["ps", "curl"]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_playwright_missing_test_database_flag_stops_before_health_check(
|
|
212
|
+
tmp_path: Path,
|
|
213
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
214
|
+
) -> None:
|
|
215
|
+
(tmp_path / "manage.py").write_text("", encoding="utf-8")
|
|
216
|
+
process_listing = "python manage.py runserver 8000\n"
|
|
217
|
+
all_calls: list[list[str]] = []
|
|
218
|
+
|
|
219
|
+
def run_process(
|
|
220
|
+
all_arguments: list[str],
|
|
221
|
+
**_keyword_arguments: object,
|
|
222
|
+
) -> subprocess.CompletedProcess[str]:
|
|
223
|
+
all_calls.append(all_arguments)
|
|
224
|
+
return _completed_process(all_arguments, stdout_text=process_listing)
|
|
225
|
+
|
|
226
|
+
monkeypatch.setattr(run_tests.subprocess, "run", run_process)
|
|
227
|
+
exit_code = run_tests.main(
|
|
228
|
+
[
|
|
229
|
+
"--project",
|
|
230
|
+
str(tmp_path),
|
|
231
|
+
"--",
|
|
232
|
+
"playwright",
|
|
233
|
+
"test",
|
|
234
|
+
"--base-url",
|
|
235
|
+
"http://localhost:8000",
|
|
236
|
+
]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
assert exit_code != 0
|
|
240
|
+
assert [each_call[0] for each_call in all_calls] == ["ps"]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_playwright_builds_frontend_and_collects_static_files(
|
|
244
|
+
tmp_path: Path,
|
|
245
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
246
|
+
) -> None:
|
|
247
|
+
project_root = tmp_path / "django-project"
|
|
248
|
+
_create_django_project(project_root, has_database=True)
|
|
249
|
+
(project_root / "frontend").mkdir()
|
|
250
|
+
all_calls: list[tuple[list[str], dict[str, object]]] = []
|
|
251
|
+
monkeypatch.setattr(run_tests, "check_runserver_port_conflicts", lambda *_: None)
|
|
252
|
+
monkeypatch.setattr(
|
|
253
|
+
run_tests.subprocess, "run", _build_recording_process(all_calls)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
exit_code = run_tests.main(
|
|
257
|
+
["--project", str(project_root), "--", "playwright", "test"]
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
assert exit_code == 0
|
|
261
|
+
assert [each_call[0] for each_call in all_calls] == FRONTEND_COMMANDS
|
|
262
|
+
assert all_calls[1][1]["cwd"] == project_root / "frontend"
|
|
263
|
+
assert all_calls[2][1]["cwd"] == project_root.resolve()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_invalid_runner_does_not_launch_a_child(
|
|
267
|
+
tmp_path: Path,
|
|
268
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
269
|
+
) -> None:
|
|
270
|
+
all_calls: list[list[str]] = []
|
|
271
|
+
|
|
272
|
+
def record_launch(all_arguments: list[str], **_keyword_arguments: object) -> None:
|
|
273
|
+
all_calls.append(all_arguments)
|
|
274
|
+
|
|
275
|
+
monkeypatch.setattr(run_tests.subprocess, "run", record_launch)
|
|
276
|
+
exit_code = run_tests.main(
|
|
277
|
+
["--project", str(tmp_path), "--", "python", "-m", "unittest"]
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
assert exit_code == 2
|
|
281
|
+
assert all_calls == []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Constants package for the test-runner command."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration package for the test-runner command."""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Names, messages, and limits used by the test-runner command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
DEFAULT_DJANGO_URL: str = "http://localhost:8000"
|
|
8
|
+
DEFAULT_PLAYWRIGHT_URL: str = "http://localhost:3000"
|
|
9
|
+
DJANGO_DATABASE_FILENAME: str = "db.sqlite3"
|
|
10
|
+
MANAGE_PY_FILENAME: str = "manage.py"
|
|
11
|
+
FRONTEND_DIRECTORY_NAME: str = "frontend"
|
|
12
|
+
PYTEST_RUNNER_NAME: str = "pytest"
|
|
13
|
+
PLAYWRIGHT_RUNNER_NAME: str = "playwright"
|
|
14
|
+
PYTHON_MODULE_FLAG: str = "-m"
|
|
15
|
+
PLAYWRIGHT_TEST_SUBCOMMAND: str = "test"
|
|
16
|
+
COMMAND_SEPARATOR: str = "--"
|
|
17
|
+
PROJECT_FLAG: str = "--project"
|
|
18
|
+
BASE_URL_FLAG: str = "--base-url"
|
|
19
|
+
ALL_EXECUTABLE_SUFFIXES: tuple[str, ...] = (".exe", ".cmd", ".bat")
|
|
20
|
+
ARGUMENT_ASSIGNMENT_SEPARATOR: str = "="
|
|
21
|
+
COMMAND_ARGUMENT_SEPARATOR: str = " "
|
|
22
|
+
DIRECTORY_LIST_SEPARATOR: str = ", "
|
|
23
|
+
PWD_ENVIRONMENT_NAME: str = "PWD"
|
|
24
|
+
PYTHON_EXECUTABLE_PREFIX: str = "python"
|
|
25
|
+
PYTEST_EXECUTABLE_PREFIX: str = "pytest"
|
|
26
|
+
PLAYWRIGHT_EXECUTABLE_NAME: str = "playwright"
|
|
27
|
+
NPX_EXECUTABLE_NAME: str = "npx"
|
|
28
|
+
RUNSERVER_COMMAND_NAME: str = "runserver"
|
|
29
|
+
TEST_DATABASE_FLAG: str = "--test-db"
|
|
30
|
+
ALL_RUNSERVER_OPTIONS_WITH_VALUES: frozenset[str] = frozenset(
|
|
31
|
+
{"--pythonpath", "--settings", "--verbosity"}
|
|
32
|
+
)
|
|
33
|
+
PROCESS_LIST_ARGUMENTS: tuple[str, ...] = ("ps", "aux")
|
|
34
|
+
ALL_PROCESS_INFORMATION_FIELDS: tuple[str, ...] = ("pid", "cmdline", "cwd")
|
|
35
|
+
ALL_FRONTEND_BUILD_ARGUMENTS: tuple[str, ...] = ("npm", "run", "build")
|
|
36
|
+
ALL_COLLECTSTATIC_ARGUMENTS: tuple[str, ...] = (
|
|
37
|
+
"python",
|
|
38
|
+
"manage.py",
|
|
39
|
+
"collectstatic",
|
|
40
|
+
"--noinput",
|
|
41
|
+
)
|
|
42
|
+
CURL_EXECUTABLE_NAME: str = "curl"
|
|
43
|
+
CURL_SILENT_FLAG: str = "-s"
|
|
44
|
+
CURL_OUTPUT_FLAG: str = "-o"
|
|
45
|
+
CURL_WRITE_OUT_FLAG: str = "-w"
|
|
46
|
+
CURL_STATUS_FORMAT: str = "%{http_code}"
|
|
47
|
+
CURL_MAX_TIME_FLAG: str = "--max-time"
|
|
48
|
+
NULL_DEVICE_PATH: str = "/dev/null"
|
|
49
|
+
ALL_LOCAL_SERVER_HOSTS: frozenset[str] = frozenset(
|
|
50
|
+
{"localhost", "127.0.0.1", "0.0.0.0"}
|
|
51
|
+
)
|
|
52
|
+
ALL_BLOCKED_STATUS_CODES: frozenset[int] = frozenset({500, 502, 503, 504})
|
|
53
|
+
SERVER_URL_PATTERN: re.Pattern[str] = re.compile(r"https?://[^\s\"']+")
|
|
54
|
+
ALL_URL_TRAILING_PUNCTUATION: str = ".,;:)]}"
|
|
55
|
+
DEFAULT_DJANGO_PORT: str = "8000"
|
|
56
|
+
PROCESS_CURL_TIMEOUT_SECONDS: int = 2
|
|
57
|
+
HEALTH_CHECK_TIMEOUT_SECONDS: int = 3
|
|
58
|
+
BUILD_TIMEOUT_SECONDS: int = 120
|
|
59
|
+
EXIT_CODE_SUCCESS: int = 0
|
|
60
|
+
EXIT_CODE_FAILURE: int = 1
|
|
61
|
+
EXIT_CODE_INVALID_ARGUMENTS: int = 2
|
|
62
|
+
MINIMUM_PLAYWRIGHT_ARGUMENT_COUNT: int = 2
|
|
63
|
+
MINIMUM_NPX_ARGUMENT_COUNT: int = 3
|
|
64
|
+
MINIMUM_PROCESS_ARGUMENT_COUNT: int = 3
|
|
65
|
+
MINIMUM_RUNSERVER_PROCESS_COUNT: int = 2
|
|
66
|
+
RUNSERVER_COMMAND_INDEX: int = 2
|
|
67
|
+
PLAYWRIGHT_TEST_COMMAND_INDEX: int = 2
|
|
68
|
+
ERROR_COMMAND_SEPARATOR_REQUIRED: str = "the child command must follow --"
|
|
69
|
+
ERROR_CHILD_COMMAND_REQUIRED: str = "a supported child test command is required"
|
|
70
|
+
ERROR_UNSUPPORTED_RUNNER: str = "only pytest and playwright test commands are supported"
|
|
71
|
+
ERROR_PREFLIGHT_FAILURE_TEMPLATE: str = "test preflight failed: {}"
|
|
72
|
+
ERROR_PROJECT_NOT_DIRECTORY_TEMPLATE: str = "project is not a directory: {}"
|
|
73
|
+
ERROR_CHILD_LAUNCH_TEMPLATE: str = "test child could not start: {}"
|
|
74
|
+
HEALTH_CHECK_ERROR_TEMPLATE: str = (
|
|
75
|
+
"BLOCKED: Server at {} is not healthy ({}). Fix the server before running tests."
|
|
76
|
+
)
|
|
77
|
+
UNREACHABLE_ERROR_TEMPLATE: str = (
|
|
78
|
+
"BLOCKED: Server at {} is unreachable. Start the server before running tests."
|
|
79
|
+
)
|
|
80
|
+
MISSING_DATABASE_ERROR_TEMPLATE: str = (
|
|
81
|
+
"BLOCKED: No database file ({}) found in {}. Run migrations before running tests."
|
|
82
|
+
)
|
|
83
|
+
FRONTEND_BUILD_FAILED_MESSAGE: str = (
|
|
84
|
+
"BLOCKED: Frontend build failed. Fix build errors before running e2e tests."
|
|
85
|
+
)
|
|
86
|
+
MISSING_TEST_DATABASE_FLAG_TEMPLATE: str = (
|
|
87
|
+
"BLOCKED: Django server on port {} is not running with --test-db. Restart "
|
|
88
|
+
"with: python manage.py runserver --test-db 0.0.0.0:{}"
|
|
89
|
+
)
|
|
90
|
+
PORT_CONFLICT_ERROR_TEMPLATE: str = (
|
|
91
|
+
"BLOCKED: Multiple Django runserver processes are bound to port {} across "
|
|
92
|
+
"worktrees: {}. Stop stale servers first."
|
|
93
|
+
)
|
|
94
|
+
PROCESS_LIST_RUNSERVER_TOKEN: str = "runserver"
|
|
95
|
+
PROCESS_LIST_SELF_FILTER_TOKEN: str = "grep"
|
|
96
|
+
UTF8_ENCODING: str = "utf-8"
|
|
@@ -26,7 +26,7 @@ Author-keyed logic accounts for the split: a self-PR check keys on the login tha
|
|
|
26
26
|
|
|
27
27
|
## Hooks that still gate a cloud run
|
|
28
28
|
|
|
29
|
-
The commit and push gates read git commands, which a cloud run still issues through Bash, so they fire normally and the run follows them: `session_edit_stage_gate`, `block_main_commit`,
|
|
29
|
+
The commit and push gates read git commands, which a cloud run still issues through Bash, so they fire normally and the run follows them: `session_edit_stage_gate`, `block_main_commit`, and the pre-push base-ref check that needs `origin/HEAD` set (SKILL.md Step 2). Run tests through the `test-runner` skill when Django or Playwright readiness checks apply.
|
|
30
30
|
|
|
31
31
|
## Hooks that go quiet on the MCP path
|
|
32
32
|
|
|
@@ -88,6 +88,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
88
88
|
'pr-loop-lifecycle',
|
|
89
89
|
'pr-review-responder',
|
|
90
90
|
'pr-scope-resolve',
|
|
91
|
+
'pull-request',
|
|
91
92
|
'pre-compact',
|
|
92
93
|
'privacy-hygiene',
|
|
93
94
|
'prototype',
|
|
@@ -122,6 +123,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
122
123
|
'syncing-submodules',
|
|
123
124
|
'task-build',
|
|
124
125
|
'team-advisor',
|
|
126
|
+
'test-runner',
|
|
125
127
|
'update',
|
|
126
128
|
'usage-pause',
|
|
127
129
|
'verified-build',
|
|
@@ -28,3 +28,11 @@ test('EVER_SHIPPED_SKILL_NAMES includes every current shipped skill', () => {
|
|
|
28
28
|
|
|
29
29
|
assert.deepEqual(allUncoveredSkillNames, [], 'every current skill must enter the fallback set');
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test('EVER_SHIPPED_SKILL_NAMES retains the explicit test runner', () => {
|
|
33
|
+
assert.ok(EVER_SHIPPED_SKILL_NAMES.has('test-runner'));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('EVER_SHIPPED_SKILL_NAMES includes the pull request skill', () => {
|
|
37
|
+
assert.equal(EVER_SHIPPED_SKILL_NAMES.has('pull-request'), true);
|
|
38
|
+
});
|
package/bin/install.mjs
CHANGED
|
@@ -172,6 +172,7 @@ export const CORE_SKILLS = [
|
|
|
172
172
|
'grok-spawn',
|
|
173
173
|
'pr-small-cl',
|
|
174
174
|
'everything-search',
|
|
175
|
+
'test-runner',
|
|
175
176
|
'privacy-hygiene',
|
|
176
177
|
'issue-tracker',
|
|
177
178
|
'task-build',
|
|
@@ -1181,6 +1182,11 @@ export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
1181
1182
|
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
1182
1183
|
'blocking/open_questions_in_plans_blocker.py',
|
|
1183
1184
|
'blocking/md_to_html_blocker.py',
|
|
1185
|
+
'blocking/gh_body_arg_blocker.py',
|
|
1186
|
+
'blocking/volatile_path_in_post_blocker.py',
|
|
1187
|
+
'blocking/conventional_pr_title_gate.py',
|
|
1188
|
+
'blocking/gh_pr_author_enforcer.py',
|
|
1189
|
+
'blocking/pr_description_writer_gate.py',
|
|
1184
1190
|
'session/untracked_repo_detector.py',
|
|
1185
1191
|
]);
|
|
1186
1192
|
|
|
@@ -1198,11 +1204,13 @@ export const POST_FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
1198
1204
|
'workflow/auto_formatter.py',
|
|
1199
1205
|
'workflow/doc_gist_auto_publish.py',
|
|
1200
1206
|
'workflow/md_to_html_companion.py',
|
|
1207
|
+
'blocking/gh_pr_author_restore.py',
|
|
1201
1208
|
]);
|
|
1202
1209
|
|
|
1203
|
-
/** Hook scripts that still ship but no longer run as registered hooks. */
|
|
1204
1210
|
export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
|
|
1205
1211
|
'session/untracked_repo_detector.py',
|
|
1212
|
+
'session/gh_pr_author_session_cleanup.py',
|
|
1213
|
+
'observability/pr_description_writer_spawn_tracker.py',
|
|
1206
1214
|
]);
|
|
1207
1215
|
|
|
1208
1216
|
/**
|