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
|
@@ -15,10 +15,12 @@ if parent_directory not in sys.path:
|
|
|
15
15
|
sys.path.insert(0, parent_directory)
|
|
16
16
|
|
|
17
17
|
from _claude_permissions_common import ( # noqa: E402
|
|
18
|
+
all_project_path_aliases_for_reap,
|
|
18
19
|
build_agent_config_deny_rules,
|
|
19
20
|
build_permission_rules,
|
|
20
21
|
exit_with_error,
|
|
21
22
|
get_current_project_path,
|
|
23
|
+
is_inert_file_permission_rule,
|
|
22
24
|
is_trust_entry_for_project,
|
|
23
25
|
is_valid_project_root,
|
|
24
26
|
load_settings,
|
|
@@ -29,8 +31,12 @@ from _claude_permissions_common import ( # noqa: E402
|
|
|
29
31
|
from pr_loop_shared_constants.claude_permissions_constants import (
|
|
30
32
|
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
31
33
|
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
34
|
+
ALL_LEGACY_PERMISSION_REAP_TOOLS,
|
|
32
35
|
ALL_PERMISSION_ALLOW_TOOLS,
|
|
33
36
|
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
37
|
+
INERT_RULES_REMOVED_LOG_PREFIX,
|
|
38
|
+
PROJECT_LOCAL_INERT_RULES_REMOVED_LOG_PREFIX,
|
|
39
|
+
get_claude_project_local_settings_path,
|
|
34
40
|
get_claude_user_settings_path,
|
|
35
41
|
)
|
|
36
42
|
from pr_loop_shared_constants.claude_settings_keys_constants import (
|
|
@@ -46,6 +52,179 @@ from stale_worktree_rule_sweep import ( # noqa: E402
|
|
|
46
52
|
)
|
|
47
53
|
|
|
48
54
|
|
|
55
|
+
def _reap_tool_names(all_mint_tool_names: tuple[str, ...]) -> tuple[str, ...]:
|
|
56
|
+
"""Return mint tool names plus legacy Write/Glob forms a revoke run reaps.
|
|
57
|
+
|
|
58
|
+
Keeps mint tuples (Edit/Read) untouched and widens only the revoke surface
|
|
59
|
+
so inert pre-#158 Write()/Glob() rules are removed with the current mint set.
|
|
60
|
+
"""
|
|
61
|
+
return all_mint_tool_names + ALL_LEGACY_PERMISSION_REAP_TOOLS
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def remove_inert_file_permission_rules(all_settings: dict[str, object]) -> int:
|
|
65
|
+
"""Remove Write/Glob/NotebookEdit allow and deny rules path-agnostically.
|
|
66
|
+
|
|
67
|
+
::
|
|
68
|
+
|
|
69
|
+
remove_inert_file_permission_rules(
|
|
70
|
+
{"permissions": {"allow": ["Write(/wt/**)", "Edit(/p/.claude/**)"]}}
|
|
71
|
+
) # ok: 1, allow left with Edit only
|
|
72
|
+
|
|
73
|
+
Claude only honors Edit for file permission checks; leftover Write, Glob,
|
|
74
|
+
and NotebookEdit rules from older grants only produce startup warnings.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
all_settings: Parsed Claude user settings dictionary (mutated in place).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Number of inert rules removed from allow and deny combined.
|
|
81
|
+
"""
|
|
82
|
+
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
83
|
+
if not isinstance(permissions_section, dict):
|
|
84
|
+
return 0
|
|
85
|
+
total_removed_count = 0
|
|
86
|
+
for each_list_key in (CLAUDE_SETTINGS_ALLOW_KEY, CLAUDE_SETTINGS_DENY_KEY):
|
|
87
|
+
existing_rule_list = permissions_section.get(each_list_key)
|
|
88
|
+
if not isinstance(existing_rule_list, list):
|
|
89
|
+
continue
|
|
90
|
+
total_removed_count += remove_matching_entries_from_list(
|
|
91
|
+
existing_rule_list, is_inert_file_permission_rule
|
|
92
|
+
)
|
|
93
|
+
return total_removed_count
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_project_scoped_allow_rules_for_reap(project_path: str) -> list[str]:
|
|
97
|
+
"""Build allow rules for every path alias of *project_path* (incl. $HOME).
|
|
98
|
+
|
|
99
|
+
::
|
|
100
|
+
|
|
101
|
+
build_project_scoped_allow_rules_for_reap("/repo")
|
|
102
|
+
# ok: includes Edit(/repo/.claude/**) and Write(/repo/.claude/**)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
project_path: POSIX project root from get_current_project_path.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Allow rule strings covering mint plus legacy tools for each path alias.
|
|
109
|
+
"""
|
|
110
|
+
all_allow_rules: list[str] = []
|
|
111
|
+
for each_path_alias in all_project_path_aliases_for_reap(project_path):
|
|
112
|
+
all_allow_rules.extend(
|
|
113
|
+
build_permission_rules(
|
|
114
|
+
each_path_alias, _reap_tool_names(ALL_PERMISSION_ALLOW_TOOLS)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
return all_allow_rules
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_project_scoped_deny_rules_for_reap(project_path: str) -> list[str]:
|
|
121
|
+
"""Build agent-config deny rules for every path alias of *project_path*.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
project_path: POSIX project root from get_current_project_path.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Deny rule strings for mint plus legacy tools across path aliases.
|
|
128
|
+
"""
|
|
129
|
+
all_deny_rules: list[str] = []
|
|
130
|
+
for each_path_alias in all_project_path_aliases_for_reap(project_path):
|
|
131
|
+
all_deny_rules.extend(
|
|
132
|
+
build_agent_config_deny_rules(
|
|
133
|
+
each_path_alias,
|
|
134
|
+
_reap_tool_names(ALL_AGENT_CONFIG_DENY_TOOLS),
|
|
135
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return all_deny_rules
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _remove_directory_and_trust_for_path_aliases(
|
|
142
|
+
all_settings: dict[str, object], project_path: str
|
|
143
|
+
) -> tuple[int, int]:
|
|
144
|
+
"""Remove additionalDirectories and trust entries for each path alias."""
|
|
145
|
+
directories_removed_count = 0
|
|
146
|
+
environment_entries_removed_count = 0
|
|
147
|
+
for each_path_alias in all_project_path_aliases_for_reap(project_path):
|
|
148
|
+
directories_removed_count += remove_directory_from_additional_directories(
|
|
149
|
+
all_settings, each_path_alias
|
|
150
|
+
)
|
|
151
|
+
environment_entries_removed_count += remove_trust_entries_for_project(
|
|
152
|
+
all_settings, each_path_alias, AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX
|
|
153
|
+
)
|
|
154
|
+
return directories_removed_count, environment_entries_removed_count
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def strip_inert_rules_from_settings_file(settings_path: Path) -> int:
|
|
158
|
+
"""Load *settings_path*, strip inert tools, save when anything changed.
|
|
159
|
+
|
|
160
|
+
::
|
|
161
|
+
|
|
162
|
+
strip_inert_rules_from_settings_file(missing_path) # ok: 0
|
|
163
|
+
strip_inert_rules_from_settings_file(local_with_Write) # ok: N > 0
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
settings_path: User or project-local Claude settings JSON path.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Number of inert rules removed; 0 when the file is missing or clean.
|
|
170
|
+
"""
|
|
171
|
+
if not settings_path.is_file():
|
|
172
|
+
return 0
|
|
173
|
+
settings = load_settings(settings_path)
|
|
174
|
+
inert_rules_removed_count = remove_inert_file_permission_rules(settings)
|
|
175
|
+
if inert_rules_removed_count == 0:
|
|
176
|
+
return 0
|
|
177
|
+
prune_empty_list_then_empty_section(
|
|
178
|
+
settings,
|
|
179
|
+
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
180
|
+
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
181
|
+
)
|
|
182
|
+
prune_empty_list_then_empty_section(
|
|
183
|
+
settings,
|
|
184
|
+
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
185
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
186
|
+
)
|
|
187
|
+
save_settings(settings_path, settings)
|
|
188
|
+
return inert_rules_removed_count
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _print_revoke_summary(
|
|
192
|
+
project_path: str,
|
|
193
|
+
claude_user_settings_path: Path,
|
|
194
|
+
allow_rules_removed_count: int,
|
|
195
|
+
permission_rules_count: int,
|
|
196
|
+
deny_rules_removed_count: int,
|
|
197
|
+
agent_config_deny_rules_count: int,
|
|
198
|
+
inert_rules_removed_count: int,
|
|
199
|
+
project_local_inert_rules_removed_count: int,
|
|
200
|
+
directories_removed_count: int,
|
|
201
|
+
environment_entries_removed_count: int,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Print the post-revoke summary lines to stdout."""
|
|
204
|
+
inert_rules_removed_log_prefix = INERT_RULES_REMOVED_LOG_PREFIX
|
|
205
|
+
project_local_inert_rules_removed_log_prefix = (
|
|
206
|
+
PROJECT_LOCAL_INERT_RULES_REMOVED_LOG_PREFIX
|
|
207
|
+
)
|
|
208
|
+
print(f"Project path: {project_path}")
|
|
209
|
+
print(f"Settings file: {claude_user_settings_path}")
|
|
210
|
+
print(
|
|
211
|
+
f"Allow rules removed: {allow_rules_removed_count} of {permission_rules_count}"
|
|
212
|
+
)
|
|
213
|
+
print(
|
|
214
|
+
f"Deny rules removed: {deny_rules_removed_count} of "
|
|
215
|
+
f"{agent_config_deny_rules_count}"
|
|
216
|
+
)
|
|
217
|
+
print(f"{inert_rules_removed_log_prefix}{inert_rules_removed_count}")
|
|
218
|
+
print(
|
|
219
|
+
f"{project_local_inert_rules_removed_log_prefix}"
|
|
220
|
+
f"{project_local_inert_rules_removed_count}"
|
|
221
|
+
)
|
|
222
|
+
print(f"Additional directories removed: {directories_removed_count}")
|
|
223
|
+
print(
|
|
224
|
+
f"Auto-mode environment entries removed: {environment_entries_removed_count}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
49
228
|
def remove_values_from_list(
|
|
50
229
|
all_target_list: list[object], all_values_to_remove: set[str]
|
|
51
230
|
) -> int:
|
|
@@ -192,13 +371,79 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
|
192
371
|
)
|
|
193
372
|
|
|
194
373
|
|
|
374
|
+
def _apply_user_settings_revoke(
|
|
375
|
+
all_settings: dict[str, object],
|
|
376
|
+
project_path: str,
|
|
377
|
+
all_permission_rules: list[str],
|
|
378
|
+
all_agent_config_deny_rules: list[str],
|
|
379
|
+
) -> tuple[int, int, int, int, int, int]:
|
|
380
|
+
"""Apply project-scoped and inert removals to the user settings dict.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Counts: allow, deny, directories, environment, worktree_sweep, inert.
|
|
384
|
+
"""
|
|
385
|
+
worktree_sweep_removed_count = sweep_stale_worktree_rules_from_settings(
|
|
386
|
+
all_settings
|
|
387
|
+
)
|
|
388
|
+
inert_rules_removed_count = remove_inert_file_permission_rules(all_settings)
|
|
389
|
+
allow_rules_removed_count = remove_rules_from_allow_list(
|
|
390
|
+
all_settings, all_permission_rules
|
|
391
|
+
)
|
|
392
|
+
deny_rules_removed_count = remove_rules_from_deny_list(
|
|
393
|
+
all_settings, all_agent_config_deny_rules
|
|
394
|
+
)
|
|
395
|
+
directories_removed_count, environment_entries_removed_count = (
|
|
396
|
+
_remove_directory_and_trust_for_path_aliases(all_settings, project_path)
|
|
397
|
+
)
|
|
398
|
+
return (
|
|
399
|
+
allow_rules_removed_count,
|
|
400
|
+
deny_rules_removed_count,
|
|
401
|
+
directories_removed_count,
|
|
402
|
+
environment_entries_removed_count,
|
|
403
|
+
worktree_sweep_removed_count,
|
|
404
|
+
inert_rules_removed_count,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _require_valid_project_root_or_exit() -> Path:
|
|
409
|
+
"""Return cwd when it is a project root, otherwise exit with an error."""
|
|
410
|
+
project_root_path = Path.cwd()
|
|
411
|
+
if is_valid_project_root(project_root_path):
|
|
412
|
+
return project_root_path
|
|
413
|
+
print(
|
|
414
|
+
f"ERROR: cwd {project_root_path} is not a project root "
|
|
415
|
+
f"(no .git or .claude). Run from a project root.",
|
|
416
|
+
file=sys.stderr,
|
|
417
|
+
)
|
|
418
|
+
raise SystemExit(1)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _sum_user_settings_change_counts(
|
|
422
|
+
allow_rules_removed_count: int,
|
|
423
|
+
deny_rules_removed_count: int,
|
|
424
|
+
directories_removed_count: int,
|
|
425
|
+
environment_entries_removed_count: int,
|
|
426
|
+
worktree_sweep_removed_count: int,
|
|
427
|
+
inert_rules_removed_count: int,
|
|
428
|
+
) -> int:
|
|
429
|
+
"""Return the total number of user-settings mutations for this revoke."""
|
|
430
|
+
return (
|
|
431
|
+
allow_rules_removed_count
|
|
432
|
+
+ deny_rules_removed_count
|
|
433
|
+
+ directories_removed_count
|
|
434
|
+
+ environment_entries_removed_count
|
|
435
|
+
+ worktree_sweep_removed_count
|
|
436
|
+
+ inert_rules_removed_count
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
195
440
|
def revoke_permissions_for_current_directory() -> None:
|
|
196
441
|
"""Revoke permissions previously granted for the current project directory.
|
|
197
442
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
443
|
+
Removes matching allow/deny rules from ~/.claude/settings.json, strips
|
|
444
|
+
inert Write/Glob/NotebookEdit rules from user settings and project
|
|
445
|
+
``.claude/settings.local.json`` when present, clears trust entries, and
|
|
446
|
+
prunes empty sections.
|
|
202
447
|
|
|
203
448
|
Raises:
|
|
204
449
|
SystemExit: When the current directory is not a valid project root.
|
|
@@ -206,59 +451,56 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
206
451
|
contains glob metacharacters.
|
|
207
452
|
"""
|
|
208
453
|
claude_user_settings_path: Path = get_claude_user_settings_path()
|
|
209
|
-
project_root_path =
|
|
210
|
-
if not is_valid_project_root(project_root_path):
|
|
211
|
-
print(
|
|
212
|
-
f"ERROR: cwd {project_root_path} is not a project root "
|
|
213
|
-
f"(no .git or .claude). Run from a project root.",
|
|
214
|
-
file=sys.stderr,
|
|
215
|
-
)
|
|
216
|
-
raise SystemExit(1)
|
|
454
|
+
project_root_path = _require_valid_project_root_or_exit()
|
|
217
455
|
project_path = get_current_project_path()
|
|
218
|
-
|
|
219
|
-
all_agent_config_deny_rules =
|
|
220
|
-
project_path
|
|
221
|
-
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
222
|
-
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
456
|
+
all_permission_rules = build_project_scoped_allow_rules_for_reap(project_path)
|
|
457
|
+
all_agent_config_deny_rules = build_project_scoped_deny_rules_for_reap(
|
|
458
|
+
project_path
|
|
223
459
|
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
460
|
+
all_settings = load_settings(claude_user_settings_path)
|
|
461
|
+
(
|
|
462
|
+
allow_rules_removed_count,
|
|
463
|
+
deny_rules_removed_count,
|
|
464
|
+
directories_removed_count,
|
|
465
|
+
environment_entries_removed_count,
|
|
466
|
+
worktree_sweep_removed_count,
|
|
467
|
+
inert_rules_removed_count,
|
|
468
|
+
) = _apply_user_settings_revoke(
|
|
469
|
+
all_settings,
|
|
470
|
+
project_path,
|
|
471
|
+
all_permission_rules,
|
|
472
|
+
all_agent_config_deny_rules,
|
|
229
473
|
)
|
|
230
|
-
|
|
231
|
-
|
|
474
|
+
user_settings_changes_count = _sum_user_settings_change_counts(
|
|
475
|
+
allow_rules_removed_count,
|
|
476
|
+
deny_rules_removed_count,
|
|
477
|
+
directories_removed_count,
|
|
478
|
+
environment_entries_removed_count,
|
|
479
|
+
worktree_sweep_removed_count,
|
|
480
|
+
inert_rules_removed_count,
|
|
232
481
|
)
|
|
233
|
-
|
|
234
|
-
|
|
482
|
+
if user_settings_changes_count > 0:
|
|
483
|
+
prune_settings_after_revoke(all_settings)
|
|
484
|
+
save_settings(claude_user_settings_path, all_settings)
|
|
485
|
+
project_local_inert_rules_removed_count = strip_inert_rules_from_settings_file(
|
|
486
|
+
get_claude_project_local_settings_path(project_root_path)
|
|
235
487
|
)
|
|
236
|
-
|
|
237
|
-
allow_rules_removed_count
|
|
238
|
-
+ deny_rules_removed_count
|
|
239
|
-
+ directories_removed_count
|
|
240
|
-
+ environment_entries_removed_count
|
|
241
|
-
+ worktree_sweep_removed_count
|
|
242
|
-
)
|
|
243
|
-
if total_changes_count == 0:
|
|
488
|
+
if user_settings_changes_count + project_local_inert_rules_removed_count == 0:
|
|
244
489
|
print(f"Project path: {project_path}")
|
|
245
490
|
print(f"Settings file: {claude_user_settings_path}")
|
|
246
491
|
print("No changes to revoke; settings file left untouched.")
|
|
247
492
|
return
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
print(f"Additional directories removed: {directories_removed_count}")
|
|
260
|
-
print(
|
|
261
|
-
f"Auto-mode environment entries removed: {environment_entries_removed_count}"
|
|
493
|
+
_print_revoke_summary(
|
|
494
|
+
project_path,
|
|
495
|
+
claude_user_settings_path,
|
|
496
|
+
allow_rules_removed_count,
|
|
497
|
+
len(all_permission_rules),
|
|
498
|
+
deny_rules_removed_count,
|
|
499
|
+
len(all_agent_config_deny_rules),
|
|
500
|
+
inert_rules_removed_count,
|
|
501
|
+
project_local_inert_rules_removed_count,
|
|
502
|
+
directories_removed_count,
|
|
503
|
+
environment_entries_removed_count,
|
|
262
504
|
)
|
|
263
505
|
|
|
264
506
|
|
|
@@ -13,14 +13,23 @@ import pytest
|
|
|
13
13
|
def _load_common_module() -> ModuleType:
|
|
14
14
|
module_path = Path(__file__).parent.parent / "_claude_permissions_common.py"
|
|
15
15
|
parent_directory = str(module_path.parent.resolve())
|
|
16
|
-
if parent_directory
|
|
17
|
-
sys.path.
|
|
16
|
+
if parent_directory in sys.path:
|
|
17
|
+
sys.path.remove(parent_directory)
|
|
18
|
+
sys.path.insert(0, parent_directory)
|
|
19
|
+
for each_module_name in list(sys.modules):
|
|
20
|
+
if each_module_name == "pr_loop_shared_constants" or each_module_name.startswith(
|
|
21
|
+
"pr_loop_shared_constants."
|
|
22
|
+
):
|
|
23
|
+
del sys.modules[each_module_name]
|
|
24
|
+
if each_module_name == "_claude_permissions_common":
|
|
25
|
+
del sys.modules[each_module_name]
|
|
18
26
|
spec = importlib.util.spec_from_file_location(
|
|
19
27
|
"_claude_permissions_common", module_path
|
|
20
28
|
)
|
|
21
29
|
assert spec is not None
|
|
22
30
|
assert spec.loader is not None
|
|
23
31
|
module = importlib.util.module_from_spec(spec)
|
|
32
|
+
sys.modules["_claude_permissions_common"] = module
|
|
24
33
|
spec.loader.exec_module(module)
|
|
25
34
|
return module
|
|
26
35
|
|
|
@@ -187,3 +196,34 @@ def test_is_valid_project_root_uses_extracted_directory_marker_constants() -> No
|
|
|
187
196
|
assert '".git"' not in source_text
|
|
188
197
|
assert "'.claude'" not in source_text
|
|
189
198
|
assert '".claude"' not in source_text
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_permission_rule_tool_name_extracts_tool_prefix() -> None:
|
|
202
|
+
assert common.permission_rule_tool_name("Edit(/repo/.claude/**)") == "Edit"
|
|
203
|
+
assert common.permission_rule_tool_name("Write(c:/Users/jon/.claude/worktrees/x/**)") == (
|
|
204
|
+
"Write"
|
|
205
|
+
)
|
|
206
|
+
assert common.permission_rule_tool_name(None) is None
|
|
207
|
+
assert common.permission_rule_tool_name("not-a-rule") is None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_is_inert_file_permission_rule_for_write_glob_notebookedit() -> None:
|
|
211
|
+
assert common.is_inert_file_permission_rule("Write(/wt/.claude/**)") is True
|
|
212
|
+
assert common.is_inert_file_permission_rule("Glob(/wt/.claude/hooks/**)") is True
|
|
213
|
+
assert common.is_inert_file_permission_rule("NotebookEdit(**/.claude/worktrees/**)") is True
|
|
214
|
+
assert common.is_inert_file_permission_rule("Edit(/repo/.claude/**)") is False
|
|
215
|
+
assert common.is_inert_file_permission_rule("Bash(echo hi)") is False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_all_project_path_aliases_for_reap_adds_home_alias_when_project_is_home() -> None:
|
|
219
|
+
all_aliases = common.all_project_path_aliases_for_reap(
|
|
220
|
+
"C:/Users/jon", home_directory_path="C:/Users/jon"
|
|
221
|
+
)
|
|
222
|
+
assert all_aliases == ("C:/Users/jon", "$HOME")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_all_project_path_aliases_for_reap_skips_home_alias_for_other_projects() -> None:
|
|
226
|
+
all_aliases = common.all_project_path_aliases_for_reap(
|
|
227
|
+
"C:/dev/other", home_directory_path="C:/Users/jon"
|
|
228
|
+
)
|
|
229
|
+
assert all_aliases == ("C:/dev/other",)
|
|
@@ -36,6 +36,42 @@ def test_exposes_all_agent_config_deny_tools_tuple() -> None:
|
|
|
36
36
|
assert "Glob" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def test_exposes_all_legacy_permission_reap_tools_tuple() -> None:
|
|
40
|
+
assert constants_module.ALL_LEGACY_PERMISSION_REAP_TOOLS == ("Write", "Glob")
|
|
41
|
+
assert "NotebookEdit" not in constants_module.ALL_LEGACY_PERMISSION_REAP_TOOLS
|
|
42
|
+
assert "Edit" not in constants_module.ALL_LEGACY_PERMISSION_REAP_TOOLS
|
|
43
|
+
assert "Read" not in constants_module.ALL_LEGACY_PERMISSION_REAP_TOOLS
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_exposes_all_inert_file_permission_tools_tuple() -> None:
|
|
47
|
+
assert constants_module.ALL_INERT_FILE_PERMISSION_TOOLS == (
|
|
48
|
+
"Write",
|
|
49
|
+
"Glob",
|
|
50
|
+
"NotebookEdit",
|
|
51
|
+
)
|
|
52
|
+
assert "Edit" not in constants_module.ALL_INERT_FILE_PERMISSION_TOOLS
|
|
53
|
+
assert "Read" not in constants_module.ALL_INERT_FILE_PERMISSION_TOOLS
|
|
54
|
+
assert "Write" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
55
|
+
assert "NotebookEdit" not in constants_module.ALL_PERMISSION_ALLOW_TOOLS
|
|
56
|
+
assert "NotebookEdit" not in constants_module.ALL_AGENT_CONFIG_DENY_TOOLS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_exposes_home_project_path_alias() -> None:
|
|
60
|
+
assert constants_module.HOME_PROJECT_PATH_ALIAS == "$HOME"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_get_claude_project_local_settings_path_uses_settings_local_filename() -> None:
|
|
64
|
+
project_root = Path("C:/dev/Projects/LLM Plugins/claude-dev-env")
|
|
65
|
+
local_settings_path = constants_module.get_claude_project_local_settings_path(
|
|
66
|
+
project_root
|
|
67
|
+
)
|
|
68
|
+
assert local_settings_path.name == constants_module.CLAUDE_SETTINGS_LOCAL_FILENAME
|
|
69
|
+
assert local_settings_path.parent.name == (
|
|
70
|
+
constants_module.CLAUDE_SETTINGS_DIRECTORY_NAME
|
|
71
|
+
)
|
|
72
|
+
assert local_settings_path.parent.parent == project_root
|
|
73
|
+
|
|
74
|
+
|
|
39
75
|
def test_auto_mode_environment_entry_template_is_format_string() -> None:
|
|
40
76
|
rendered_template_text = (
|
|
41
77
|
constants_module.AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
@@ -74,6 +74,7 @@ def initialize_git_repository(repository_root: Path) -> None:
|
|
|
74
74
|
run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
|
|
75
75
|
run_git_in_repository(repository_root, "config", "user.name", "Test")
|
|
76
76
|
run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
|
|
77
|
+
run_git_in_repository(repository_root, "config", "core.autocrlf", "false")
|
|
77
78
|
disabled_hooks_directory = repository_root / "disabled-git-hooks"
|
|
78
79
|
disabled_hooks_directory.mkdir()
|
|
79
80
|
run_git_in_repository(
|
|
@@ -88,7 +89,7 @@ def commit_all_files(repository_root: Path, commit_message: str) -> None:
|
|
|
88
89
|
|
|
89
90
|
def write_file(file_path: Path, content: str) -> None:
|
|
90
91
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
-
file_path.write_text(content, encoding="utf-8")
|
|
92
|
+
file_path.write_text(content, encoding="utf-8", newline="")
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
def stage_file(repository_root: Path, relative_path: str) -> None:
|
|
@@ -2799,3 +2800,101 @@ def test_run_pytest_for_group_treats_no_tests_collected_as_a_pass_per_batch(
|
|
|
2799
2800
|
"a batch that collects no tests is not a failure; only a real pytest "
|
|
2800
2801
|
"failure exit code may block the commit"
|
|
2801
2802
|
)
|
|
2803
|
+
|
|
2804
|
+
|
|
2805
|
+
_PNG_SIGNATURE_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR" + b"\x00" * 16
|
|
2806
|
+
_SKIPPING_UNREADABLE_MARKER = "skipping unreadable file"
|
|
2807
|
+
|
|
2808
|
+
|
|
2809
|
+
def _write_png(file_path: Path) -> None:
|
|
2810
|
+
"""Write a minimal PNG-signature binary file at *file_path*."""
|
|
2811
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2812
|
+
file_path.write_bytes(_PNG_SIGNATURE_BYTES)
|
|
2813
|
+
|
|
2814
|
+
|
|
2815
|
+
def _clean_module_source() -> str:
|
|
2816
|
+
"""Return Python source the enforcer accepts as a clean new module."""
|
|
2817
|
+
return '"""Clean module for binary-flood gate tests."""\n\n\ndef greet() -> str:\n return "hello"\n'
|
|
2818
|
+
|
|
2819
|
+
|
|
2820
|
+
def test_diff_mode_tracked_new_png_does_not_flood_stderr(
|
|
2821
|
+
temporary_git_repository: Path,
|
|
2822
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2823
|
+
capsys: pytest.CaptureFixture[str],
|
|
2824
|
+
) -> None:
|
|
2825
|
+
"""A newly committed PNG must not emit a UTF-8 skip line on the push surface."""
|
|
2826
|
+
write_file(temporary_git_repository / "anchor.py", "anchor = 1\n")
|
|
2827
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
2828
|
+
_write_png(temporary_git_repository / "assets" / "icon.png")
|
|
2829
|
+
commit_all_files(temporary_git_repository, "add icon png")
|
|
2830
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
2831
|
+
|
|
2832
|
+
exit_code = gate_module.main(["--base", "HEAD~1"])
|
|
2833
|
+
|
|
2834
|
+
captured = capsys.readouterr()
|
|
2835
|
+
assert exit_code == 0
|
|
2836
|
+
assert _SKIPPING_UNREADABLE_MARKER not in captured.err
|
|
2837
|
+
assert "icon.png" not in captured.err
|
|
2838
|
+
|
|
2839
|
+
|
|
2840
|
+
def test_diff_mode_untracked_png_does_not_flood_stderr(
|
|
2841
|
+
temporary_git_repository: Path,
|
|
2842
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2843
|
+
capsys: pytest.CaptureFixture[str],
|
|
2844
|
+
) -> None:
|
|
2845
|
+
"""An untracked PNG joins the combined surface and must not flood stderr."""
|
|
2846
|
+
write_file(temporary_git_repository / "anchor.py", "anchor = 1\n")
|
|
2847
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
2848
|
+
_write_png(temporary_git_repository / "untracked_icon.png")
|
|
2849
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
2850
|
+
|
|
2851
|
+
exit_code = gate_module.main(["--base", "HEAD"])
|
|
2852
|
+
|
|
2853
|
+
captured = capsys.readouterr()
|
|
2854
|
+
assert exit_code == 0
|
|
2855
|
+
assert _SKIPPING_UNREADABLE_MARKER not in captured.err
|
|
2856
|
+
assert "untracked_icon.png" not in captured.err
|
|
2857
|
+
|
|
2858
|
+
|
|
2859
|
+
def test_diff_mode_image_only_surface_exits_zero_not_empty_file_set(
|
|
2860
|
+
temporary_git_repository: Path,
|
|
2861
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2862
|
+
capsys: pytest.CaptureFixture[str],
|
|
2863
|
+
) -> None:
|
|
2864
|
+
"""Git reports image files, yet zero code files means a clean pass (not EMPTY_FILE_SET)."""
|
|
2865
|
+
write_file(temporary_git_repository / "seed.txt", "seed\n")
|
|
2866
|
+
commit_all_files(temporary_git_repository, "seed")
|
|
2867
|
+
_write_png(temporary_git_repository / "only_image.png")
|
|
2868
|
+
commit_all_files(temporary_git_repository, "image only")
|
|
2869
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
2870
|
+
|
|
2871
|
+
exit_code = gate_module.main(["--base", "HEAD~1"])
|
|
2872
|
+
|
|
2873
|
+
captured = capsys.readouterr()
|
|
2874
|
+
assert exit_code == 0
|
|
2875
|
+
assert exit_code != gate_module.EMPTY_FILE_SET_EXIT_CODE
|
|
2876
|
+
assert _SKIPPING_UNREADABLE_MARKER not in captured.err
|
|
2877
|
+
assert gate_module.EMPTY_FILE_SET_MESSAGE not in captured.err
|
|
2878
|
+
assert "inspected 0 file(s)" in captured.err
|
|
2879
|
+
|
|
2880
|
+
|
|
2881
|
+
def test_diff_mode_mixed_py_and_png_validates_py_silently_skips_png(
|
|
2882
|
+
temporary_git_repository: Path,
|
|
2883
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2884
|
+
capsys: pytest.CaptureFixture[str],
|
|
2885
|
+
) -> None:
|
|
2886
|
+
"""A mixed surface line-maps and validates the .py; the PNG is skipped with no flood."""
|
|
2887
|
+
write_file(temporary_git_repository / "anchor.py", "anchor = 1\n")
|
|
2888
|
+
commit_all_files(temporary_git_repository, "baseline")
|
|
2889
|
+
write_file(temporary_git_repository / "fresh_module.py", _clean_module_source())
|
|
2890
|
+
_write_png(temporary_git_repository / "mixed_icon.png")
|
|
2891
|
+
commit_all_files(temporary_git_repository, "add py and png")
|
|
2892
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
2893
|
+
|
|
2894
|
+
exit_code = gate_module.main(["--base", "HEAD~1"])
|
|
2895
|
+
|
|
2896
|
+
captured = capsys.readouterr()
|
|
2897
|
+
assert exit_code == 0
|
|
2898
|
+
assert _SKIPPING_UNREADABLE_MARKER not in captured.err
|
|
2899
|
+
assert "inspected 1 file(s)" in captured.err
|
|
2900
|
+
assert "mixed_icon.png" not in captured.err
|