claude-dev-env 2.4.0 → 2.5.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 +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +2 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +0 -1
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -1,301 +1,301 @@
|
|
|
1
|
-
"""Behavioral tests for stale_worktree_rule_sweep.
|
|
2
|
-
|
|
3
|
-
Drives the real sweep functions against real temp directories and a real
|
|
4
|
-
temp settings file, and drives the real grant and revoke flows end to end.
|
|
5
|
-
A live worktree directory exists on disk; a dead worktree path does not. The
|
|
6
|
-
sweep keeps every rule whose worktree directory still exists and drops every
|
|
7
|
-
rule whose worktree directory is gone, then removes exact duplicates from
|
|
8
|
-
each rule array. The grant and revoke runs perform that sweep before they
|
|
9
|
-
save, so a run whose only effect is the sweep still persists the cleanup.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import importlib.util
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from types import ModuleType
|
|
18
|
-
|
|
19
|
-
import pytest
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _load_scripts_module(module_name: str) -> ModuleType:
|
|
23
|
-
scripts_directory = Path(__file__).parent.parent
|
|
24
|
-
parent_directory = str(scripts_directory.resolve())
|
|
25
|
-
if parent_directory not in sys.path:
|
|
26
|
-
sys.path.insert(0, parent_directory)
|
|
27
|
-
module_path = scripts_directory / f"{module_name}.py"
|
|
28
|
-
specification = importlib.util.spec_from_file_location(module_name, module_path)
|
|
29
|
-
assert specification is not None
|
|
30
|
-
assert specification.loader is not None
|
|
31
|
-
module = importlib.util.module_from_spec(specification)
|
|
32
|
-
specification.loader.exec_module(module)
|
|
33
|
-
return module
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _load_sweep_module() -> ModuleType:
|
|
37
|
-
return _load_scripts_module("stale_worktree_rule_sweep")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
permissions_common = _load_scripts_module("_claude_permissions_common")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _make_live_worktree_directory(worktrees_root: Path) -> Path:
|
|
44
|
-
live_worktree_directory = worktrees_root / "repo" / "live-worktree"
|
|
45
|
-
live_worktree_directory.mkdir(parents=True)
|
|
46
|
-
return live_worktree_directory
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _allow_rule_for(worktree_directory: Path) -> str:
|
|
50
|
-
return f"Edit({worktree_directory}/.claude/**)"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _deny_rule_for(worktree_directory: Path) -> str:
|
|
54
|
-
return f"Read({worktree_directory}/.claude/settings*.json)"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _permission_list(all_settings: dict[str, object], list_key: str) -> list[object]:
|
|
58
|
-
permissions_section = all_settings["permissions"]
|
|
59
|
-
assert isinstance(permissions_section, dict)
|
|
60
|
-
rule_list = permissions_section[list_key]
|
|
61
|
-
assert isinstance(rule_list, list)
|
|
62
|
-
return rule_list
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_sweep_keeps_live_worktree_rules_and_drops_dead_ones(tmp_path: Path) -> None:
|
|
66
|
-
sweep_module = _load_sweep_module()
|
|
67
|
-
live_worktree_directory = _make_live_worktree_directory(tmp_path)
|
|
68
|
-
dead_worktree_directory = tmp_path / "repo" / "dead-worktree"
|
|
69
|
-
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
70
|
-
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
71
|
-
live_deny_rule = _deny_rule_for(live_worktree_directory)
|
|
72
|
-
dead_deny_rule = _deny_rule_for(dead_worktree_directory)
|
|
73
|
-
all_settings: dict[str, object] = {
|
|
74
|
-
"permissions": {
|
|
75
|
-
"allow": [live_allow_rule, dead_allow_rule],
|
|
76
|
-
"deny": [live_deny_rule, dead_deny_rule],
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
80
|
-
all_settings, tmp_path
|
|
81
|
-
)
|
|
82
|
-
assert removed_count == 2
|
|
83
|
-
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
84
|
-
assert _permission_list(all_settings, "deny") == [live_deny_rule]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def test_sweep_keeps_live_flat_worktree_rules_and_drops_dead_ones(tmp_path: Path) -> None:
|
|
88
|
-
sweep_module = _load_sweep_module()
|
|
89
|
-
live_worktree_directory = tmp_path / "flat-live"
|
|
90
|
-
live_worktree_directory.mkdir(parents=True)
|
|
91
|
-
dead_worktree_directory = tmp_path / "flat-dead"
|
|
92
|
-
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
93
|
-
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
94
|
-
all_settings: dict[str, object] = {
|
|
95
|
-
"permissions": {
|
|
96
|
-
"allow": [live_allow_rule, dead_allow_rule],
|
|
97
|
-
"deny": [],
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
|
-
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
101
|
-
all_settings, tmp_path
|
|
102
|
-
)
|
|
103
|
-
assert removed_count == 1
|
|
104
|
-
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_sweep_leaves_rules_for_projects_outside_the_worktrees_root(
|
|
108
|
-
tmp_path: Path,
|
|
109
|
-
) -> None:
|
|
110
|
-
sweep_module = _load_sweep_module()
|
|
111
|
-
unrelated_project_rule = "Edit(/home/developer/project/.claude/**)"
|
|
112
|
-
all_settings: dict[str, object] = {
|
|
113
|
-
"permissions": {"allow": [unrelated_project_rule]},
|
|
114
|
-
}
|
|
115
|
-
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
116
|
-
all_settings, tmp_path
|
|
117
|
-
)
|
|
118
|
-
assert removed_count == 0
|
|
119
|
-
assert _permission_list(all_settings, "allow") == [unrelated_project_rule]
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def test_sweep_removes_exact_duplicate_rules(tmp_path: Path) -> None:
|
|
123
|
-
sweep_module = _load_sweep_module()
|
|
124
|
-
live_worktree_directory = _make_live_worktree_directory(tmp_path)
|
|
125
|
-
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
126
|
-
all_settings: dict[str, object] = {
|
|
127
|
-
"permissions": {"allow": [live_allow_rule, live_allow_rule]},
|
|
128
|
-
}
|
|
129
|
-
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
130
|
-
all_settings, tmp_path
|
|
131
|
-
)
|
|
132
|
-
assert removed_count == 1
|
|
133
|
-
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def test_sweep_ignores_malformed_and_non_string_entries(tmp_path: Path) -> None:
|
|
137
|
-
sweep_module = _load_sweep_module()
|
|
138
|
-
malformed_entry = "not a rule at all"
|
|
139
|
-
all_settings: dict[str, object] = {
|
|
140
|
-
"permissions": {"allow": [malformed_entry, 7, malformed_entry]},
|
|
141
|
-
}
|
|
142
|
-
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
143
|
-
all_settings, tmp_path
|
|
144
|
-
)
|
|
145
|
-
assert removed_count == 1
|
|
146
|
-
assert _permission_list(all_settings, "allow") == [malformed_entry, 7]
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def test_sweep_tolerates_missing_permission_sections(tmp_path: Path) -> None:
|
|
150
|
-
sweep_module = _load_sweep_module()
|
|
151
|
-
all_settings: dict[str, object] = {}
|
|
152
|
-
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
153
|
-
all_settings, tmp_path
|
|
154
|
-
)
|
|
155
|
-
assert removed_count == 0
|
|
156
|
-
assert all_settings == {}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def test_sweep_round_trips_through_a_real_temp_settings_file(tmp_path: Path) -> None:
|
|
160
|
-
sweep_module = _load_sweep_module()
|
|
161
|
-
worktrees_root = tmp_path / "worktrees"
|
|
162
|
-
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
163
|
-
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
164
|
-
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
165
|
-
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
166
|
-
settings_path = tmp_path / "settings.json"
|
|
167
|
-
permissions_common.save_settings(
|
|
168
|
-
settings_path,
|
|
169
|
-
{"permissions": {"allow": [live_allow_rule, dead_allow_rule]}},
|
|
170
|
-
)
|
|
171
|
-
loaded_settings = permissions_common.load_settings(settings_path)
|
|
172
|
-
sweep_module.sweep_and_deduplicate_permission_lists(loaded_settings, worktrees_root)
|
|
173
|
-
permissions_common.save_settings(settings_path, loaded_settings)
|
|
174
|
-
reloaded_settings = permissions_common.load_settings(settings_path)
|
|
175
|
-
assert _permission_list(reloaded_settings, "allow") == [live_allow_rule]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def test_wrapper_resolves_worktrees_root_under_the_user_claude_home(
|
|
179
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
180
|
-
) -> None:
|
|
181
|
-
sweep_module = _load_sweep_module()
|
|
182
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
183
|
-
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
184
|
-
worktrees_root = tmp_path / ".claude" / "worktrees"
|
|
185
|
-
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
186
|
-
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
187
|
-
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
188
|
-
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
189
|
-
all_settings: dict[str, object] = {
|
|
190
|
-
"permissions": {"allow": [live_allow_rule, dead_allow_rule]},
|
|
191
|
-
}
|
|
192
|
-
removed_count = sweep_module.sweep_stale_worktree_rules_from_settings(all_settings)
|
|
193
|
-
assert removed_count == 1
|
|
194
|
-
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def _prepare_home_with_worktrees(
|
|
198
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
199
|
-
) -> tuple[Path, Path, Path]:
|
|
200
|
-
monkeypatch.setenv("HOME", str(tmp_path))
|
|
201
|
-
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
202
|
-
worktrees_root = tmp_path / ".claude" / "worktrees"
|
|
203
|
-
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
204
|
-
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
205
|
-
settings_path = tmp_path / ".claude" / "settings.json"
|
|
206
|
-
return settings_path, live_worktree_directory, dead_worktree_directory
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def _make_fake_project(tmp_path: Path) -> Path:
|
|
210
|
-
fake_project_root = tmp_path / "fake_project"
|
|
211
|
-
(fake_project_root / ".claude").mkdir(parents=True)
|
|
212
|
-
return fake_project_root
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _seed_allow_rules(settings_path: Path, all_rules: list[object]) -> None:
|
|
216
|
-
permissions_common.save_settings(
|
|
217
|
-
settings_path, {"permissions": {"allow": all_rules}}
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def _saved_allow_list(settings_path: Path) -> list[object]:
|
|
222
|
-
saved_settings = permissions_common.load_settings(settings_path)
|
|
223
|
-
permissions_section = saved_settings.get("permissions")
|
|
224
|
-
if not isinstance(permissions_section, dict):
|
|
225
|
-
return []
|
|
226
|
-
allow_list = permissions_section.get("allow")
|
|
227
|
-
return allow_list if isinstance(allow_list, list) else []
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def test_real_grant_run_sweeps_a_dead_worktree_rule_from_saved_settings(
|
|
231
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
232
|
-
) -> None:
|
|
233
|
-
settings_path, live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
234
|
-
tmp_path, monkeypatch
|
|
235
|
-
)
|
|
236
|
-
live_rule = _allow_rule_for(live_directory)
|
|
237
|
-
dead_rule = _allow_rule_for(dead_directory)
|
|
238
|
-
unrelated_rule = "Edit(/home/developer/project/.claude/**)"
|
|
239
|
-
_seed_allow_rules(settings_path, [live_rule, dead_rule, unrelated_rule])
|
|
240
|
-
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
241
|
-
grant_module = _load_scripts_module("grant_project_claude_permissions")
|
|
242
|
-
grant_module.grant_permissions_for_current_directory()
|
|
243
|
-
saved_allow = _saved_allow_list(settings_path)
|
|
244
|
-
assert dead_rule not in saved_allow
|
|
245
|
-
assert live_rule in saved_allow
|
|
246
|
-
assert unrelated_rule in saved_allow
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def test_real_grant_run_with_only_a_sweep_change_still_saves(
|
|
250
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
251
|
-
) -> None:
|
|
252
|
-
settings_path, _live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
253
|
-
tmp_path, monkeypatch
|
|
254
|
-
)
|
|
255
|
-
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
256
|
-
grant_module = _load_scripts_module("grant_project_claude_permissions")
|
|
257
|
-
grant_module.grant_permissions_for_current_directory()
|
|
258
|
-
dead_rule = _allow_rule_for(dead_directory)
|
|
259
|
-
granted_settings = permissions_common.load_settings(settings_path)
|
|
260
|
-
_permission_list(granted_settings, "allow").append(dead_rule)
|
|
261
|
-
permissions_common.save_settings(settings_path, granted_settings)
|
|
262
|
-
grant_module.grant_permissions_for_current_directory()
|
|
263
|
-
assert dead_rule not in _saved_allow_list(settings_path)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def test_real_revoke_run_sweeps_a_dead_worktree_rule_from_saved_settings(
|
|
267
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
268
|
-
) -> None:
|
|
269
|
-
settings_path, live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
270
|
-
tmp_path, monkeypatch
|
|
271
|
-
)
|
|
272
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
273
|
-
monkeypatch.chdir(fake_project_root)
|
|
274
|
-
revoke_module = _load_scripts_module("revoke_project_claude_permissions")
|
|
275
|
-
project_path = str(fake_project_root).replace("\\", "/")
|
|
276
|
-
project_allow_rules = revoke_module.build_permission_rules(
|
|
277
|
-
project_path, revoke_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
278
|
-
)
|
|
279
|
-
live_rule = _allow_rule_for(live_directory)
|
|
280
|
-
dead_rule = _allow_rule_for(dead_directory)
|
|
281
|
-
_seed_allow_rules(settings_path, [*project_allow_rules, live_rule, dead_rule])
|
|
282
|
-
revoke_module.revoke_permissions_for_current_directory()
|
|
283
|
-
saved_allow = _saved_allow_list(settings_path)
|
|
284
|
-
assert dead_rule not in saved_allow
|
|
285
|
-
assert live_rule in saved_allow
|
|
286
|
-
for each_project_rule in project_allow_rules:
|
|
287
|
-
assert each_project_rule not in saved_allow
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def test_real_revoke_run_with_only_a_sweep_change_still_saves(
|
|
291
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
292
|
-
) -> None:
|
|
293
|
-
settings_path, _live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
294
|
-
tmp_path, monkeypatch
|
|
295
|
-
)
|
|
296
|
-
dead_rule = _allow_rule_for(dead_directory)
|
|
297
|
-
_seed_allow_rules(settings_path, [dead_rule])
|
|
298
|
-
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
299
|
-
revoke_module = _load_scripts_module("revoke_project_claude_permissions")
|
|
300
|
-
revoke_module.revoke_permissions_for_current_directory()
|
|
301
|
-
assert dead_rule not in _saved_allow_list(settings_path)
|
|
1
|
+
"""Behavioral tests for stale_worktree_rule_sweep.
|
|
2
|
+
|
|
3
|
+
Drives the real sweep functions against real temp directories and a real
|
|
4
|
+
temp settings file, and drives the real grant and revoke flows end to end.
|
|
5
|
+
A live worktree directory exists on disk; a dead worktree path does not. The
|
|
6
|
+
sweep keeps every rule whose worktree directory still exists and drops every
|
|
7
|
+
rule whose worktree directory is gone, then removes exact duplicates from
|
|
8
|
+
each rule array. The grant and revoke runs perform that sweep before they
|
|
9
|
+
save, so a run whose only effect is the sweep still persists the cleanup.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_scripts_module(module_name: str) -> ModuleType:
|
|
23
|
+
scripts_directory = Path(__file__).parent.parent
|
|
24
|
+
parent_directory = str(scripts_directory.resolve())
|
|
25
|
+
if parent_directory not in sys.path:
|
|
26
|
+
sys.path.insert(0, parent_directory)
|
|
27
|
+
module_path = scripts_directory / f"{module_name}.py"
|
|
28
|
+
specification = importlib.util.spec_from_file_location(module_name, module_path)
|
|
29
|
+
assert specification is not None
|
|
30
|
+
assert specification.loader is not None
|
|
31
|
+
module = importlib.util.module_from_spec(specification)
|
|
32
|
+
specification.loader.exec_module(module)
|
|
33
|
+
return module
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_sweep_module() -> ModuleType:
|
|
37
|
+
return _load_scripts_module("stale_worktree_rule_sweep")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
permissions_common = _load_scripts_module("_claude_permissions_common")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _make_live_worktree_directory(worktrees_root: Path) -> Path:
|
|
44
|
+
live_worktree_directory = worktrees_root / "repo" / "live-worktree"
|
|
45
|
+
live_worktree_directory.mkdir(parents=True)
|
|
46
|
+
return live_worktree_directory
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _allow_rule_for(worktree_directory: Path) -> str:
|
|
50
|
+
return f"Edit({worktree_directory}/.claude/**)"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _deny_rule_for(worktree_directory: Path) -> str:
|
|
54
|
+
return f"Read({worktree_directory}/.claude/settings*.json)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _permission_list(all_settings: dict[str, object], list_key: str) -> list[object]:
|
|
58
|
+
permissions_section = all_settings["permissions"]
|
|
59
|
+
assert isinstance(permissions_section, dict)
|
|
60
|
+
rule_list = permissions_section[list_key]
|
|
61
|
+
assert isinstance(rule_list, list)
|
|
62
|
+
return rule_list
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_sweep_keeps_live_worktree_rules_and_drops_dead_ones(tmp_path: Path) -> None:
|
|
66
|
+
sweep_module = _load_sweep_module()
|
|
67
|
+
live_worktree_directory = _make_live_worktree_directory(tmp_path)
|
|
68
|
+
dead_worktree_directory = tmp_path / "repo" / "dead-worktree"
|
|
69
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
70
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
71
|
+
live_deny_rule = _deny_rule_for(live_worktree_directory)
|
|
72
|
+
dead_deny_rule = _deny_rule_for(dead_worktree_directory)
|
|
73
|
+
all_settings: dict[str, object] = {
|
|
74
|
+
"permissions": {
|
|
75
|
+
"allow": [live_allow_rule, dead_allow_rule],
|
|
76
|
+
"deny": [live_deny_rule, dead_deny_rule],
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
80
|
+
all_settings, tmp_path
|
|
81
|
+
)
|
|
82
|
+
assert removed_count == 2
|
|
83
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
84
|
+
assert _permission_list(all_settings, "deny") == [live_deny_rule]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_sweep_keeps_live_flat_worktree_rules_and_drops_dead_ones(tmp_path: Path) -> None:
|
|
88
|
+
sweep_module = _load_sweep_module()
|
|
89
|
+
live_worktree_directory = tmp_path / "flat-live"
|
|
90
|
+
live_worktree_directory.mkdir(parents=True)
|
|
91
|
+
dead_worktree_directory = tmp_path / "flat-dead"
|
|
92
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
93
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
94
|
+
all_settings: dict[str, object] = {
|
|
95
|
+
"permissions": {
|
|
96
|
+
"allow": [live_allow_rule, dead_allow_rule],
|
|
97
|
+
"deny": [],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
101
|
+
all_settings, tmp_path
|
|
102
|
+
)
|
|
103
|
+
assert removed_count == 1
|
|
104
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_sweep_leaves_rules_for_projects_outside_the_worktrees_root(
|
|
108
|
+
tmp_path: Path,
|
|
109
|
+
) -> None:
|
|
110
|
+
sweep_module = _load_sweep_module()
|
|
111
|
+
unrelated_project_rule = "Edit(/home/developer/project/.claude/**)"
|
|
112
|
+
all_settings: dict[str, object] = {
|
|
113
|
+
"permissions": {"allow": [unrelated_project_rule]},
|
|
114
|
+
}
|
|
115
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
116
|
+
all_settings, tmp_path
|
|
117
|
+
)
|
|
118
|
+
assert removed_count == 0
|
|
119
|
+
assert _permission_list(all_settings, "allow") == [unrelated_project_rule]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_sweep_removes_exact_duplicate_rules(tmp_path: Path) -> None:
|
|
123
|
+
sweep_module = _load_sweep_module()
|
|
124
|
+
live_worktree_directory = _make_live_worktree_directory(tmp_path)
|
|
125
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
126
|
+
all_settings: dict[str, object] = {
|
|
127
|
+
"permissions": {"allow": [live_allow_rule, live_allow_rule]},
|
|
128
|
+
}
|
|
129
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
130
|
+
all_settings, tmp_path
|
|
131
|
+
)
|
|
132
|
+
assert removed_count == 1
|
|
133
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_sweep_ignores_malformed_and_non_string_entries(tmp_path: Path) -> None:
|
|
137
|
+
sweep_module = _load_sweep_module()
|
|
138
|
+
malformed_entry = "not a rule at all"
|
|
139
|
+
all_settings: dict[str, object] = {
|
|
140
|
+
"permissions": {"allow": [malformed_entry, 7, malformed_entry]},
|
|
141
|
+
}
|
|
142
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
143
|
+
all_settings, tmp_path
|
|
144
|
+
)
|
|
145
|
+
assert removed_count == 1
|
|
146
|
+
assert _permission_list(all_settings, "allow") == [malformed_entry, 7]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_sweep_tolerates_missing_permission_sections(tmp_path: Path) -> None:
|
|
150
|
+
sweep_module = _load_sweep_module()
|
|
151
|
+
all_settings: dict[str, object] = {}
|
|
152
|
+
removed_count = sweep_module.sweep_and_deduplicate_permission_lists(
|
|
153
|
+
all_settings, tmp_path
|
|
154
|
+
)
|
|
155
|
+
assert removed_count == 0
|
|
156
|
+
assert all_settings == {}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_sweep_round_trips_through_a_real_temp_settings_file(tmp_path: Path) -> None:
|
|
160
|
+
sweep_module = _load_sweep_module()
|
|
161
|
+
worktrees_root = tmp_path / "worktrees"
|
|
162
|
+
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
163
|
+
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
164
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
165
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
166
|
+
settings_path = tmp_path / "settings.json"
|
|
167
|
+
permissions_common.save_settings(
|
|
168
|
+
settings_path,
|
|
169
|
+
{"permissions": {"allow": [live_allow_rule, dead_allow_rule]}},
|
|
170
|
+
)
|
|
171
|
+
loaded_settings = permissions_common.load_settings(settings_path)
|
|
172
|
+
sweep_module.sweep_and_deduplicate_permission_lists(loaded_settings, worktrees_root)
|
|
173
|
+
permissions_common.save_settings(settings_path, loaded_settings)
|
|
174
|
+
reloaded_settings = permissions_common.load_settings(settings_path)
|
|
175
|
+
assert _permission_list(reloaded_settings, "allow") == [live_allow_rule]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_wrapper_resolves_worktrees_root_under_the_user_claude_home(
|
|
179
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
180
|
+
) -> None:
|
|
181
|
+
sweep_module = _load_sweep_module()
|
|
182
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
183
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
184
|
+
worktrees_root = tmp_path / ".claude" / "worktrees"
|
|
185
|
+
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
186
|
+
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
187
|
+
live_allow_rule = _allow_rule_for(live_worktree_directory)
|
|
188
|
+
dead_allow_rule = _allow_rule_for(dead_worktree_directory)
|
|
189
|
+
all_settings: dict[str, object] = {
|
|
190
|
+
"permissions": {"allow": [live_allow_rule, dead_allow_rule]},
|
|
191
|
+
}
|
|
192
|
+
removed_count = sweep_module.sweep_stale_worktree_rules_from_settings(all_settings)
|
|
193
|
+
assert removed_count == 1
|
|
194
|
+
assert _permission_list(all_settings, "allow") == [live_allow_rule]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _prepare_home_with_worktrees(
|
|
198
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
199
|
+
) -> tuple[Path, Path, Path]:
|
|
200
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
201
|
+
monkeypatch.setenv("USERPROFILE", str(tmp_path))
|
|
202
|
+
worktrees_root = tmp_path / ".claude" / "worktrees"
|
|
203
|
+
live_worktree_directory = _make_live_worktree_directory(worktrees_root)
|
|
204
|
+
dead_worktree_directory = worktrees_root / "repo" / "dead-worktree"
|
|
205
|
+
settings_path = tmp_path / ".claude" / "settings.json"
|
|
206
|
+
return settings_path, live_worktree_directory, dead_worktree_directory
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _make_fake_project(tmp_path: Path) -> Path:
|
|
210
|
+
fake_project_root = tmp_path / "fake_project"
|
|
211
|
+
(fake_project_root / ".claude").mkdir(parents=True)
|
|
212
|
+
return fake_project_root
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _seed_allow_rules(settings_path: Path, all_rules: list[object]) -> None:
|
|
216
|
+
permissions_common.save_settings(
|
|
217
|
+
settings_path, {"permissions": {"allow": all_rules}}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _saved_allow_list(settings_path: Path) -> list[object]:
|
|
222
|
+
saved_settings = permissions_common.load_settings(settings_path)
|
|
223
|
+
permissions_section = saved_settings.get("permissions")
|
|
224
|
+
if not isinstance(permissions_section, dict):
|
|
225
|
+
return []
|
|
226
|
+
allow_list = permissions_section.get("allow")
|
|
227
|
+
return allow_list if isinstance(allow_list, list) else []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_real_grant_run_sweeps_a_dead_worktree_rule_from_saved_settings(
|
|
231
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
232
|
+
) -> None:
|
|
233
|
+
settings_path, live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
234
|
+
tmp_path, monkeypatch
|
|
235
|
+
)
|
|
236
|
+
live_rule = _allow_rule_for(live_directory)
|
|
237
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
238
|
+
unrelated_rule = "Edit(/home/developer/project/.claude/**)"
|
|
239
|
+
_seed_allow_rules(settings_path, [live_rule, dead_rule, unrelated_rule])
|
|
240
|
+
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
241
|
+
grant_module = _load_scripts_module("grant_project_claude_permissions")
|
|
242
|
+
grant_module.grant_permissions_for_current_directory()
|
|
243
|
+
saved_allow = _saved_allow_list(settings_path)
|
|
244
|
+
assert dead_rule not in saved_allow
|
|
245
|
+
assert live_rule in saved_allow
|
|
246
|
+
assert unrelated_rule in saved_allow
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_real_grant_run_with_only_a_sweep_change_still_saves(
|
|
250
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
251
|
+
) -> None:
|
|
252
|
+
settings_path, _live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
253
|
+
tmp_path, monkeypatch
|
|
254
|
+
)
|
|
255
|
+
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
256
|
+
grant_module = _load_scripts_module("grant_project_claude_permissions")
|
|
257
|
+
grant_module.grant_permissions_for_current_directory()
|
|
258
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
259
|
+
granted_settings = permissions_common.load_settings(settings_path)
|
|
260
|
+
_permission_list(granted_settings, "allow").append(dead_rule)
|
|
261
|
+
permissions_common.save_settings(settings_path, granted_settings)
|
|
262
|
+
grant_module.grant_permissions_for_current_directory()
|
|
263
|
+
assert dead_rule not in _saved_allow_list(settings_path)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_real_revoke_run_sweeps_a_dead_worktree_rule_from_saved_settings(
|
|
267
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
268
|
+
) -> None:
|
|
269
|
+
settings_path, live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
270
|
+
tmp_path, monkeypatch
|
|
271
|
+
)
|
|
272
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
273
|
+
monkeypatch.chdir(fake_project_root)
|
|
274
|
+
revoke_module = _load_scripts_module("revoke_project_claude_permissions")
|
|
275
|
+
project_path = str(fake_project_root).replace("\\", "/")
|
|
276
|
+
project_allow_rules = revoke_module.build_permission_rules(
|
|
277
|
+
project_path, revoke_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
278
|
+
)
|
|
279
|
+
live_rule = _allow_rule_for(live_directory)
|
|
280
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
281
|
+
_seed_allow_rules(settings_path, [*project_allow_rules, live_rule, dead_rule])
|
|
282
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
283
|
+
saved_allow = _saved_allow_list(settings_path)
|
|
284
|
+
assert dead_rule not in saved_allow
|
|
285
|
+
assert live_rule in saved_allow
|
|
286
|
+
for each_project_rule in project_allow_rules:
|
|
287
|
+
assert each_project_rule not in saved_allow
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_real_revoke_run_with_only_a_sweep_change_still_saves(
|
|
291
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
292
|
+
) -> None:
|
|
293
|
+
settings_path, _live_directory, dead_directory = _prepare_home_with_worktrees(
|
|
294
|
+
tmp_path, monkeypatch
|
|
295
|
+
)
|
|
296
|
+
dead_rule = _allow_rule_for(dead_directory)
|
|
297
|
+
_seed_allow_rules(settings_path, [dead_rule])
|
|
298
|
+
monkeypatch.chdir(_make_fake_project(tmp_path))
|
|
299
|
+
revoke_module = _load_scripts_module("revoke_project_claude_permissions")
|
|
300
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
301
|
+
assert dead_rule not in _saved_allow_list(settings_path)
|