claude-dev-env 1.34.1 → 1.36.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/clean-coder.md +109 -1
- package/agents/docs-agent.md +1 -1
- package/agents/project-docs-analyzer.md +0 -1
- package/agents/skill-to-agent-converter.md +0 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/commands/initialize.md +0 -1
- package/commands/readability-review.md +4 -4
- package/commands/review-plan.md +2 -4
- package/commands/stubcheck.md +1 -2
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +686 -60
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +273 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +93 -125
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +94 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +576 -95
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +47 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/agents/agent-writer.md +0 -157
- package/agents/config-centralizer.md +0 -686
- package/agents/config-extraction-agent.md +0 -225
- package/agents/doc-orchestrator.md +0 -47
- package/agents/docx-agent.md +0 -211
- package/agents/magic-value-eliminator-agent.md +0 -72
- package/agents/mandatory-agent-workflow-agent.md +0 -88
- package/agents/parallel-workflow-coordinator.md +0 -779
- package/agents/pdf-agent.md +0 -302
- package/agents/project-context-loader.md +0 -238
- package/agents/readability-review-agent.md +0 -76
- package/agents/refactoring-specialist.md +0 -69
- package/agents/right-sized-engineer.md +0 -129
- package/agents/session-continuity-manager.md +0 -53
- package/agents/stub-detector-agent.md +0 -140
- package/agents/tdd-test-writer.md +0 -62
- package/agents/test-data-builder.md +0 -68
- package/agents/tooling-builder.md +0 -78
- package/agents/validation-expert.md +0 -71
- package/agents/xlsx-agent.md +0 -169
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -148,19 +148,6 @@ def test_should_skip_workflow_registry_files() -> None:
|
|
|
148
148
|
assert issues == []
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
def test_should_cap_issues_at_three() -> None:
|
|
152
|
-
source = (
|
|
153
|
-
"def f() -> None:\n"
|
|
154
|
-
" one = True\n"
|
|
155
|
-
" two = False\n"
|
|
156
|
-
" three = True\n"
|
|
157
|
-
" four = False\n"
|
|
158
|
-
" five = True\n"
|
|
159
|
-
)
|
|
160
|
-
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
161
|
-
assert len(issues) == 3
|
|
162
|
-
|
|
163
|
-
|
|
164
151
|
def test_should_not_flag_syntax_error_as_issue() -> None:
|
|
165
152
|
source = "def f(:\n valid = True\n"
|
|
166
153
|
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
@@ -174,3 +161,52 @@ def test_validate_content_invokes_boolean_naming_check() -> None:
|
|
|
174
161
|
assert matching_issues, (
|
|
175
162
|
f"expected validate_content to surface the boolean-naming issue, got {issues!r}"
|
|
176
163
|
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_should_flag_substring_is_when_not_at_prefix_position() -> None:
|
|
167
|
+
source = "def f() -> None:\n left_is_upper_snake = True\n"
|
|
168
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
169
|
+
_assert_flags_name(issues, "left_is_upper_snake", 2)
|
|
170
|
+
assert len(issues) == 1, (
|
|
171
|
+
f"'is_' in middle position must not satisfy the prefix rule, got: {issues}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_should_flag_substring_has_when_not_at_prefix_position() -> None:
|
|
176
|
+
source = "def f() -> None:\n user_has_permission = True\n"
|
|
177
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
178
|
+
_assert_flags_name(issues, "user_has_permission", 2)
|
|
179
|
+
assert len(issues) == 1, (
|
|
180
|
+
f"'has_' in middle position must not satisfy the prefix rule, got: {issues}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_should_flag_substring_should_when_not_at_prefix_position() -> None:
|
|
185
|
+
source = "def f() -> None:\n user_should_retry = True\n"
|
|
186
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
187
|
+
_assert_flags_name(issues, "user_should_retry", 2)
|
|
188
|
+
assert len(issues) == 1
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_should_flag_substring_can_when_not_at_prefix_position() -> None:
|
|
192
|
+
source = "def f() -> None:\n user_can_edit = True\n"
|
|
193
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
194
|
+
_assert_flags_name(issues, "user_can_edit", 2)
|
|
195
|
+
assert len(issues) == 1
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_flag_right_is_literal_substring_match() -> None:
|
|
199
|
+
source = "def f() -> None:\n right_is_literal = False\n"
|
|
200
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
201
|
+
_assert_flags_name(issues, "right_is_literal", 2)
|
|
202
|
+
assert len(issues) == 1, (
|
|
203
|
+
f"PR #232 finding: substring 'is_' in 'right_is_literal' must be flagged, got: {issues}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_should_allow_is_prefix_at_start_when_compound_word_follows() -> None:
|
|
208
|
+
source = "def f() -> None:\n is_left_upper_snake = True\n"
|
|
209
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
210
|
+
assert issues == [], (
|
|
211
|
+
f"is_left_upper_snake has prefix at position 0, must pass, got: {issues}"
|
|
212
|
+
)
|
|
@@ -122,29 +122,3 @@ def test_should_flag_decorator_with_skip_in_name() -> None:
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def test_stops_at_max_issues_per_check() -> None:
|
|
126
|
-
source = (
|
|
127
|
-
"import pytest\n"
|
|
128
|
-
"\n"
|
|
129
|
-
"@pytest.mark.skip(reason='a')\n"
|
|
130
|
-
"def test_one() -> None:\n"
|
|
131
|
-
" pass\n"
|
|
132
|
-
"\n"
|
|
133
|
-
"@pytest.mark.skip(reason='b')\n"
|
|
134
|
-
"def test_two() -> None:\n"
|
|
135
|
-
" pass\n"
|
|
136
|
-
"\n"
|
|
137
|
-
"@pytest.mark.skip(reason='c')\n"
|
|
138
|
-
"def test_three() -> None:\n"
|
|
139
|
-
" pass\n"
|
|
140
|
-
"\n"
|
|
141
|
-
"@pytest.mark.skip(reason='d')\n"
|
|
142
|
-
"def test_four() -> None:\n"
|
|
143
|
-
" pass\n"
|
|
144
|
-
"\n"
|
|
145
|
-
"@pytest.mark.skip(reason='e')\n"
|
|
146
|
-
"def test_five() -> None:\n"
|
|
147
|
-
" pass\n"
|
|
148
|
-
)
|
|
149
|
-
issues = code_rules_enforcer.check_skip_decorators_in_tests(source, TEST_FILE_PATH)
|
|
150
|
-
assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
11
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
12
|
+
|
|
13
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
14
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
15
|
+
CONFIG_FILE_PATH = "packages/app/config/constants.py"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_should_flag_env_var_name_string_in_function_body() -> None:
|
|
19
|
+
source = (
|
|
20
|
+
"import os\n"
|
|
21
|
+
"\n"
|
|
22
|
+
"def fetch_secret() -> str:\n"
|
|
23
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
24
|
+
)
|
|
25
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
26
|
+
source, PRODUCTION_FILE_PATH
|
|
27
|
+
)
|
|
28
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
29
|
+
f"Expected env-var name flagged, got: {issues}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_should_flag_settings_key_all_caps_with_underscore() -> None:
|
|
34
|
+
source = "def lookup(settings: dict) -> str:\n return settings['HOOKS_PATH']\n"
|
|
35
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
36
|
+
source, PRODUCTION_FILE_PATH
|
|
37
|
+
)
|
|
38
|
+
assert any("HOOKS_PATH" in each_issue for each_issue in issues), (
|
|
39
|
+
f"Expected settings key flagged, got: {issues}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_should_flag_dotted_segment_string() -> None:
|
|
44
|
+
source = "def is_git_dir(path: str) -> bool:\n return path.endswith('.git')\n"
|
|
45
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
46
|
+
source, PRODUCTION_FILE_PATH
|
|
47
|
+
)
|
|
48
|
+
assert any(".git" in each_issue for each_issue in issues), (
|
|
49
|
+
f"Expected '.git' flagged, got: {issues}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_not_flag_single_letter_uppercase() -> None:
|
|
54
|
+
source = "def is_added(line: str) -> bool:\n return line.startswith('A')\n"
|
|
55
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
56
|
+
source, PRODUCTION_FILE_PATH
|
|
57
|
+
)
|
|
58
|
+
assert issues == [], f"Single capital letter must not be flagged, got: {issues}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_should_not_flag_short_uppercase_acronym() -> None:
|
|
62
|
+
source = "def is_get(method: str) -> bool:\n return method == 'GET'\n"
|
|
63
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
64
|
+
source, PRODUCTION_FILE_PATH
|
|
65
|
+
)
|
|
66
|
+
assert issues == [], f"Short acronym 'GET' must not be flagged, got: {issues}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_not_flag_human_readable_message() -> None:
|
|
70
|
+
source = (
|
|
71
|
+
"def fail() -> None:\n raise RuntimeError('Could not connect to host')\n"
|
|
72
|
+
)
|
|
73
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
74
|
+
source, PRODUCTION_FILE_PATH
|
|
75
|
+
)
|
|
76
|
+
assert issues == [], f"Human-readable message must not be flagged, got: {issues}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_should_not_flag_lowercase_string() -> None:
|
|
80
|
+
source = "def get_label() -> str:\n return 'hello'\n"
|
|
81
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
82
|
+
source, PRODUCTION_FILE_PATH
|
|
83
|
+
)
|
|
84
|
+
assert issues == [], f"Lowercase string must not be flagged, got: {issues}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_should_not_flag_module_level_string() -> None:
|
|
88
|
+
source = "DEFAULT_KEY = 'STRIPE_SECRET'\n"
|
|
89
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
90
|
+
source, PRODUCTION_FILE_PATH
|
|
91
|
+
)
|
|
92
|
+
assert issues == [], (
|
|
93
|
+
f"Module-level string must not be flagged (it IS the constant), got: {issues}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_should_not_flag_docstring() -> None:
|
|
98
|
+
source = (
|
|
99
|
+
"def consume() -> None:\n"
|
|
100
|
+
' """STRIPE_SECRET is documented here for reference."""\n'
|
|
101
|
+
" return None\n"
|
|
102
|
+
)
|
|
103
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
104
|
+
source, PRODUCTION_FILE_PATH
|
|
105
|
+
)
|
|
106
|
+
assert issues == [], f"Docstring must not be flagged, got: {issues}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_should_skip_in_test_files() -> None:
|
|
110
|
+
source = (
|
|
111
|
+
"import os\n"
|
|
112
|
+
"\n"
|
|
113
|
+
"def test_env() -> None:\n"
|
|
114
|
+
" assert os.environ['STRIPE_SECRET'] == 'x'\n"
|
|
115
|
+
)
|
|
116
|
+
issues = code_rules_enforcer.check_string_literal_magic(source, TEST_FILE_PATH)
|
|
117
|
+
assert issues == [], f"Test files exempt, got: {issues}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_should_skip_in_config_files() -> None:
|
|
121
|
+
source = "def env_keys() -> list[str]:\n return ['STRIPE_SECRET', 'DB_HOST']\n"
|
|
122
|
+
issues = code_rules_enforcer.check_string_literal_magic(source, CONFIG_FILE_PATH)
|
|
123
|
+
assert issues == [], f"Config files exempt, got: {issues}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_should_not_flag_default_argument_string_literal() -> None:
|
|
127
|
+
source = (
|
|
128
|
+
"def consume(key: str = 'STRIPE_SECRET') -> str:\n"
|
|
129
|
+
" return key\n"
|
|
130
|
+
)
|
|
131
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
132
|
+
source, PRODUCTION_FILE_PATH
|
|
133
|
+
)
|
|
134
|
+
assert issues == [], (
|
|
135
|
+
f"Default argument value (signature, not body) must not be flagged, got: {issues}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_not_flag_decorator_string_literal() -> None:
|
|
140
|
+
source = (
|
|
141
|
+
"from functools import lru_cache\n"
|
|
142
|
+
"\n"
|
|
143
|
+
"def cache_with_tag(tag: str):\n"
|
|
144
|
+
" return lru_cache\n"
|
|
145
|
+
"\n"
|
|
146
|
+
"@cache_with_tag('STRIPE_SECRET')\n"
|
|
147
|
+
"def consume() -> str:\n"
|
|
148
|
+
" return 'hello'\n"
|
|
149
|
+
)
|
|
150
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
151
|
+
source, PRODUCTION_FILE_PATH
|
|
152
|
+
)
|
|
153
|
+
assert issues == [], (
|
|
154
|
+
f"Decorator argument (not body) must not be flagged, got: {issues}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_should_not_flag_annotation_literal_type_argument() -> None:
|
|
159
|
+
source = (
|
|
160
|
+
"from typing import Literal\n"
|
|
161
|
+
"\n"
|
|
162
|
+
"def consume(method: Literal['STRIPE_SECRET']) -> str:\n"
|
|
163
|
+
" return method\n"
|
|
164
|
+
)
|
|
165
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
166
|
+
source, PRODUCTION_FILE_PATH
|
|
167
|
+
)
|
|
168
|
+
assert issues == [], (
|
|
169
|
+
f"Literal type annotation (signature, not body) must not be flagged, got: {issues}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_should_not_flag_default_arg_of_nested_function_when_scanning_outer() -> None:
|
|
174
|
+
source = (
|
|
175
|
+
"def outer() -> None:\n"
|
|
176
|
+
" def inner(key: str = 'STRIPE_SECRET') -> str:\n"
|
|
177
|
+
" return key\n"
|
|
178
|
+
" return None\n"
|
|
179
|
+
)
|
|
180
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
181
|
+
source, PRODUCTION_FILE_PATH
|
|
182
|
+
)
|
|
183
|
+
assert issues == [], (
|
|
184
|
+
f"Nested function's default arg (signature) must not be flagged from outer scan, got: {issues}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_should_flag_class_attribute_in_nested_class_body() -> None:
|
|
189
|
+
source = (
|
|
190
|
+
"def outer() -> str:\n"
|
|
191
|
+
" class Inner:\n"
|
|
192
|
+
" attribute: str = 'STRIPE_SECRET'\n"
|
|
193
|
+
" return 'no_magic_here'\n"
|
|
194
|
+
)
|
|
195
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
196
|
+
source, PRODUCTION_FILE_PATH
|
|
197
|
+
)
|
|
198
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
199
|
+
f"Nested ClassDef body executes when outer() runs; class attribute must be flagged, got: {issues}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_should_flag_class_attribute_in_nested_class_inside_function() -> None:
|
|
204
|
+
source = (
|
|
205
|
+
"def outer() -> None:\n"
|
|
206
|
+
" class Inner:\n"
|
|
207
|
+
" KEY: str = 'STRIPE_SECRET'\n"
|
|
208
|
+
" return None\n"
|
|
209
|
+
)
|
|
210
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
211
|
+
source, PRODUCTION_FILE_PATH
|
|
212
|
+
)
|
|
213
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
214
|
+
f"Class-level attribute inside a nested ClassDef inside outer fn body must be flagged "
|
|
215
|
+
f"(it executes when outer() runs), got: {issues}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_should_still_flag_literal_in_nested_function_body() -> None:
|
|
220
|
+
source = (
|
|
221
|
+
"def outer() -> str:\n"
|
|
222
|
+
" def inner() -> str:\n"
|
|
223
|
+
" return 'STRIPE_SECRET'\n"
|
|
224
|
+
" return inner()\n"
|
|
225
|
+
)
|
|
226
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
227
|
+
source, PRODUCTION_FILE_PATH
|
|
228
|
+
)
|
|
229
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
230
|
+
f"Inner function's body magic literal must still be flagged via inner scan, got: {issues}"
|
|
231
|
+
)
|
|
232
|
+
assert len(issues) == 1, (
|
|
233
|
+
f"Inner literal must be flagged exactly once (no duplicate from outer walk), got: {issues}"
|
|
234
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Tests for sys.path.insert dedup-guard rule.
|
|
2
|
+
|
|
3
|
+
Bot reviewers on PR #289 flagged grant_project_claude_permissions.py:13
|
|
4
|
+
and revoke_project_claude_permissions.py for unconditionally calling
|
|
5
|
+
sys.path.insert(0, X) without checking whether X was already present.
|
|
6
|
+
The convention in the rest of the repo is to guard the call with
|
|
7
|
+
`if str(X) not in sys.path:` (or equivalent) to avoid pushing the
|
|
8
|
+
same path repeatedly when modules get reloaded.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import pathlib
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
19
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
21
|
+
|
|
22
|
+
_hook_spec = importlib.util.spec_from_file_location(
|
|
23
|
+
"code_rules_enforcer",
|
|
24
|
+
_HOOK_DIRECTORY / "code_rules_enforcer.py",
|
|
25
|
+
)
|
|
26
|
+
assert _hook_spec is not None
|
|
27
|
+
assert _hook_spec.loader is not None
|
|
28
|
+
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
29
|
+
_hook_spec.loader.exec_module(_hook_module)
|
|
30
|
+
check_sys_path_insert_deduplication_guard = _hook_module.check_sys_path_insert_deduplication_guard
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
34
|
+
TEST_FILE_PATH = "packages/app/tests/test_loader.py"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_flag_unguarded_module_level_insert() -> None:
|
|
38
|
+
source = (
|
|
39
|
+
"import sys\n"
|
|
40
|
+
"from pathlib import Path\n"
|
|
41
|
+
"REPOSITORY_ROOT = Path(__file__).resolve().parent\n"
|
|
42
|
+
"sys.path.insert(0, str(REPOSITORY_ROOT))\n"
|
|
43
|
+
)
|
|
44
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
45
|
+
assert any("sys.path.insert" in each_issue for each_issue in issues), (
|
|
46
|
+
f"Expected unguarded sys.path.insert flagged, got: {issues}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_not_flag_when_preceded_by_membership_guard() -> None:
|
|
51
|
+
source = (
|
|
52
|
+
"import sys\n"
|
|
53
|
+
"from pathlib import Path\n"
|
|
54
|
+
"REPOSITORY_ROOT = str(Path(__file__).resolve().parent)\n"
|
|
55
|
+
"if REPOSITORY_ROOT not in sys.path:\n"
|
|
56
|
+
" sys.path.insert(0, REPOSITORY_ROOT)\n"
|
|
57
|
+
)
|
|
58
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
59
|
+
assert issues == [], (
|
|
60
|
+
f"Guarded insert (if X not in sys.path) must not be flagged, got: {issues}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_should_flag_unguarded_function_local_insert() -> None:
|
|
65
|
+
source = (
|
|
66
|
+
"import sys\ndef configure() -> None:\n sys.path.insert(0, '/some/path')\n"
|
|
67
|
+
)
|
|
68
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
69
|
+
assert any("sys.path.insert" in each_issue for each_issue in issues), (
|
|
70
|
+
f"Function-local unguarded insert must be flagged, got: {issues}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_should_not_flag_function_local_when_guarded() -> None:
|
|
75
|
+
source = (
|
|
76
|
+
"import sys\n"
|
|
77
|
+
"def configure() -> None:\n"
|
|
78
|
+
" target = '/some/path'\n"
|
|
79
|
+
" if target not in sys.path:\n"
|
|
80
|
+
" sys.path.insert(0, target)\n"
|
|
81
|
+
)
|
|
82
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
83
|
+
assert issues == [], f"Guarded function-local insert must pass, got: {issues}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_should_not_flag_sys_path_append_or_extend() -> None:
|
|
87
|
+
source = (
|
|
88
|
+
"import sys\nsys.path.append('/some/path')\nsys.path.extend(['/a', '/b'])\n"
|
|
89
|
+
)
|
|
90
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
91
|
+
assert issues == [], (
|
|
92
|
+
f"This rule targets sys.path.insert specifically, append/extend exempt, got: {issues}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_skip_test_files() -> None:
|
|
97
|
+
source = "import sys\nsys.path.insert(0, '/some/path')\n"
|
|
98
|
+
issues = check_sys_path_insert_deduplication_guard(source, TEST_FILE_PATH)
|
|
99
|
+
assert issues == [], (
|
|
100
|
+
f"Test files exempt — fixtures often manipulate sys.path, got: {issues}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
105
|
+
source = "import sys\nsys.path.insert(\n not python\n"
|
|
106
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
107
|
+
assert issues == [], f"Parse failure must return empty, got: {issues}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_should_recognize_str_call_around_path_in_guard() -> None:
|
|
111
|
+
source = (
|
|
112
|
+
"import sys\n"
|
|
113
|
+
"from pathlib import Path\n"
|
|
114
|
+
"ROOT = Path('/x')\n"
|
|
115
|
+
"if str(ROOT) not in sys.path:\n"
|
|
116
|
+
" sys.path.insert(0, str(ROOT))\n"
|
|
117
|
+
)
|
|
118
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
119
|
+
assert issues == [], (
|
|
120
|
+
f"str(ROOT) wrapped in both guard and insert must not flag, got: {issues}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_should_include_line_number_in_issue() -> None:
|
|
125
|
+
source = "import sys\n\n\nsys.path.insert(0, '/x')\n"
|
|
126
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
127
|
+
assert any("Line 4" in each_issue for each_issue in issues), (
|
|
128
|
+
f"Expected line 4 reference, got: {issues}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_should_flag_inverted_membership_guard_inserting_in_then_branch() -> None:
|
|
133
|
+
source = (
|
|
134
|
+
"import sys\n"
|
|
135
|
+
"TARGET = '/some/path'\n"
|
|
136
|
+
"if TARGET in sys.path:\n"
|
|
137
|
+
" sys.path.insert(0, TARGET)\n"
|
|
138
|
+
)
|
|
139
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
140
|
+
assert any("sys.path.insert" in each_issue for each_issue in issues), (
|
|
141
|
+
"`if X in sys.path: sys.path.insert(0, X)` inserts a duplicate when X "
|
|
142
|
+
f"is already present; only `not in` guards then-branch inserts. Got: {issues}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_flag_inverted_membership_guard_with_str_wrapper() -> None:
|
|
147
|
+
source = (
|
|
148
|
+
"import sys\n"
|
|
149
|
+
"from pathlib import Path\n"
|
|
150
|
+
"ROOT = Path('/x')\n"
|
|
151
|
+
"if str(ROOT) in sys.path:\n"
|
|
152
|
+
" sys.path.insert(0, str(ROOT))\n"
|
|
153
|
+
)
|
|
154
|
+
issues = check_sys_path_insert_deduplication_guard(source, PRODUCTION_FILE_PATH)
|
|
155
|
+
assert any("sys.path.insert" in each_issue for each_issue in issues), (
|
|
156
|
+
f"Inverted membership guard with str() wrapper must still flag, got: {issues}"
|
|
157
|
+
)
|