claude-dev-env 1.40.0 → 1.42.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 (66) hide show
  1. package/CLAUDE.md +9 -1
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
  3. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
  4. package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +173 -6
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
  7. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +135 -14
  8. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
  10. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  11. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +4 -2
  12. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  13. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  14. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  15. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  16. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  17. package/hooks/blocking/pr_description_enforcer.py +1 -3
  18. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  19. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  20. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  21. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  22. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  23. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  24. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  25. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  26. package/hooks/config/pr_description_enforcer_constants.py +5 -0
  27. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  28. package/hooks/hooks.json +40 -0
  29. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  30. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  31. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  32. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  33. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  34. package/package.json +1 -1
  35. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  36. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  37. package/skills/bugteam/reference/audit-contract.md +22 -0
  38. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  39. package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
  40. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  41. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
  42. package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
  43. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
  44. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  45. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  46. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  47. package/skills/implement/SKILL.md +66 -0
  48. package/skills/implement/scripts/append_note.py +133 -0
  49. package/skills/implement/scripts/config/__init__.py +0 -0
  50. package/skills/implement/scripts/config/notes_constants.py +12 -0
  51. package/skills/implement/scripts/test_append_note.py +191 -0
  52. package/skills/pr-converge/SKILL.md +8 -2
  53. package/skills/pr-converge/config/constants.py +7 -1
  54. package/skills/pr-converge/reference/state-schema.md +36 -8
  55. package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
  56. package/skills/pr-converge/scripts/check_convergence.py +167 -28
  57. package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
  58. package/skills/pr-converge/scripts/conftest.py +60 -0
  59. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
  60. package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
  61. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  62. package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
  63. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
  64. package/skills/refine/SKILL.md +257 -0
  65. package/skills/refine/templates/implementation-notes-template.html +56 -0
  66. package/skills/refine/templates/plan-template.md +60 -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
+ sys.modules.pop("config", None)
53
+ return _load_module_from_path(
54
+ "grant_project_claude_permissions",
55
+ scripts_directory / "grant_project_claude_permissions.py",
56
+ )
57
+
58
+
59
+ def _load_revoke_module() -> ModuleType:
60
+ scripts_directory = _scripts_directory()
61
+ scripts_directory_str = str(scripts_directory.resolve())
62
+ if scripts_directory_str not in sys.path:
63
+ sys.path.insert(0, scripts_directory_str)
64
+ sys.modules.pop("config", None)
65
+ return _load_module_from_path(
66
+ "revoke_project_claude_permissions",
67
+ scripts_directory / "revoke_project_claude_permissions.py",
68
+ )
69
+
70
+
71
+ def _load_constants_module() -> ModuleType:
72
+ return _load_module_from_path(
73
+ "config.claude_permissions_constants",
74
+ _scripts_directory() / "config" / "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
@@ -26,6 +26,16 @@ def test_exposes_all_permission_allow_tools_tuple() -> None:
26
26
  assert constants_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Write", "Read")
27
27
 
28
28
 
29
+ def test_exposes_all_agent_config_deny_tools_tuple_with_glob() -> None:
30
+ assert constants_module.ALL_AGENT_CONFIG_DENY_TOOLS == (
31
+ "Edit",
32
+ "Write",
33
+ "Read",
34
+ "Glob",
35
+ )
36
+ assert "Glob" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
37
+
38
+
29
39
  def test_auto_mode_environment_entry_template_is_format_string() -> None:
30
40
  rendered_template_text = (
31
41
  constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
@@ -36,6 +46,29 @@ def test_auto_mode_environment_entry_template_is_format_string() -> None:
36
46
  assert ".claude/**" in rendered_template_text
37
47
 
38
48
 
49
+ def test_template_derives_human_readable_pattern_list_from_pattern_tuple() -> None:
50
+ """Every pattern in ALL_AGENT_CONFIG_PATH_PATTERNS must surface in the
51
+ rendered template through its derived human-readable form, and the
52
+ template must still expose the {project_path} placeholder for .format()
53
+ substitution at runtime."""
54
+ template_text: str = constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
55
+ assert "{project_path}" in template_text
56
+ for each_pattern in constants_module.ALL_AGENT_CONFIG_PATH_PATTERNS:
57
+ if each_pattern.endswith("/**"):
58
+ directory_name = each_pattern[: -len("/**")]
59
+ expected_phrase = f"anything under {directory_name}/"
60
+ elif each_pattern == "mcp.json":
61
+ expected_phrase = "the mcp.json file"
62
+ else:
63
+ expected_phrase = each_pattern
64
+ assert expected_phrase in template_text, (
65
+ f"template missing derived phrase for pattern {each_pattern!r}: "
66
+ f"expected {expected_phrase!r}"
67
+ )
68
+ rendered_template_text = template_text.format(project_path="/tmp/x")
69
+ assert "/tmp/x" in rendered_template_text
70
+
71
+
39
72
  def test_get_claude_user_settings_path_ends_in_settings_json() -> None:
40
73
  resolved_settings_path = constants_module.get_claude_user_settings_path()
41
74
  assert resolved_settings_path.name == constants_module.CLAUDE_SETTINGS_FILENAME
@@ -43,7 +43,7 @@ def test_grant_module_guards_sys_path_insert_against_duplicates() -> None:
43
43
  module_source = (
44
44
  Path(__file__).parent.parent / "grant_project_claude_permissions.py"
45
45
  ).read_text(encoding="utf-8")
46
- assert "if str(Path(__file__).resolve().parent) not in sys.path:" in module_source, (
46
+ assert "if parent_directory not in sys.path:" in module_source, (
47
47
  "grant_project_claude_permissions.py must guard sys.path.insert against "
48
48
  "duplicate entries on reload (consistent with sibling modules)"
49
49
  )
@@ -32,7 +32,9 @@ def _load_revoke_module() -> ModuleType:
32
32
  def test_module_imports_constants_from_config_modules() -> None:
33
33
  revoke_module = _load_revoke_module()
34
34
  assert revoke_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Write", "Read")
35
- assert "{project_path}" in revoke_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
35
+ assert revoke_module.AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX == (
36
+ "Trusted local workspace:"
37
+ )
36
38
  assert revoke_module.CLAUDE_SETTINGS_PERMISSIONS_KEY == "permissions"
37
39
 
38
40
 
@@ -43,7 +45,7 @@ def test_revoke_module_guards_sys_path_insert_against_duplicates() -> None:
43
45
  module_source = (
44
46
  Path(__file__).parent.parent / "revoke_project_claude_permissions.py"
45
47
  ).read_text(encoding="utf-8")
46
- assert "if str(Path(__file__).resolve().parent) not in sys.path:" in module_source, (
48
+ assert "if parent_directory not in sys.path:" in module_source, (
47
49
  "revoke_project_claude_permissions.py must guard sys.path.insert against "
48
50
  "duplicate entries on reload (consistent with sibling modules)"
49
51
  )