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.
Files changed (148) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/agents/docs-agent.md +1 -1
  3. package/agents/project-docs-analyzer.md +0 -1
  4. package/agents/skill-to-agent-converter.md +0 -1
  5. package/bin/install.mjs +28 -8
  6. package/bin/install.test.mjs +9 -1
  7. package/commands/initialize.md +0 -1
  8. package/commands/readability-review.md +4 -4
  9. package/commands/review-plan.md +2 -4
  10. package/commands/stubcheck.md +1 -2
  11. package/docs/CODE_RULES.md +3 -0
  12. package/docs/agents-md-alignment-plan.md +123 -0
  13. package/hooks/blocking/code_rules_enforcer.py +686 -60
  14. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  15. package/hooks/blocking/test_code_rules_enforcer.py +273 -39
  16. package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
  17. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  18. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  19. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
  20. package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
  21. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
  22. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
  23. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  24. package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
  25. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
  26. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
  27. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
  28. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
  29. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  31. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  32. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  33. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  34. package/hooks/config/banned_identifiers_constants.py +24 -0
  35. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  36. package/hooks/config/hook_log_extractor_constants.py +1 -1
  37. package/hooks/config/pre_tool_use_stdin.py +48 -0
  38. package/hooks/config/setup_project_paths_constants.py +4 -0
  39. package/hooks/config/stuttering_check_config.py +14 -0
  40. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  41. package/hooks/config/sys_path_insert_constants.py +4 -0
  42. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  43. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  44. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  45. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  46. package/hooks/config/unused_module_import_constants.py +7 -0
  47. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  48. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  49. package/hooks/git-hooks/config.py +3 -3
  50. package/hooks/git-hooks/test_gate_utils.py +10 -10
  51. package/hooks/mypy.ini +2 -0
  52. package/package.json +1 -1
  53. package/rules/gh-paginate.md +125 -0
  54. package/skills/bugteam/CONSTRAINTS.md +12 -6
  55. package/skills/bugteam/PROMPTS.md +0 -39
  56. package/skills/bugteam/SKILL.md +93 -125
  57. package/skills/bugteam/SKILL_EVALS.md +25 -23
  58. package/skills/bugteam/reference/README.md +2 -0
  59. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  60. package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
  61. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  62. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  63. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  64. package/skills/bugteam/test_skill_additions.py +13 -4
  65. package/skills/bugteam/test_team_lifecycle.py +94 -0
  66. package/skills/findbugs/SKILL.md +3 -3
  67. package/skills/fixbugs/SKILL.md +4 -4
  68. package/skills/monitor-open-prs/SKILL.md +32 -2
  69. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  70. package/skills/pr-converge/SKILL.md +576 -95
  71. package/skills/pr-converge/scripts/README.md +145 -0
  72. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  73. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  74. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  75. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  76. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  77. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  78. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  79. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  80. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  81. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  82. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  83. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  84. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  85. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  86. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  87. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  88. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  89. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  90. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  91. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  92. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  93. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  94. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  95. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  96. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  97. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  98. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  99. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  100. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  101. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  102. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  103. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  104. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  105. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  106. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  107. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  108. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  109. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  110. package/skills/pr-converge/test_team_lifecycle.py +47 -0
  111. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  112. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  113. package/skills/qbug/SKILL.md +4 -4
  114. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  115. package/skills/resume-review/SKILL.md +261 -0
  116. package/agents/agent-writer.md +0 -157
  117. package/agents/config-centralizer.md +0 -686
  118. package/agents/config-extraction-agent.md +0 -225
  119. package/agents/doc-orchestrator.md +0 -47
  120. package/agents/docx-agent.md +0 -211
  121. package/agents/magic-value-eliminator-agent.md +0 -72
  122. package/agents/mandatory-agent-workflow-agent.md +0 -88
  123. package/agents/parallel-workflow-coordinator.md +0 -779
  124. package/agents/pdf-agent.md +0 -302
  125. package/agents/project-context-loader.md +0 -238
  126. package/agents/readability-review-agent.md +0 -76
  127. package/agents/refactoring-specialist.md +0 -69
  128. package/agents/right-sized-engineer.md +0 -129
  129. package/agents/session-continuity-manager.md +0 -53
  130. package/agents/stub-detector-agent.md +0 -140
  131. package/agents/tdd-test-writer.md +0 -62
  132. package/agents/test-data-builder.md +0 -68
  133. package/agents/tooling-builder.md +0 -78
  134. package/agents/validation-expert.md +0 -71
  135. package/agents/xlsx-agent.md +0 -169
  136. package/skills/bugteam/scripts/README.md +0 -58
  137. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  138. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  139. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  140. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  141. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  142. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  143. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  144. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  145. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  146. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  147. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  148. /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
+ )