claude-dev-env 1.41.0 → 1.43.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/CLAUDE.md +8 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
- package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
- package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +124 -20
- package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
- package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
- package/_shared/pr-loop/scripts/preflight.py +13 -31
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +76 -33
- package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -2
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +5 -3
- package/agents/pr-description-writer.md +50 -140
- package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
- package/hooks/_gh_pr_author_swap_utils.py +1 -1
- package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
- package/hooks/blocking/code_rules_enforcer.py +217 -99
- package/hooks/blocking/code_rules_path_utils.py +8 -1
- package/hooks/blocking/destructive_command_blocker.py +1 -1
- package/hooks/blocking/es_exe_path_rewriter.py +7 -13
- package/hooks/blocking/gh_body_arg_blocker.py +6 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
- package/hooks/blocking/gh_pr_author_restore.py +5 -5
- package/hooks/blocking/hedging_language_blocker.py +4 -10
- package/hooks/blocking/md_path_exemptions.py +205 -0
- package/hooks/blocking/md_to_html_blocker.py +48 -20
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
- package/hooks/blocking/pr_description_enforcer.py +626 -41
- package/hooks/blocking/question_to_user_enforcer.py +4 -10
- package/hooks/blocking/state_description_blocker.py +6 -12
- package/hooks/blocking/tdd_enforcer.py +1 -1
- package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +3 -3
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
- package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
- package/hooks/blocking/test_hedging_language_blocker.py +2 -2
- package/hooks/blocking/test_md_to_html_blocker.py +463 -8
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
- package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
- package/hooks/blocking/windows_rmtree_blocker.py +5 -11
- package/hooks/diagnostic/hook_log_extractor.py +1 -1
- package/hooks/diagnostic/hook_log_init.py +1 -1
- package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
- package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
- package/hooks/diagnostic/test_hook_log_init.py +2 -2
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
- package/hooks/git-hooks/gate_utils.py +1 -1
- package/hooks/git-hooks/pre_commit.py +1 -1
- package/hooks/git-hooks/pre_push.py +1 -1
- package/hooks/git-hooks/test_config.py +5 -5
- package/hooks/git-hooks/test_pre_push.py +6 -6
- package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
- package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
- package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
- package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
- package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
- package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
- package/hooks/{config → hooks_constants}/test_messages.py +2 -6
- package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
- package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
- package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
- package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
- package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
- package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
- package/hooks/session/session_env_cleanup.py +4 -10
- package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
- package/hooks/session/test_untracked_repo_detector.py +2 -2
- package/hooks/session/untracked_repo_detector.py +6 -12
- package/hooks/test__gh_pr_author_swap_utils.py +1 -1
- package/hooks/validators/run_all_validators.py +16 -5
- package/hooks/validators/test_output_formatter.py +46 -0
- package/hooks/workflow/doc_gist_auto_publish.py +1 -1
- package/hooks/workflow/md_to_html_companion.py +8 -15
- package/hooks/workflow/test_md_to_html_companion.py +184 -23
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +1 -1
- package/rules/vault-context.md +1 -1
- package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
- package/scripts/setup_project_paths.py +49 -11
- package/scripts/sweep_empty_dirs.py +10 -1
- package/scripts/test_setup_project_paths.py +2 -2
- package/scripts/test_sweep_empty_dirs.py +2 -6
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +1 -1
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +110 -13
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
- package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +71 -25
- package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
- package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
- package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
- package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
- package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
- package/skills/doc-gist/SKILL.md +1 -1
- package/skills/doc-gist/scripts/gist_upload.py +1 -1
- package/skills/implement/SKILL.md +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
- package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
- package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
- package/skills/pr-converge/scripts/check_convergence.py +175 -29
- package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
- package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
- package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
- package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
- package/skills/refine/SKILL.md +257 -0
- package/skills/refine/templates/implementation-notes-template.html +56 -0
- package/skills/refine/templates/plan-template.md +60 -0
- package/skills/session-log/SKILL.md +98 -233
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
- package/hooks/config/pr_description_enforcer_constants.py +0 -19
- package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +0 -20
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
- /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
- /package/hooks/{config → hooks_constants}/__init__.py +0 -0
- /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
- /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
- /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
- /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/messages.py +0 -0
- /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
- /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
- /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
- /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
- /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
- /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
- /package/skills/{pr-converge/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
- /package/skills/{pr-converge/scripts/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
- /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
- /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Behavior tests for the agent-config carve-out and stale-trust-entry purge.
|
|
2
|
+
|
|
3
|
+
Covers two Bugbot findings on PR #467:
|
|
4
|
+
- Deny rules must be written to permissions.deny so agent-config edits
|
|
5
|
+
require explicit per-edit user approval.
|
|
6
|
+
- Trust entries in autoMode.environment must be purged on grant
|
|
7
|
+
(preventing accumulation across template revisions) and removed on
|
|
8
|
+
revoke regardless of the exact template wording.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_module_from_path(module_name: str, module_path: Path) -> ModuleType:
|
|
24
|
+
specification = importlib.util.spec_from_file_location(module_name, module_path)
|
|
25
|
+
assert specification is not None
|
|
26
|
+
assert specification.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(specification)
|
|
28
|
+
specification.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _scripts_directory() -> Path:
|
|
33
|
+
return Path(__file__).parent.parent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_common_module() -> ModuleType:
|
|
37
|
+
scripts_directory = _scripts_directory()
|
|
38
|
+
scripts_directory_str = str(scripts_directory.resolve())
|
|
39
|
+
if scripts_directory_str not in sys.path:
|
|
40
|
+
sys.path.insert(0, scripts_directory_str)
|
|
41
|
+
return _load_module_from_path(
|
|
42
|
+
"_claude_permissions_common",
|
|
43
|
+
scripts_directory / "_claude_permissions_common.py",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_grant_module() -> ModuleType:
|
|
48
|
+
scripts_directory = _scripts_directory()
|
|
49
|
+
scripts_directory_str = str(scripts_directory.resolve())
|
|
50
|
+
if scripts_directory_str not in sys.path:
|
|
51
|
+
sys.path.insert(0, scripts_directory_str)
|
|
52
|
+
return _load_module_from_path(
|
|
53
|
+
"grant_project_claude_permissions",
|
|
54
|
+
scripts_directory / "grant_project_claude_permissions.py",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_revoke_module() -> ModuleType:
|
|
59
|
+
scripts_directory = _scripts_directory()
|
|
60
|
+
scripts_directory_str = str(scripts_directory.resolve())
|
|
61
|
+
if scripts_directory_str not in sys.path:
|
|
62
|
+
sys.path.insert(0, scripts_directory_str)
|
|
63
|
+
return _load_module_from_path(
|
|
64
|
+
"revoke_project_claude_permissions",
|
|
65
|
+
scripts_directory / "revoke_project_claude_permissions.py",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_constants_module() -> ModuleType:
|
|
70
|
+
return _load_module_from_path(
|
|
71
|
+
"pr_loop_shared_constants.claude_permissions_constants",
|
|
72
|
+
_scripts_directory()
|
|
73
|
+
/ "pr_loop_shared_constants"
|
|
74
|
+
/ "claude_permissions_constants.py",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _seed_grant_then_run(
|
|
79
|
+
fake_settings_path: Path,
|
|
80
|
+
fake_project_root: Path,
|
|
81
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
82
|
+
pre_existing_settings: dict[str, Any],
|
|
83
|
+
) -> None:
|
|
84
|
+
fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
|
|
85
|
+
grant_module = _load_grant_module()
|
|
86
|
+
monkeypatch.setattr(
|
|
87
|
+
grant_module,
|
|
88
|
+
"get_claude_user_settings_path",
|
|
89
|
+
lambda: fake_settings_path,
|
|
90
|
+
)
|
|
91
|
+
monkeypatch.chdir(fake_project_root)
|
|
92
|
+
grant_module.grant_permissions_for_current_directory()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _seed_revoke_then_run(
|
|
96
|
+
fake_settings_path: Path,
|
|
97
|
+
fake_project_root: Path,
|
|
98
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
99
|
+
pre_existing_settings: dict[str, Any],
|
|
100
|
+
) -> None:
|
|
101
|
+
fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
|
|
102
|
+
revoke_module = _load_revoke_module()
|
|
103
|
+
monkeypatch.setattr(
|
|
104
|
+
revoke_module,
|
|
105
|
+
"get_claude_user_settings_path",
|
|
106
|
+
lambda: fake_settings_path,
|
|
107
|
+
)
|
|
108
|
+
monkeypatch.chdir(fake_project_root)
|
|
109
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _make_fake_project(tmp_path: Path) -> Path:
|
|
113
|
+
fake_project_root = tmp_path / "fake_project"
|
|
114
|
+
(fake_project_root / ".claude").mkdir(parents=True)
|
|
115
|
+
return fake_project_root
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _project_path_as_posix(fake_project_root: Path) -> str:
|
|
119
|
+
return str(fake_project_root).replace("\\", "/")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_grant_writes_deny_rules_for_every_tool_and_pattern(
|
|
123
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
124
|
+
) -> None:
|
|
125
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
126
|
+
fake_settings_path = tmp_path / "settings.json"
|
|
127
|
+
constants_module = _load_constants_module()
|
|
128
|
+
_seed_grant_then_run(
|
|
129
|
+
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
130
|
+
)
|
|
131
|
+
capsys.readouterr()
|
|
132
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
133
|
+
deny_list = written_settings["permissions"]["deny"]
|
|
134
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
135
|
+
for each_tool in constants_module.ALL_AGENT_CONFIG_DENY_TOOLS:
|
|
136
|
+
for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
137
|
+
expected_rule = f"{each_tool}({project_path_posix}/.claude/{each_pattern})"
|
|
138
|
+
assert expected_rule in deny_list, (
|
|
139
|
+
f"deny list missing expected rule {expected_rule!r}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_grant_writes_glob_deny_rules_for_every_agent_config_pattern(
|
|
144
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Glob must be in the deny tuple so agent-config paths require approval.
|
|
147
|
+
|
|
148
|
+
The AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE promises Edit/Write/Read/Glob
|
|
149
|
+
trust EXCEPT for agent-config files. Glob deny rules are how the EXCEPT
|
|
150
|
+
clause is honored for the Glob tool.
|
|
151
|
+
"""
|
|
152
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
153
|
+
fake_settings_path = tmp_path / "settings.json"
|
|
154
|
+
constants_module = _load_constants_module()
|
|
155
|
+
_seed_grant_then_run(
|
|
156
|
+
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
157
|
+
)
|
|
158
|
+
capsys.readouterr()
|
|
159
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
160
|
+
deny_list = written_settings["permissions"]["deny"]
|
|
161
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
162
|
+
assert "Glob" in constants_module.ALL_AGENT_CONFIG_DENY_TOOLS
|
|
163
|
+
assert "Glob" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
164
|
+
for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
165
|
+
expected_glob_rule = f"Glob({project_path_posix}/.claude/{each_pattern})"
|
|
166
|
+
assert expected_glob_rule in deny_list, (
|
|
167
|
+
f"deny list missing expected Glob rule {expected_glob_rule!r}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_grant_purges_stale_trust_entries_then_writes_current_template(
|
|
172
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
173
|
+
) -> None:
|
|
174
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
175
|
+
fake_settings_path = tmp_path / "settings.json"
|
|
176
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
177
|
+
stale_entry_a = (
|
|
178
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** old wording form A"
|
|
179
|
+
)
|
|
180
|
+
stale_entry_b = (
|
|
181
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** "
|
|
182
|
+
f"different earlier wording"
|
|
183
|
+
)
|
|
184
|
+
unrelated_entry = "Some unrelated environment hint"
|
|
185
|
+
pre_existing_settings: dict[str, Any] = {
|
|
186
|
+
"autoMode": {
|
|
187
|
+
"environment": [stale_entry_a, stale_entry_b, unrelated_entry],
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
_seed_grant_then_run(
|
|
191
|
+
fake_settings_path,
|
|
192
|
+
fake_project_root,
|
|
193
|
+
monkeypatch,
|
|
194
|
+
pre_existing_settings=pre_existing_settings,
|
|
195
|
+
)
|
|
196
|
+
captured = capsys.readouterr()
|
|
197
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
198
|
+
environment_list = written_settings["autoMode"]["environment"]
|
|
199
|
+
assert stale_entry_a not in environment_list
|
|
200
|
+
assert stale_entry_b not in environment_list
|
|
201
|
+
assert unrelated_entry in environment_list
|
|
202
|
+
matching_trust_entries = [
|
|
203
|
+
each_entry
|
|
204
|
+
for each_entry in environment_list
|
|
205
|
+
if isinstance(each_entry, str)
|
|
206
|
+
and each_entry.startswith("Trusted local workspace:")
|
|
207
|
+
and f"{project_path_posix}/.claude/**" in each_entry
|
|
208
|
+
]
|
|
209
|
+
assert len(matching_trust_entries) == 1
|
|
210
|
+
assert "Stale auto-mode environment entries purged" in captured.out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_revoke_removes_deny_rules(
|
|
214
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
215
|
+
) -> None:
|
|
216
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
217
|
+
fake_settings_path = tmp_path / "settings.json"
|
|
218
|
+
common_module = _load_common_module()
|
|
219
|
+
constants_module = _load_constants_module()
|
|
220
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
221
|
+
all_deny_rules = common_module.build_agent_config_deny_rules(
|
|
222
|
+
project_path_posix,
|
|
223
|
+
constants_module.ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
224
|
+
constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
225
|
+
)
|
|
226
|
+
pre_existing_settings: dict[str, Any] = {
|
|
227
|
+
"permissions": {
|
|
228
|
+
"deny": list(all_deny_rules),
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
_seed_revoke_then_run(
|
|
232
|
+
fake_settings_path,
|
|
233
|
+
fake_project_root,
|
|
234
|
+
monkeypatch,
|
|
235
|
+
pre_existing_settings=pre_existing_settings,
|
|
236
|
+
)
|
|
237
|
+
capsys.readouterr()
|
|
238
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
239
|
+
permissions_section = written_settings.get("permissions", {})
|
|
240
|
+
remaining_deny_list = permissions_section.get("deny", [])
|
|
241
|
+
for each_rule in all_deny_rules:
|
|
242
|
+
assert each_rule not in remaining_deny_list
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_revoke_removes_every_legacy_trust_entry_for_project(
|
|
246
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
247
|
+
) -> None:
|
|
248
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
249
|
+
fake_settings_path = tmp_path / "settings.json"
|
|
250
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
251
|
+
legacy_entry_a = (
|
|
252
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** template revision A"
|
|
253
|
+
)
|
|
254
|
+
legacy_entry_b = (
|
|
255
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** template revision B"
|
|
256
|
+
)
|
|
257
|
+
unrelated_other_project_entry = (
|
|
258
|
+
"Trusted local workspace: /some/other/project/.claude/** still valid"
|
|
259
|
+
)
|
|
260
|
+
pre_existing_settings: dict[str, Any] = {
|
|
261
|
+
"autoMode": {
|
|
262
|
+
"environment": [
|
|
263
|
+
legacy_entry_a,
|
|
264
|
+
legacy_entry_b,
|
|
265
|
+
unrelated_other_project_entry,
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
_seed_revoke_then_run(
|
|
270
|
+
fake_settings_path,
|
|
271
|
+
fake_project_root,
|
|
272
|
+
monkeypatch,
|
|
273
|
+
pre_existing_settings=pre_existing_settings,
|
|
274
|
+
)
|
|
275
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
276
|
+
environment_list = written_settings.get("autoMode", {}).get("environment", [])
|
|
277
|
+
assert legacy_entry_a not in environment_list
|
|
278
|
+
assert legacy_entry_b not in environment_list
|
|
279
|
+
assert unrelated_other_project_entry in environment_list
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_template_constant_documents_agent_config_carveout() -> None:
|
|
283
|
+
constants_module = _load_constants_module()
|
|
284
|
+
template_text = constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
285
|
+
assert "agent-config files always require explicit per-edit user approval" in (
|
|
286
|
+
template_text
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_is_trust_entry_for_project_predicate_filters_by_prefix_and_project_path() -> (
|
|
291
|
+
None
|
|
292
|
+
):
|
|
293
|
+
common_module = _load_common_module()
|
|
294
|
+
project_path_posix = "/fake/proj"
|
|
295
|
+
trust_prefix = "Trusted local workspace:"
|
|
296
|
+
non_string_value: object = 42
|
|
297
|
+
assert (
|
|
298
|
+
common_module.is_trust_entry_for_project(
|
|
299
|
+
non_string_value, project_path_posix, trust_prefix
|
|
300
|
+
)
|
|
301
|
+
is False
|
|
302
|
+
)
|
|
303
|
+
wrong_prefix_entry = (
|
|
304
|
+
f"Something else: {project_path_posix}/.claude/** with marker token"
|
|
305
|
+
)
|
|
306
|
+
assert (
|
|
307
|
+
common_module.is_trust_entry_for_project(
|
|
308
|
+
wrong_prefix_entry, project_path_posix, trust_prefix
|
|
309
|
+
)
|
|
310
|
+
is False
|
|
311
|
+
)
|
|
312
|
+
different_project_entry = (
|
|
313
|
+
"Trusted local workspace: /other/project/.claude/** unrelated"
|
|
314
|
+
)
|
|
315
|
+
assert (
|
|
316
|
+
common_module.is_trust_entry_for_project(
|
|
317
|
+
different_project_entry, project_path_posix, trust_prefix
|
|
318
|
+
)
|
|
319
|
+
is False
|
|
320
|
+
)
|
|
321
|
+
matching_entry = (
|
|
322
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** any wording form"
|
|
323
|
+
)
|
|
324
|
+
assert (
|
|
325
|
+
common_module.is_trust_entry_for_project(
|
|
326
|
+
matching_entry, project_path_posix, trust_prefix
|
|
327
|
+
)
|
|
328
|
+
is True
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
|
|
333
|
+
"""When the project_path is a path suffix of an unrelated entry's path,
|
|
334
|
+
the predicate must reject the unrelated entry (the boundary anchor case)."""
|
|
335
|
+
common_module = _load_common_module()
|
|
336
|
+
short_project_path = "/projects/foo"
|
|
337
|
+
trust_prefix = "Trusted local workspace:"
|
|
338
|
+
longer_unrelated_path_entry = (
|
|
339
|
+
"Trusted local workspace: /Users/jon/projects/foo/.claude/** unrelated path"
|
|
340
|
+
)
|
|
341
|
+
assert (
|
|
342
|
+
common_module.is_trust_entry_for_project(
|
|
343
|
+
longer_unrelated_path_entry, short_project_path, trust_prefix
|
|
344
|
+
)
|
|
345
|
+
is False
|
|
346
|
+
)
|
|
347
|
+
quoted_matching_entry = (
|
|
348
|
+
f'Trusted local workspace: "{short_project_path}/.claude/**" quoted form'
|
|
349
|
+
)
|
|
350
|
+
assert (
|
|
351
|
+
common_module.is_trust_entry_for_project(
|
|
352
|
+
quoted_matching_entry, short_project_path, trust_prefix
|
|
353
|
+
)
|
|
354
|
+
is True
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_second_grant_is_idempotent_when_no_other_settings_changed(
|
|
359
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Running grant twice in a row must perform zero changes the second time.
|
|
362
|
+
|
|
363
|
+
On the second call the existing trust entry is byte-identical to the
|
|
364
|
+
freshly-formatted current entry, so purge_stale_trust_entries treats it as
|
|
365
|
+
protected and does not remove it; add_auto_mode_environment_entry then
|
|
366
|
+
no-ops because the entry is already present.
|
|
367
|
+
"""
|
|
368
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
369
|
+
fake_settings_path = tmp_path / "settings.json"
|
|
370
|
+
_seed_grant_then_run(
|
|
371
|
+
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
372
|
+
)
|
|
373
|
+
first_run_output = capsys.readouterr()
|
|
374
|
+
assert "No changes needed" not in first_run_output.out
|
|
375
|
+
grant_module = _load_grant_module()
|
|
376
|
+
monkeypatch.setattr(
|
|
377
|
+
grant_module,
|
|
378
|
+
"get_claude_user_settings_path",
|
|
379
|
+
lambda: fake_settings_path,
|
|
380
|
+
)
|
|
381
|
+
monkeypatch.chdir(fake_project_root)
|
|
382
|
+
grant_module.grant_permissions_for_current_directory()
|
|
383
|
+
second_run_output = capsys.readouterr()
|
|
384
|
+
assert "No changes needed; settings file left untouched." in second_run_output.out
|
|
385
|
+
assert "Stale auto-mode environment entries purged" not in second_run_output.out
|
|
@@ -138,10 +138,12 @@ def test_save_settings_temp_suffix_includes_pid_and_random_token(
|
|
|
138
138
|
|
|
139
139
|
def test_text_file_encoding_sourced_from_config() -> None:
|
|
140
140
|
config_module_path = (
|
|
141
|
-
Path(__file__).parent.parent
|
|
141
|
+
Path(__file__).parent.parent
|
|
142
|
+
/ "pr_loop_shared_constants"
|
|
143
|
+
/ "claude_permissions_constants.py"
|
|
142
144
|
)
|
|
143
145
|
specification = importlib.util.spec_from_file_location(
|
|
144
|
-
"
|
|
146
|
+
"pr_loop_shared_constants.claude_permissions_constants", config_module_path
|
|
145
147
|
)
|
|
146
148
|
assert specification is not None
|
|
147
149
|
assert specification.loader is not None
|
|
@@ -7,10 +7,12 @@ from types import ModuleType
|
|
|
7
7
|
|
|
8
8
|
def _load_constants_module() -> ModuleType:
|
|
9
9
|
module_path = (
|
|
10
|
-
Path(__file__).parent.parent
|
|
10
|
+
Path(__file__).parent.parent
|
|
11
|
+
/ "pr_loop_shared_constants"
|
|
12
|
+
/ "claude_permissions_constants.py"
|
|
11
13
|
)
|
|
12
14
|
specification = importlib.util.spec_from_file_location(
|
|
13
|
-
"
|
|
15
|
+
"pr_loop_shared_constants.claude_permissions_constants", module_path
|
|
14
16
|
)
|
|
15
17
|
assert specification is not None
|
|
16
18
|
assert specification.loader is not None
|
|
@@ -26,6 +28,16 @@ def test_exposes_all_permission_allow_tools_tuple() -> None:
|
|
|
26
28
|
assert constants_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Write", "Read")
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def test_exposes_all_agent_config_deny_tools_tuple_with_glob() -> None:
|
|
32
|
+
assert constants_module.ALL_AGENT_CONFIG_DENY_TOOLS == (
|
|
33
|
+
"Edit",
|
|
34
|
+
"Write",
|
|
35
|
+
"Read",
|
|
36
|
+
"Glob",
|
|
37
|
+
)
|
|
38
|
+
assert "Glob" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
39
|
+
|
|
40
|
+
|
|
29
41
|
def test_auto_mode_environment_entry_template_is_format_string() -> None:
|
|
30
42
|
rendered_template_text = (
|
|
31
43
|
constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
@@ -36,6 +48,29 @@ def test_auto_mode_environment_entry_template_is_format_string() -> None:
|
|
|
36
48
|
assert ".claude/**" in rendered_template_text
|
|
37
49
|
|
|
38
50
|
|
|
51
|
+
def test_template_derives_human_readable_pattern_list_from_pattern_tuple() -> None:
|
|
52
|
+
"""Every pattern in ALL_AGENT_CONFIG_PATH_PATTERNS must surface in the
|
|
53
|
+
rendered template through its derived human-readable form, and the
|
|
54
|
+
template must still expose the {project_path} placeholder for .format()
|
|
55
|
+
substitution at runtime."""
|
|
56
|
+
template_text: str = constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
57
|
+
assert "{project_path}" in template_text
|
|
58
|
+
for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
59
|
+
if each_pattern.endswith("/**"):
|
|
60
|
+
directory_name = each_pattern[: -len("/**")]
|
|
61
|
+
expected_phrase = f"anything under {directory_name}/"
|
|
62
|
+
elif each_pattern == "mcp.json":
|
|
63
|
+
expected_phrase = "the mcp.json file"
|
|
64
|
+
else:
|
|
65
|
+
expected_phrase = each_pattern
|
|
66
|
+
assert expected_phrase in template_text, (
|
|
67
|
+
f"template missing derived phrase for pattern {each_pattern!r}: "
|
|
68
|
+
f"expected {expected_phrase!r}"
|
|
69
|
+
)
|
|
70
|
+
rendered_template_text = template_text.format(project_path="/tmp/x")
|
|
71
|
+
assert "/tmp/x" in rendered_template_text
|
|
72
|
+
|
|
73
|
+
|
|
39
74
|
def test_get_claude_user_settings_path_ends_in_settings_json() -> None:
|
|
40
75
|
resolved_settings_path = constants_module.get_claude_user_settings_path()
|
|
41
76
|
assert resolved_settings_path.name == constants_module.CLAUDE_SETTINGS_FILENAME
|
|
@@ -7,10 +7,12 @@ from types import ModuleType
|
|
|
7
7
|
|
|
8
8
|
def _load_constants_module() -> ModuleType:
|
|
9
9
|
module_path = (
|
|
10
|
-
Path(__file__).parent.parent
|
|
10
|
+
Path(__file__).parent.parent
|
|
11
|
+
/ "pr_loop_shared_constants"
|
|
12
|
+
/ "claude_settings_keys_constants.py"
|
|
11
13
|
)
|
|
12
14
|
specification = importlib.util.spec_from_file_location(
|
|
13
|
-
"
|
|
15
|
+
"pr_loop_shared_constants.claude_settings_keys_constants", module_path
|
|
14
16
|
)
|
|
15
17
|
assert specification is not None
|
|
16
18
|
assert specification.loader is not None
|
|
@@ -7,10 +7,12 @@ from types import ModuleType
|
|
|
7
7
|
|
|
8
8
|
def _load_constants_module() -> ModuleType:
|
|
9
9
|
module_path = (
|
|
10
|
-
Path(__file__).parent.parent
|
|
10
|
+
Path(__file__).parent.parent
|
|
11
|
+
/ "pr_loop_shared_constants"
|
|
12
|
+
/ "code_rules_gate_constants.py"
|
|
11
13
|
)
|
|
12
14
|
specification = importlib.util.spec_from_file_location(
|
|
13
|
-
"
|
|
15
|
+
"pr_loop_shared_constants.code_rules_gate_constants", module_path
|
|
14
16
|
)
|
|
15
17
|
assert specification is not None
|
|
16
18
|
assert specification.loader is not None
|
|
@@ -11,9 +11,13 @@ from types import ModuleType
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _load_constants_module() -> ModuleType:
|
|
14
|
-
module_path =
|
|
14
|
+
module_path = (
|
|
15
|
+
Path(__file__).parent.parent
|
|
16
|
+
/ "pr_loop_shared_constants"
|
|
17
|
+
/ "fix_hookspath_constants.py"
|
|
18
|
+
)
|
|
15
19
|
specification = importlib.util.spec_from_file_location(
|
|
16
|
-
"
|
|
20
|
+
"pr_loop_shared_constants.fix_hookspath_constants", module_path
|
|
17
21
|
)
|
|
18
22
|
assert specification is not None
|
|
19
23
|
assert specification.loader is not None
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Smoke tests for grant_project_claude_permissions wiring.
|
|
2
2
|
|
|
3
3
|
Confirms the module imports cleanly with the constants now sourced from
|
|
4
|
-
|
|
4
|
+
pr_loop_shared_constants/claude_permissions_constants.py and
|
|
5
|
+
pr_loop_shared_constants/claude_settings_keys_constants.py.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
@@ -17,7 +18,6 @@ def _load_grant_module() -> ModuleType:
|
|
|
17
18
|
parent_directory = str(scripts_directory.resolve())
|
|
18
19
|
if parent_directory not in sys.path:
|
|
19
20
|
sys.path.insert(0, parent_directory)
|
|
20
|
-
sys.modules.pop("config", None)
|
|
21
21
|
module_path = scripts_directory / "grant_project_claude_permissions.py"
|
|
22
22
|
specification = importlib.util.spec_from_file_location(
|
|
23
23
|
"grant_project_claude_permissions", module_path
|
|
@@ -36,11 +36,10 @@ from typing import Any
|
|
|
36
36
|
THIS_FILE_DIRECTORY = Path(__file__).resolve().parent
|
|
37
37
|
SCRIPT_DIRECTORY = THIS_FILE_DIRECTORY.parent
|
|
38
38
|
|
|
39
|
-
sys.modules.pop("config", None)
|
|
40
39
|
if str(SCRIPT_DIRECTORY) not in sys.path:
|
|
41
40
|
sys.path.insert(0, str(SCRIPT_DIRECTORY))
|
|
42
41
|
|
|
43
|
-
from
|
|
42
|
+
from pr_loop_shared_constants.post_audit_thread_constants import ( # noqa: E402
|
|
44
43
|
ALL_GH_AUTH_TOKEN_COMMAND_PARTS,
|
|
45
44
|
ALL_RETRY_BACKOFF_SECONDS,
|
|
46
45
|
BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
|
|
@@ -7,10 +7,12 @@ from types import ModuleType
|
|
|
7
7
|
|
|
8
8
|
def _load_constants_module() -> ModuleType:
|
|
9
9
|
module_path = (
|
|
10
|
-
Path(__file__).parent.parent
|
|
10
|
+
Path(__file__).parent.parent
|
|
11
|
+
/ "pr_loop_shared_constants"
|
|
12
|
+
/ "post_audit_thread_constants.py"
|
|
11
13
|
)
|
|
12
14
|
specification = importlib.util.spec_from_file_location(
|
|
13
|
-
"
|
|
15
|
+
"pr_loop_shared_constants.post_audit_thread_constants", module_path
|
|
14
16
|
)
|
|
15
17
|
assert specification is not None
|
|
16
18
|
assert specification.loader is not None
|
|
@@ -31,7 +31,7 @@ def _load_preflight_module() -> ModuleType:
|
|
|
31
31
|
|
|
32
32
|
preflight = _load_preflight_module()
|
|
33
33
|
|
|
34
|
-
from
|
|
34
|
+
from pr_loop_shared_constants.preflight_constants import ( # noqa: E402
|
|
35
35
|
PYTEST_INI_FILENAME,
|
|
36
36
|
PYTEST_NO_TESTS_COLLECTED_EXIT_CODE,
|
|
37
37
|
)
|
|
@@ -204,16 +204,19 @@ def test_should_exit_nonzero_when_subprocess_run_raises_os_error(
|
|
|
204
204
|
|
|
205
205
|
|
|
206
206
|
def test_preflight_uses_shared_hooks_path_suffix_constant() -> None:
|
|
207
|
-
"""Preflight's expected suffix must come from
|
|
208
|
-
so the canonical hooks
|
|
207
|
+
"""Preflight's expected suffix must come from
|
|
208
|
+
pr_loop_shared_constants.fix_hookspath_constants so the canonical hooks
|
|
209
|
+
directory is defined in exactly one place."""
|
|
209
210
|
scripts_directory = str(Path(__file__).parent.parent.resolve())
|
|
210
211
|
if scripts_directory not in sys.path:
|
|
211
212
|
sys.path.insert(0, scripts_directory)
|
|
212
213
|
constants_module_path = (
|
|
213
|
-
Path(__file__).parent.parent
|
|
214
|
+
Path(__file__).parent.parent
|
|
215
|
+
/ "pr_loop_shared_constants"
|
|
216
|
+
/ "fix_hookspath_constants.py"
|
|
214
217
|
)
|
|
215
218
|
constants_specification = importlib.util.spec_from_file_location(
|
|
216
|
-
"
|
|
219
|
+
"pr_loop_shared_constants.fix_hookspath_constants",
|
|
217
220
|
constants_module_path,
|
|
218
221
|
)
|
|
219
222
|
assert constants_specification is not None
|
|
@@ -234,15 +237,18 @@ def test_preflight_skip_uses_shared_env_var_constant(
|
|
|
234
237
|
capsys: pytest.CaptureFixture[str],
|
|
235
238
|
monkeypatch: pytest.MonkeyPatch,
|
|
236
239
|
) -> None:
|
|
237
|
-
"""The preflight skip env-var name must come from
|
|
240
|
+
"""The preflight skip env-var name must come from
|
|
241
|
+
pr_loop_shared_constants/preflight_constants.py."""
|
|
238
242
|
scripts_directory = str(Path(__file__).parent.parent.resolve())
|
|
239
243
|
if scripts_directory not in sys.path:
|
|
240
244
|
sys.path.insert(0, scripts_directory)
|
|
241
245
|
constants_module_path = (
|
|
242
|
-
Path(__file__).parent.parent
|
|
246
|
+
Path(__file__).parent.parent
|
|
247
|
+
/ "pr_loop_shared_constants"
|
|
248
|
+
/ "preflight_constants.py"
|
|
243
249
|
)
|
|
244
250
|
constants_specification = importlib.util.spec_from_file_location(
|
|
245
|
-
"
|
|
251
|
+
"pr_loop_shared_constants.preflight_constants",
|
|
246
252
|
constants_module_path,
|
|
247
253
|
)
|
|
248
254
|
assert constants_specification is not None
|
|
@@ -306,8 +312,9 @@ def test_preflight_does_not_import_unused_repository_root_marker_constant() -> N
|
|
|
306
312
|
|
|
307
313
|
def test_pytest_no_tests_collected_helper_returns_named_constant() -> None:
|
|
308
314
|
"""The pytest "no tests collected" exit code must be sourced from the
|
|
309
|
-
named constant in
|
|
310
|
-
literal 5 inside the function body (CODE_RULES magic-values
|
|
315
|
+
named constant in pr_loop_shared_constants/preflight_constants.py rather
|
|
316
|
+
than the bare literal 5 inside the function body (CODE_RULES magic-values
|
|
317
|
+
rule)."""
|
|
311
318
|
assert preflight._pytest_exit_code_no_tests_collected() == (
|
|
312
319
|
PYTEST_NO_TESTS_COLLECTED_EXIT_CODE
|
|
313
320
|
)
|
|
@@ -317,30 +324,6 @@ def test_pytest_no_tests_collected_helper_returns_named_constant() -> None:
|
|
|
317
324
|
)
|
|
318
325
|
|
|
319
326
|
|
|
320
|
-
def test_preflight_bootstrap_moves_script_directory_to_front() -> None:
|
|
321
|
-
"""Import bootstrap keeps exactly one script directory entry at the front."""
|
|
322
|
-
module_path = Path(__file__).parent.parent / "preflight.py"
|
|
323
|
-
script_directory_resolved = str(module_path.parent.resolve())
|
|
324
|
-
script_directory_absolute = str(module_path.parent.absolute())
|
|
325
|
-
original_sys_path = list(sys.path)
|
|
326
|
-
try:
|
|
327
|
-
sys.path.insert(0, script_directory_resolved)
|
|
328
|
-
sys.path.insert(0, script_directory_resolved)
|
|
329
|
-
sys.path.insert(0, str(module_path.parents[4]))
|
|
330
|
-
_load_preflight_module()
|
|
331
|
-
assert os.path.samefile(sys.path[0], script_directory_resolved)
|
|
332
|
-
equivalent_count = sum(
|
|
333
|
-
1
|
|
334
|
-
for each_entry in sys.path
|
|
335
|
-
if os.path.exists(each_entry)
|
|
336
|
-
and os.path.samefile(each_entry, script_directory_resolved)
|
|
337
|
-
)
|
|
338
|
-
assert equivalent_count == 1
|
|
339
|
-
assert sys.path[0] == script_directory_absolute
|
|
340
|
-
finally:
|
|
341
|
-
sys.path[:] = original_sys_path
|
|
342
|
-
|
|
343
|
-
|
|
344
327
|
def test_main_uses_correct_changed_files_function_name() -> None:
|
|
345
328
|
"""main() must call get_changed_files, not the undefined get_all_changed_files."""
|
|
346
329
|
main_source = inspect.getsource(preflight.main)
|
|
@@ -528,24 +511,6 @@ def test_explicit_scope_all_with_base_ref_should_not_call_get_changed_files(
|
|
|
528
511
|
mock_get_changed.assert_not_called()
|
|
529
512
|
|
|
530
513
|
|
|
531
|
-
def test_preflight_bootstrap_matches_code_rules_sys_path_pattern() -> None:
|
|
532
|
-
"""Bootstrap must clear duplicate script_directory entries, then guard insert."""
|
|
533
|
-
module_path = Path(__file__).parent.parent / "preflight.py"
|
|
534
|
-
source = module_path.read_text(encoding="utf-8")
|
|
535
|
-
assert "_entry_points_at_preflight_script_directory" in source, (
|
|
536
|
-
"Bootstrap must remove script_directory entries using path equivalence"
|
|
537
|
-
)
|
|
538
|
-
assert "for each_index in range(len(sys.path) - 1, -1, -1):" in source, (
|
|
539
|
-
"Bootstrap must walk sys.path to drop duplicate script directory entries"
|
|
540
|
-
)
|
|
541
|
-
assert "_preflight_scripts_path_entry not in sys.path:" in source, (
|
|
542
|
-
"Bootstrap insert must be guarded for code_rules_gate compliance"
|
|
543
|
-
)
|
|
544
|
-
assert "sys.path.insert(0, _preflight_scripts_path_entry)" in source, (
|
|
545
|
-
"Bootstrap must insert the absolute script directory at index 0"
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
|
|
549
514
|
def test_has_discoverable_tests_should_include_untracked_test_files(
|
|
550
515
|
tmp_path: Path,
|
|
551
516
|
) -> None:
|