claude-dev-env 8.6.0 → 8.7.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/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +4 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +155 -18
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +130 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +232 -90
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +27 -54
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_state_description_blocker.py +59 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +2 -1
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +87 -0
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapter_detectors.py +1 -1
- package/scripts/policy_lint/adapter_support.py +3 -14
- package/scripts/policy_lint/adapters.py +30 -1
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +4 -1
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -291,3 +291,21 @@ def test_is_dedicated_constants_module_true_for_config_directory() -> None:
|
|
|
291
291
|
|
|
292
292
|
def test_is_dedicated_constants_module_false_for_ordinary_module() -> None:
|
|
293
293
|
assert _SHARED_MODULE._is_dedicated_constants_module("app/services/orders.py") is False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.parametrize(
|
|
297
|
+
"normalized_marker_table_name",
|
|
298
|
+
[
|
|
299
|
+
"ALL_AGENT_HOME_TOOLING_PATTERNS",
|
|
300
|
+
"ALL_HOOK_INFRASTRUCTURE_PATTERNS",
|
|
301
|
+
"ALL_WORKFLOW_REGISTRY_PATTERNS",
|
|
302
|
+
"ALL_MIGRATION_PATH_PATTERNS",
|
|
303
|
+
],
|
|
304
|
+
)
|
|
305
|
+
def test_normalized_path_marker_table_carries_no_backslash_entry(
|
|
306
|
+
normalized_marker_table_name: str,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Each table's consumer normalizes the path with .replace("\\\\", "/") but
|
|
309
|
+
not the marker, so a backslash entry can never match."""
|
|
310
|
+
marker_table = getattr(_SHARED_MODULE, normalized_marker_table_name)
|
|
311
|
+
assert all("\\" not in each_marker for each_marker in marker_table)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Tests for the shared fixtures this directory's ``conftest.py`` provides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
from code_rules_shared import is_hook_infrastructure
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def should_yield_a_directory_a_target_inside_it_routes_as_hook_infrastructure(
|
|
11
|
+
hook_blocking_dir: pathlib.Path,
|
|
12
|
+
) -> None:
|
|
13
|
+
target_file = hook_blocking_dir / "new_blocker.py"
|
|
14
|
+
assert is_hook_infrastructure(str(target_file)) is True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def should_yield_a_directory_that_exists_on_disk(
|
|
18
|
+
hook_blocking_dir: pathlib.Path,
|
|
19
|
+
) -> None:
|
|
20
|
+
assert hook_blocking_dir.is_dir()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def should_keep_the_hook_blocking_dir_explanation_only_on_the_fixture() -> None:
|
|
24
|
+
directory = pathlib.Path(__file__).parent
|
|
25
|
+
exempt_paths = {directory / "conftest.py", pathlib.Path(__file__)}
|
|
26
|
+
explanation_fragment = "tail mirrors a production hook directory"
|
|
27
|
+
offending_files = [
|
|
28
|
+
path.name
|
|
29
|
+
for path in sorted(directory.glob("*.py"))
|
|
30
|
+
if path not in exempt_paths and explanation_fragment in path.read_text(encoding="utf-8")
|
|
31
|
+
]
|
|
32
|
+
assert offending_files == [], (
|
|
33
|
+
"the hook_blocking_dir fixture's explanation belongs on conftest.py alone, "
|
|
34
|
+
f"got it repeated in: {offending_files}"
|
|
35
|
+
)
|
|
@@ -26,6 +26,7 @@ if _HOOKS_ROOT not in sys.path:
|
|
|
26
26
|
from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
|
|
27
27
|
from state_description_blocker import ( # noqa: E402
|
|
28
28
|
build_deny_payload,
|
|
29
|
+
find_violations_with_lines,
|
|
29
30
|
)
|
|
30
31
|
from state_description_blocker import evaluate as _evaluate_without_opt_in # noqa: E402
|
|
31
32
|
|
|
@@ -33,6 +34,11 @@ from code_rules_enforcer_test_support import (
|
|
|
33
34
|
repository_file_path,
|
|
34
35
|
repository_root,
|
|
35
36
|
)
|
|
37
|
+
from hooks_constants.code_rules_enforcer_constants import (
|
|
38
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER,
|
|
39
|
+
JAVASCRIPT_BLOCK_COMMENT_OPENER,
|
|
40
|
+
)
|
|
41
|
+
import state_description_blocker as _state_description_blocker_module
|
|
36
42
|
|
|
37
43
|
HOOK_SCRIPT_PATH = os.path.join(
|
|
38
44
|
os.path.dirname(__file__), "state_description_blocker.py"
|
|
@@ -952,3 +958,56 @@ def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
|
|
|
952
958
|
logged_record = json.loads(all_records[0])
|
|
953
959
|
assert logged_record["hook"] == "state_description_blocker.py"
|
|
954
960
|
assert logged_record["event"] == "PreToolUse"
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def test_find_violations_with_lines_names_the_docstring_line_not_the_fixture_line() -> None:
|
|
964
|
+
module_source = (
|
|
965
|
+
'fixture_text = "the field was previously required"\n'
|
|
966
|
+
"\n"
|
|
967
|
+
"\n"
|
|
968
|
+
"def read_field():\n"
|
|
969
|
+
' """Read the field.\n'
|
|
970
|
+
"\n"
|
|
971
|
+
" The field was previously required.\n"
|
|
972
|
+
' """\n'
|
|
973
|
+
" return 1\n"
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
all_violations = find_violations_with_lines(module_source, "src/module.py")
|
|
977
|
+
|
|
978
|
+
assert all_violations == [("previously", 7), ("was previously", 7)]
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def test_find_violations_with_lines_reports_first_matching_comment_line() -> None:
|
|
982
|
+
module_source = "x = 1\n# no longer needed\ny = 2\n# no longer used\n"
|
|
983
|
+
|
|
984
|
+
all_violations = find_violations_with_lines(module_source, "src/module.py")
|
|
985
|
+
|
|
986
|
+
assert all_violations == [("no longer", 2)]
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def test_find_violations_with_lines_reports_markdown_line_past_a_code_fence() -> None:
|
|
990
|
+
markdown_source = (
|
|
991
|
+
"# Title\n"
|
|
992
|
+
"\n"
|
|
993
|
+
"```\n"
|
|
994
|
+
"used to fail\n"
|
|
995
|
+
"```\n"
|
|
996
|
+
"\n"
|
|
997
|
+
"The API was previously public.\n"
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
all_violations = find_violations_with_lines(markdown_source, "docs/api.md")
|
|
1001
|
+
|
|
1002
|
+
assert all_violations == [("previously", 7), ("was previously", 7)]
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def test_block_comment_markers_are_the_javascript_constants() -> None:
|
|
1006
|
+
assert (
|
|
1007
|
+
_state_description_blocker_module.JAVASCRIPT_BLOCK_COMMENT_OPENER
|
|
1008
|
+
is JAVASCRIPT_BLOCK_COMMENT_OPENER
|
|
1009
|
+
)
|
|
1010
|
+
assert (
|
|
1011
|
+
_state_description_blocker_module.JAVASCRIPT_BLOCK_COMMENT_CLOSER
|
|
1012
|
+
is JAVASCRIPT_BLOCK_COMMENT_CLOSER
|
|
1013
|
+
)
|
|
@@ -47,20 +47,7 @@ LOCAL_SHA_FIELD_INDEX: int = 1
|
|
|
47
47
|
NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
|
|
48
48
|
"claude-dev-env pre-push: no parseable stdin lines; aborting"
|
|
49
49
|
)
|
|
50
|
-
NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
|
|
51
|
-
UNRESOLVABLE_MERGE_BASE_SENTINEL: str = "__unresolvable_merge_base__"
|
|
52
|
-
UNRESOLVABLE_MERGE_BASE_MESSAGE: str = (
|
|
53
|
-
"claude-dev-env pre-push: CODE_RULES validation is pending because the pushed "
|
|
54
|
-
"object and default branch have no shared merge base. Restore shared history "
|
|
55
|
-
"or provide a reachable default branch, then run the push again."
|
|
56
|
-
)
|
|
57
50
|
LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
|
|
58
|
-
ORIGIN_HEAD_SYMBOLIC_REFERENCE: str = "refs/remotes/origin/HEAD"
|
|
59
|
-
ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX: str = "refs/remotes/origin/"
|
|
60
|
-
ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES: tuple[str, ...] = (
|
|
61
|
-
"refs/remotes/origin/main",
|
|
62
|
-
"refs/remotes/origin/master",
|
|
63
|
-
)
|
|
64
51
|
REMOTE_BRANCH_REFERENCE_TEMPLATE: str = "refs/remotes/{remote}/{branch}"
|
|
65
52
|
ALL_GIT_SYMBOLIC_REFERENCE_COMMAND_PREFIX: tuple[str, ...] = (
|
|
66
53
|
"git",
|
|
@@ -99,7 +86,7 @@ GIT_SYMBOLIC_REFERENCE_SUBCOMMAND: str = "symbolic-ref"
|
|
|
99
86
|
GIT_COMMAND_TIMEOUT_SECONDS: int = 30
|
|
100
87
|
GH_EXECUTABLE_NAME: str = "gh"
|
|
101
88
|
GH_PR_VIEW_TIMEOUT_SECONDS: int = 5
|
|
102
|
-
|
|
89
|
+
ALL_GH_PR_VIEW_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "url", "--jq", ".url")
|
|
103
90
|
GIT_FOR_EACH_REF_SUBCOMMAND: str = "for-each-ref"
|
|
104
91
|
GIT_REFERENCE_SHORT_NAME_FORMAT_ARGUMENT: str = "--format=%(refname:short)"
|
|
105
92
|
COMMIT_OBJECT_NAME_SUFFIX: str = "^{commit}"
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from git_hooks_constants import (
|
|
7
7
|
GH_EXECUTABLE_NAME,
|
|
8
|
-
|
|
8
|
+
ALL_GH_PR_VIEW_ARGUMENTS,
|
|
9
9
|
GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
10
10
|
)
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ from subprocess_window_access import hidden_window_creation_flags
|
|
|
15
15
|
def get_pull_request_url(repo_dir: Path) -> str | None:
|
|
16
16
|
try:
|
|
17
17
|
result = subprocess.run(
|
|
18
|
-
[GH_EXECUTABLE_NAME, *
|
|
18
|
+
[GH_EXECUTABLE_NAME, *ALL_GH_PR_VIEW_ARGUMENTS],
|
|
19
19
|
cwd=repo_dir,
|
|
20
20
|
check=False,
|
|
21
21
|
capture_output=True,
|
|
@@ -11,6 +11,13 @@ def test_no_parseable_stdin_lines_message_exists_and_describes_problem() -> None
|
|
|
11
11
|
assert "no parseable stdin lines" in git_hooks_constants.NO_PARSEABLE_STDIN_LINES_MESSAGE
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
def test_pre_push_cut_leftovers_are_removed() -> None:
|
|
15
|
+
for removed_name in (
|
|
16
|
+
"ALL_DEFAULT_BRANCH_FALLBACK_REFERENCES",
|
|
17
|
+
"ORIGIN_HEAD_SYMBOLIC_REFERENCE",
|
|
18
|
+
"ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX",
|
|
19
|
+
"UNRESOLVABLE_MERGE_BASE_SENTINEL",
|
|
20
|
+
"UNRESOLVABLE_MERGE_BASE_MESSAGE",
|
|
21
|
+
"NO_PARSEABLE_STDIN_LINES_SENTINEL",
|
|
22
|
+
):
|
|
23
|
+
assert not hasattr(git_hooks_constants, removed_name)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Tests for the pull-request URL query the commit handoff runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import pull_request_handoff
|
|
11
|
+
from git_hooks_constants import (
|
|
12
|
+
ALL_GH_PR_VIEW_ARGUMENTS,
|
|
13
|
+
GH_EXECUTABLE_NAME,
|
|
14
|
+
GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_should_query_the_url_through_the_named_executable_and_arguments(
|
|
19
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
20
|
+
) -> None:
|
|
21
|
+
all_calls: list[tuple[list[str], object]] = []
|
|
22
|
+
|
|
23
|
+
def record_call(
|
|
24
|
+
command: list[str], **keyword_arguments: object
|
|
25
|
+
) -> subprocess.CompletedProcess[str]:
|
|
26
|
+
all_calls.append((command, keyword_arguments["timeout"]))
|
|
27
|
+
return subprocess.CompletedProcess(command, 0, "https://example/pull/1\n", "")
|
|
28
|
+
|
|
29
|
+
monkeypatch.setattr(pull_request_handoff.subprocess, "run", record_call)
|
|
30
|
+
|
|
31
|
+
url = pull_request_handoff.get_pull_request_url(Path("/repo"))
|
|
32
|
+
|
|
33
|
+
assert url == "https://example/pull/1"
|
|
34
|
+
assert all_calls == [
|
|
35
|
+
([GH_EXECUTABLE_NAME, *ALL_GH_PR_VIEW_ARGUMENTS], GH_PR_VIEW_TIMEOUT_SECONDS)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_report_no_url_when_the_query_exits_non_zero(
|
|
40
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
41
|
+
) -> None:
|
|
42
|
+
def failing_call(
|
|
43
|
+
command: list[str], **keyword_arguments: object
|
|
44
|
+
) -> subprocess.CompletedProcess[str]:
|
|
45
|
+
return subprocess.CompletedProcess(command, 1, "", "no pull request")
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr(pull_request_handoff.subprocess, "run", failing_call)
|
|
48
|
+
|
|
49
|
+
assert pull_request_handoff.get_pull_request_url(Path("/repo")) is None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Tests for the base-reference construction in ``_base_matches_current``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import verification_notice
|
|
10
|
+
import verification_notice_state
|
|
11
|
+
from git_hooks_constants import COMMIT_OBJECT_NAME_SUFFIX
|
|
12
|
+
from git_hooks_constants.verification_notice_constants import (
|
|
13
|
+
ALL_GIT_BASE_QUERY_PREFIX,
|
|
14
|
+
BASE_REFERENCE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_base_matches_current_queries_base_reference_plus_commit_suffix() -> None:
|
|
19
|
+
captured_queries: list[tuple[str, ...]] = []
|
|
20
|
+
|
|
21
|
+
def _fake_git_query(
|
|
22
|
+
repository_root: Path, arguments: tuple[str, ...], capture_stderr: bool
|
|
23
|
+
) -> str | None:
|
|
24
|
+
captured_queries.append(arguments)
|
|
25
|
+
return "deadbeef"
|
|
26
|
+
|
|
27
|
+
verification_notice_state._base_matches_current(Path("/repo"), "deadbeef", _fake_git_query)
|
|
28
|
+
|
|
29
|
+
assert captured_queries == [
|
|
30
|
+
(*ALL_GIT_BASE_QUERY_PREFIX, BASE_REFERENCE + COMMIT_OBJECT_NAME_SUFFIX)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_base_matches_current_derives_its_query_from_the_base_reference_constant(
|
|
35
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
36
|
+
) -> None:
|
|
37
|
+
monkeypatch.setattr(verification_notice_state, "BASE_REFERENCE", "custom/base")
|
|
38
|
+
captured_queries: list[tuple[str, ...]] = []
|
|
39
|
+
|
|
40
|
+
def _fake_git_query(
|
|
41
|
+
repository_root: Path, arguments: tuple[str, ...], capture_stderr: bool
|
|
42
|
+
) -> str | None:
|
|
43
|
+
captured_queries.append(arguments)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
verification_notice_state._base_matches_current(Path("/repo"), "deadbeef", _fake_git_query)
|
|
47
|
+
|
|
48
|
+
assert captured_queries == [
|
|
49
|
+
(*ALL_GIT_BASE_QUERY_PREFIX, "custom/base" + COMMIT_OBJECT_NAME_SUFFIX)
|
|
50
|
+
]
|
|
@@ -6,8 +6,10 @@ from collections.abc import Callable, Mapping
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Protocol
|
|
8
8
|
|
|
9
|
+
from git_hooks_constants import COMMIT_OBJECT_NAME_SUFFIX
|
|
9
10
|
from git_hooks_constants.verification_notice_constants import (
|
|
10
11
|
ALL_GIT_BASE_QUERY_PREFIX,
|
|
12
|
+
BASE_REFERENCE,
|
|
11
13
|
GIT_COMMAND_SUCCESS_EXIT_CODE,
|
|
12
14
|
HEX_DIGITS,
|
|
13
15
|
NOTICE_FAILED_LINE,
|
|
@@ -180,7 +182,7 @@ def _base_matches_current(
|
|
|
180
182
|
) -> bool:
|
|
181
183
|
current_base_revision = run_git_query(
|
|
182
184
|
repository_root,
|
|
183
|
-
(*ALL_GIT_BASE_QUERY_PREFIX,
|
|
185
|
+
(*ALL_GIT_BASE_QUERY_PREFIX, BASE_REFERENCE + COMMIT_OBJECT_NAME_SUFFIX),
|
|
184
186
|
False,
|
|
185
187
|
)
|
|
186
188
|
return current_base_revision == base_revision
|
|
@@ -38,10 +38,13 @@ CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY: str = "tmp"
|
|
|
38
38
|
EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME: str = "CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT"
|
|
39
39
|
ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
40
40
|
LEADING_DRIVE_LETTER_PATTERN: re.Pattern[str] = re.compile(r"^[a-z]:")
|
|
41
|
-
ALL_AGENT_HOME_TOOLING_PATTERNS = {"/.grok/"
|
|
42
|
-
ALL_HOOK_INFRASTRUCTURE_PATTERNS = {
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
ALL_AGENT_HOME_TOOLING_PATTERNS = {"/.grok/"}
|
|
42
|
+
ALL_HOOK_INFRASTRUCTURE_PATTERNS = {
|
|
43
|
+
"/.claude/hooks/",
|
|
44
|
+
"/packages/claude-dev-env/hooks/",
|
|
45
|
+
} | ALL_AGENT_HOME_TOOLING_PATTERNS
|
|
46
|
+
ALL_WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "_tab.py", "/states.py", "/modules.py"}
|
|
47
|
+
ALL_MIGRATION_PATH_PATTERNS = {"/migrations/"}
|
|
45
48
|
|
|
46
49
|
CONSTANTS_MODULE_SUFFIX: str = "_constants.py"
|
|
47
50
|
CONFIG_DIRECTORY_SEGMENT: str = "config"
|
|
@@ -192,7 +195,12 @@ ALL_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({
|
|
|
192
195
|
})
|
|
193
196
|
ALL_SUBSCRIPT_ONLY_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({"dict"})
|
|
194
197
|
COLLECTION_BY_NAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9]*_by_[a-z][a-z0-9_]*$")
|
|
195
|
-
ALL_CLI_FILE_PATH_MARKERS: tuple[str, ...] = (
|
|
198
|
+
ALL_CLI_FILE_PATH_MARKERS: tuple[str, ...] = (
|
|
199
|
+
"/scripts/",
|
|
200
|
+
"/diagnostics/",
|
|
201
|
+
"_cli.py",
|
|
202
|
+
"/cli.py",
|
|
203
|
+
)
|
|
196
204
|
|
|
197
205
|
LOGGING_FSTRING_PATTERN = re.compile(
|
|
198
206
|
r'\b(?:log_(?:debug|info|warning|error|critical|exception)'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Constants for the session-edit stage gate hook trio.
|
|
2
2
|
|
|
3
3
|
Shared by the PostToolUse tracker
|
|
4
|
-
(``hooks/observability/session_file_edit_tracker.py``)
|
|
5
|
-
(``hooks/
|
|
6
|
-
|
|
4
|
+
(``hooks/observability/session_file_edit_tracker.py``) and the SessionStart
|
|
5
|
+
cleanup (``hooks/session/session_edit_tracker_cleanup.py``): the per-session
|
|
6
|
+
tracker
|
|
7
7
|
filename shape and JSON payload key, the session-id sanitize pattern, the
|
|
8
8
|
SessionStart source key and the fresh-startup source value the cleanup keys its
|
|
9
9
|
deletion on, the edit-tool name set, the per-session lock filename suffix and
|
|
@@ -62,7 +62,6 @@ ALL_TRACKED_UNSTAGED_FILES_COMMAND: tuple[str, ...] = (
|
|
|
62
62
|
|
|
63
63
|
PARTIAL_COMMIT_BYPASS_MARKER: str = "# partial-commit"
|
|
64
64
|
COMMIT_SUBCOMMAND_TOKEN: str = "commit"
|
|
65
|
-
GIT_EXECUTABLE_TOKEN: str = "git"
|
|
66
65
|
ENV_ASSIGNMENT_PREFIX_PATTERN: re.Pattern[str] = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
67
66
|
ALL_STAGING_SUBCOMMAND_TOKENS: frozenset[str] = frozenset({"add", "stage"})
|
|
68
67
|
ALL_STAGE_ALL_ADD_FLAG_TOKENS: frozenset[str] = frozenset({"-A", "--all", "-u", "--update"})
|
|
@@ -40,12 +40,13 @@ ALL_COMMENT_TRANSITION_PATTERNS: list[Pattern[str]] = [
|
|
|
40
40
|
compile(r"\bas of\b", IGNORECASE),
|
|
41
41
|
]
|
|
42
42
|
|
|
43
|
-
CODE_FENCE_PATTERN: Pattern[str] = compile(r"```[\s\S]*?```")
|
|
44
43
|
INLINE_CODE_PATTERN: Pattern[str] = compile(r"``[^`]+``|`[^`]+`")
|
|
45
44
|
DOUBLE_QUOTED_SPAN_PATTERN: Pattern[str] = compile(r'"[^"\n]*"')
|
|
46
45
|
TRIPLE_QUOTED_BLOCK_PATTERN: Pattern[str] = compile(
|
|
47
46
|
r'"""([\s\S]*?)"""|\'\'\'([\s\S]*?)\'\'\''
|
|
48
47
|
)
|
|
48
|
+
DOUBLE_QUOTE_BODY_GROUP: int = 1
|
|
49
|
+
SINGLE_QUOTE_BODY_GROUP: int = 2
|
|
49
50
|
|
|
50
51
|
PYTHON_EXTENSION: str = ".py"
|
|
51
52
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Behavior tests for state_description_blocker configuration constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
9
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
11
|
+
|
|
12
|
+
from blocking.state_description_blocker import _extract_comment_lines
|
|
13
|
+
from hooks_constants.code_rules_enforcer_constants import (
|
|
14
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER,
|
|
15
|
+
JAVASCRIPT_BLOCK_COMMENT_OPENER,
|
|
16
|
+
)
|
|
17
|
+
from hooks_constants.state_description_blocker_constants import (
|
|
18
|
+
ALL_BLOCK_COMMENT_EXTENSIONS,
|
|
19
|
+
ALL_COMMENT_TRANSITION_PATTERNS,
|
|
20
|
+
ALL_HASH_ONLY_EXTENSIONS,
|
|
21
|
+
ALL_MARKDOWN_EXTENSIONS,
|
|
22
|
+
DOUBLE_QUOTE_BODY_GROUP,
|
|
23
|
+
SINGLE_QUOTE_BODY_GROUP,
|
|
24
|
+
TRIPLE_QUOTED_BLOCK_PATTERN,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_transition_patterns_match_their_own_phrase() -> None:
|
|
29
|
+
all_phrases = [
|
|
30
|
+
"instead of",
|
|
31
|
+
"previously",
|
|
32
|
+
"was previously",
|
|
33
|
+
"used to",
|
|
34
|
+
"no longer",
|
|
35
|
+
"switched to",
|
|
36
|
+
]
|
|
37
|
+
for each_phrase in all_phrases:
|
|
38
|
+
sentence = f"The field is {each_phrase} required."
|
|
39
|
+
assert any(
|
|
40
|
+
each_pattern.search(sentence) for each_pattern in ALL_COMMENT_TRANSITION_PATTERNS
|
|
41
|
+
), each_phrase
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_transition_patterns_do_not_match_unrelated_prose() -> None:
|
|
45
|
+
sentence = "The path selects the scan strategy for the file."
|
|
46
|
+
assert not any(
|
|
47
|
+
each_pattern.search(sentence) for each_pattern in ALL_COMMENT_TRANSITION_PATTERNS
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_block_comment_markers_close_a_single_line_block_comment() -> None:
|
|
52
|
+
text = f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} note {JAVASCRIPT_BLOCK_COMMENT_CLOSER}\ncode();"
|
|
53
|
+
all_comment_lines = _extract_comment_lines(text, ".js")
|
|
54
|
+
assert all_comment_lines == [
|
|
55
|
+
(1, f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} note {JAVASCRIPT_BLOCK_COMMENT_CLOSER}")
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_block_comment_markers_carry_an_unclosed_comment_to_the_next_line() -> None:
|
|
60
|
+
text = f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} still open\nclosed here {JAVASCRIPT_BLOCK_COMMENT_CLOSER}"
|
|
61
|
+
all_comment_lines = _extract_comment_lines(text, ".js")
|
|
62
|
+
assert all_comment_lines == [
|
|
63
|
+
(1, f"{JAVASCRIPT_BLOCK_COMMENT_OPENER} still open"),
|
|
64
|
+
(2, f"closed here {JAVASCRIPT_BLOCK_COMMENT_CLOSER}"),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_double_and_single_quote_body_groups_index_the_matching_quote_style() -> None:
|
|
69
|
+
double_quoted_match = TRIPLE_QUOTED_BLOCK_PATTERN.search('"""double body"""')
|
|
70
|
+
assert double_quoted_match is not None
|
|
71
|
+
assert double_quoted_match.group(DOUBLE_QUOTE_BODY_GROUP) == "double body"
|
|
72
|
+
assert double_quoted_match.group(SINGLE_QUOTE_BODY_GROUP) is None
|
|
73
|
+
|
|
74
|
+
single_quoted_match = TRIPLE_QUOTED_BLOCK_PATTERN.search("'''single body'''")
|
|
75
|
+
assert single_quoted_match is not None
|
|
76
|
+
assert single_quoted_match.group(SINGLE_QUOTE_BODY_GROUP) == "single body"
|
|
77
|
+
assert single_quoted_match.group(DOUBLE_QUOTE_BODY_GROUP) is None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_extension_sets_are_disjoint_from_markdown_extensions() -> None:
|
|
81
|
+
assert not (ALL_HASH_ONLY_EXTENSIONS & ALL_MARKDOWN_EXTENSIONS)
|
|
82
|
+
assert not (ALL_BLOCK_COMMENT_EXTENSIONS & ALL_MARKDOWN_EXTENSIONS)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_python_is_hash_only_and_never_block_comment() -> None:
|
|
86
|
+
assert ".py" in ALL_HASH_ONLY_EXTENSIONS
|
|
87
|
+
assert ".py" not in ALL_BLOCK_COMMENT_EXTENSIONS
|
|
@@ -48,27 +48,21 @@ TEST_PATH_PATTERNS: frozenset[str] = frozenset(
|
|
|
48
48
|
HOOK_INFRASTRUCTURE_PATTERNS: frozenset[str] = frozenset(
|
|
49
49
|
{
|
|
50
50
|
"/.claude/hooks/",
|
|
51
|
-
"\\.claude\\hooks\\",
|
|
52
|
-
"\\.claude/hooks/",
|
|
53
51
|
}
|
|
54
52
|
)
|
|
55
53
|
|
|
56
54
|
WORKFLOW_REGISTRY_PATTERNS: frozenset[str] = frozenset(
|
|
57
55
|
{
|
|
58
56
|
"/workflow/",
|
|
59
|
-
"\\workflow\\",
|
|
60
57
|
"_tab.py",
|
|
61
58
|
"/states.py",
|
|
62
|
-
"\\states.py",
|
|
63
59
|
"/modules.py",
|
|
64
|
-
"\\modules.py",
|
|
65
60
|
}
|
|
66
61
|
)
|
|
67
62
|
|
|
68
63
|
MIGRATION_PATH_PATTERNS: frozenset[str] = frozenset(
|
|
69
64
|
{
|
|
70
65
|
"/migrations/",
|
|
71
|
-
"\\migrations\\",
|
|
72
66
|
}
|
|
73
67
|
)
|
|
74
68
|
|
|
@@ -80,23 +74,14 @@ def is_test_file(file_path: str) -> bool:
|
|
|
80
74
|
|
|
81
75
|
def is_hook_infrastructure(file_path: str) -> bool:
|
|
82
76
|
path_normalized = file_path.lower().replace("\\", "/")
|
|
83
|
-
return any(
|
|
84
|
-
pattern.replace("\\", "/") in path_normalized
|
|
85
|
-
for pattern in HOOK_INFRASTRUCTURE_PATTERNS
|
|
86
|
-
)
|
|
77
|
+
return any(pattern in path_normalized for pattern in HOOK_INFRASTRUCTURE_PATTERNS)
|
|
87
78
|
|
|
88
79
|
|
|
89
80
|
def is_workflow_registry_file(file_path: str) -> bool:
|
|
90
81
|
path_normalized = file_path.lower().replace("\\", "/")
|
|
91
|
-
return any(
|
|
92
|
-
pattern.replace("\\", "/") in path_normalized
|
|
93
|
-
for pattern in WORKFLOW_REGISTRY_PATTERNS
|
|
94
|
-
)
|
|
82
|
+
return any(pattern in path_normalized for pattern in WORKFLOW_REGISTRY_PATTERNS)
|
|
95
83
|
|
|
96
84
|
|
|
97
85
|
def is_migration_file(file_path: str) -> bool:
|
|
98
86
|
path_normalized = file_path.lower().replace("\\", "/")
|
|
99
|
-
return any(
|
|
100
|
-
pattern.replace("\\", "/") in path_normalized
|
|
101
|
-
for pattern in MIGRATION_PATH_PATTERNS
|
|
102
|
-
)
|
|
87
|
+
return any(pattern in path_normalized for pattern in MIGRATION_PATH_PATTERNS)
|
|
@@ -14,6 +14,9 @@ _BLOCKING_DIR = str(Path(__file__).resolve().parent.parent / "blocking")
|
|
|
14
14
|
if _BLOCKING_DIR not in sys.path:
|
|
15
15
|
sys.path.insert(0, _BLOCKING_DIR)
|
|
16
16
|
|
|
17
|
+
import pytest # noqa: E402
|
|
18
|
+
|
|
19
|
+
from validators import exempt_paths # noqa: E402
|
|
17
20
|
from validators.exempt_paths import is_config_file # noqa: E402
|
|
18
21
|
from code_rules_path_utils import is_config_file as path_utils_is_config_file # noqa: E402
|
|
19
22
|
|
|
@@ -63,3 +66,20 @@ def test_is_config_file_is_identical_function_object_from_path_utils() -> None:
|
|
|
63
66
|
assert is_config_file is path_utils_is_config_file, (
|
|
64
67
|
"exempt_paths.is_config_file must be imported from code_rules_path_utils, not re-defined"
|
|
65
68
|
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize(
|
|
72
|
+
"normalized_pattern_set_name",
|
|
73
|
+
[
|
|
74
|
+
"HOOK_INFRASTRUCTURE_PATTERNS",
|
|
75
|
+
"WORKFLOW_REGISTRY_PATTERNS",
|
|
76
|
+
"MIGRATION_PATH_PATTERNS",
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
def test_normalized_pattern_set_carries_no_backslash_entry(
|
|
80
|
+
normalized_pattern_set_name: str,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Each set's consumer normalizes the path with .replace("\\\\", "/") but
|
|
83
|
+
not the pattern, so a backslash entry can never match."""
|
|
84
|
+
pattern_set = getattr(exempt_paths, normalized_pattern_set_name)
|
|
85
|
+
assert all("\\" not in each_pattern for each_pattern in pattern_set)
|
|
@@ -150,6 +150,18 @@ class TestPrintInCliEntryPoint:
|
|
|
150
150
|
tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
|
|
151
151
|
assert check_print_in_production(tree, "packages/foo/scripts/run_job.py") == []
|
|
152
152
|
|
|
153
|
+
def test_diagnostics_path_allows_print(self) -> None:
|
|
154
|
+
tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
|
|
155
|
+
assert check_print_in_production(tree, "theme_exports/tools/diagnostics/probe.py") == []
|
|
156
|
+
|
|
157
|
+
def test_windows_diagnostics_path_allows_print(self) -> None:
|
|
158
|
+
tree = ast.parse(CLI_ENTRY_POINT_WITH_PRINTS)
|
|
159
|
+
assert check_print_in_production(tree, "theme_exports\\tools\\diagnostics\\probe.py") == []
|
|
160
|
+
|
|
161
|
+
def test_near_miss_diagnostics_name_still_flagged(self) -> None:
|
|
162
|
+
tree = ast.parse(NEAR_MISS_WITH_PRINT)
|
|
163
|
+
assert len(check_print_in_production(tree, "run_diagnostics.py")) == 1
|
|
164
|
+
|
|
153
165
|
def test_near_miss_name_still_flagged(self) -> None:
|
|
154
166
|
tree = ast.parse(NEAR_MISS_WITH_PRINT)
|
|
155
167
|
assert len(check_print_in_production(tree, "publicli.py")) == 1
|
|
@@ -180,6 +192,8 @@ class TestCrossSurfaceConsistency:
|
|
|
180
192
|
"stp_preview/renderer.py",
|
|
181
193
|
"publicli.py",
|
|
182
194
|
"packages/foo/scripts/run_job.py",
|
|
195
|
+
"theme_exports/tools/diagnostics/probe.py",
|
|
196
|
+
"run_diagnostics.py",
|
|
183
197
|
):
|
|
184
198
|
production_allows = check_print_in_production(tree, path) == []
|
|
185
199
|
library_allows = check_library_print(source, path) == []
|