claude-dev-env 1.38.0 → 1.39.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 +10 -36
- package/_shared/pr-loop/audit-reply-template.md +147 -0
- package/_shared/pr-loop/fix-protocol.md +25 -4
- package/_shared/pr-loop/gh-payloads.md +37 -50
- package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
- package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +189 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
- package/_shared/pr-loop/state-schema.md +1 -1
- package/agents/clean-coder.md +2 -2
- package/bin/install.mjs +6 -7
- package/bin/install.test.mjs +8 -0
- package/commands/doc-gist.md +16 -0
- package/commands/plan.md +0 -2
- package/commands/review-plan.md +1 -1
- package/docs/CODE_RULES.md +122 -2
- package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
- package/hooks/blocking/code_rules_enforcer.py +1236 -161
- package/hooks/blocking/convergence_gate_blocker.py +130 -0
- package/hooks/blocking/destructive_command_blocker.py +74 -0
- package/hooks/blocking/gh_body_arg_blocker.py +30 -0
- package/hooks/blocking/md_to_html_blocker.py +119 -0
- package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
- package/hooks/blocking/test_code_rules_enforcer.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
- package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
- package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
- package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
- package/hooks/blocking/test_destructive_command_blocker.py +146 -0
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
- package/hooks/blocking/test_md_to_html_blocker.py +317 -0
- package/hooks/config/any_type_config.py +7 -0
- package/hooks/config/banned_identifiers_constants.py +11 -0
- package/hooks/config/blocking_check_limits.py +38 -0
- package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
- package/hooks/config/code_rules_enforcer_constants.py +53 -0
- package/hooks/config/convergence_branch_constants.py +9 -0
- package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
- package/hooks/config/html_companion_constants.py +20 -0
- package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
- package/hooks/config/test_banned_identifiers_constants.py +17 -0
- package/hooks/hooks.json +28 -20
- package/hooks/pyproject.toml +69 -0
- package/hooks/validators/mypy_integration.py +47 -1
- package/hooks/validators/run_all_validators.py +3 -3
- package/hooks/validators/test_mypy_integration.py +50 -1
- package/hooks/workflow/doc_gist_auto_publish.py +144 -0
- package/hooks/workflow/md_to_html_companion.py +365 -0
- package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
- package/hooks/workflow/test_md_to_html_companion.py +452 -0
- package/package.json +1 -1
- package/rules/gh-body-file.md +2 -0
- package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
- package/scripts/check.ps1 +106 -0
- package/scripts/config/timing.py +11 -0
- package/scripts/sweep_empty_dirs.py +138 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_sweep_empty_dirs.py +183 -0
- package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
- package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
- package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
- package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
- package/skills/bugteam/CONSTRAINTS.md +21 -22
- package/skills/bugteam/EXAMPLES.md +3 -3
- package/skills/bugteam/PROMPTS.md +227 -67
- package/skills/bugteam/SKILL.md +114 -455
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +112 -39
- package/skills/bugteam/reference/audit-contract.md +4 -22
- package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
- package/skills/bugteam/reference/design-rationale.md +2 -2
- package/skills/bugteam/reference/github-pr-reviews.md +50 -57
- package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
- package/skills/bugteam/reference/team-setup.md +106 -9
- package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
- package/skills/bugteam/scripts/README.md +60 -0
- package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
- package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
- package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
- package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
- package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
- package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
- package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
- package/skills/bugteam/test_skill_additions.py +1 -11
- package/skills/code/SKILL.md +176 -0
- package/skills/doc-gist/SKILL.md +99 -0
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
- package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
- package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
- package/skills/doc-gist/references/examples/05-design-system.html +629 -0
- package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
- package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
- package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
- package/skills/doc-gist/references/examples/11-status-report.html +528 -0
- package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
- package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
- package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
- package/skills/doc-gist/references/examples/README.md +5 -0
- package/skills/doc-gist/scripts/config/__init__.py +0 -0
- package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
- package/skills/doc-gist/scripts/gist_upload.py +177 -0
- package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
- package/skills/findbugs/SKILL.md +68 -2
- package/skills/monitor-open-prs/SKILL.md +13 -32
- package/skills/monitor-open-prs/test_skill_contract.py +0 -11
- package/skills/pr-consistency-audit/SKILL.md +112 -0
- package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
- package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
- package/skills/pr-converge/SKILL.md +227 -23
- package/skills/pr-converge/config/__init__.py +0 -0
- package/skills/pr-converge/config/constants.py +62 -0
- package/skills/pr-converge/reference/convergence-gates.md +138 -44
- package/skills/pr-converge/reference/examples.md +43 -11
- package/skills/pr-converge/reference/fix-protocol.md +6 -5
- package/skills/pr-converge/reference/ground-rules.md +5 -3
- package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
- package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
- package/skills/pr-converge/reference/per-tick.md +90 -31
- package/skills/pr-converge/reference/state-schema.md +22 -1
- package/skills/pr-converge/reference/stop-conditions.md +9 -7
- package/skills/pr-converge/scripts/README.md +34 -46
- package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
- package/skills/pr-converge/scripts/check_convergence.py +497 -0
- package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
- package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
- package/skills/qbug/SKILL.md +132 -27
- package/skills/session-log/SKILL.md +216 -114
- package/skills/session-tidy/SKILL.md +1 -1
- package/skills/skill-builder/SKILL.md +138 -56
- package/skills/skill-builder/references/delegation-map.md +72 -113
- package/skills/skill-builder/references/progressive-disclosure.md +122 -0
- package/skills/skill-builder/references/self-audit-checklist.md +92 -0
- package/skills/skill-builder/references/skill-types.md +228 -0
- package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
- package/skills/skill-builder/templates/gap-analysis.md +15 -8
- package/skills/skill-builder/workflows/improve-skill.md +86 -57
- package/skills/skill-builder/workflows/new-skill.md +80 -168
- package/skills/skill-builder/workflows/polish-skill.md +78 -54
- package/skills/structure-prompt/SKILL.md +50 -0
- package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
- package/skills/structure-prompt/reference/block-classification.md +27 -0
- package/skills/structure-prompt/reference/canonical-case.md +48 -0
- package/skills/structure-prompt/reference/citation-depth.md +70 -0
- package/skills/structure-prompt/reference/cleanup.md +33 -0
- package/skills/structure-prompt/reference/constraints.md +33 -0
- package/skills/structure-prompt/reference/directives.md +37 -0
- package/skills/structure-prompt/reference/examples.md +72 -0
- package/skills/structure-prompt/reference/instantiation.md +51 -0
- package/skills/structure-prompt/reference/output-contract.md +72 -0
- package/skills/structure-prompt/reference/per-category.md +23 -0
- package/skills/structure-prompt/reference/persona.md +38 -0
- package/skills/structure-prompt/reference/research.md +33 -0
- package/skills/structure-prompt/reference/structure.md +28 -0
- package/agents/code-standards-agent.md +0 -93
- package/agents/groq-coder.md +0 -113
- package/agents/plan-executor.md +0 -226
- package/agents/project-docs-analyzer.md +0 -53
- package/agents/project-structure-organizer-agent.md +0 -72
- package/agents/skill-to-agent-converter.md +0 -370
- package/agents/skill-writer-agent.md +0 -470
- package/agents/user-docs-writer.md +0 -67
- package/agents/workflow-visual-documenter.md +0 -82
- package/commands/readability-review.md +0 -20
- package/hooks/mypy.ini +0 -2
- package/hooks/notification/attention_needed_notify.py +0 -71
- package/hooks/notification/claude_notification_handler.py +0 -67
- package/hooks/notification/notification_utils.py +0 -267
- package/hooks/notification/subagent_complete_notify.py +0 -381
- package/hooks/notification/test_attention_needed_notify.py +0 -47
- package/hooks/notification/test_claude_notification_handler.py +0 -54
- package/hooks/notification/test_notification_utils.py +0 -91
- package/hooks/notification/test_subagent_complete_notify.py +0 -79
- package/scripts/config/groq_bugteam_config.py +0 -230
- package/scripts/config/test_groq_bugteam_config.py +0 -83
- package/scripts/config/test_spec_implementer_prompt.py +0 -32
- package/scripts/groq_bugteam.README.md +0 -131
- package/scripts/groq_bugteam.py +0 -647
- package/scripts/groq_bugteam_dotenv.py +0 -40
- package/scripts/groq_bugteam_spec.py +0 -226
- package/scripts/test_groq_bugteam.py +0 -529
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
- package/scripts/test_groq_bugteam_dotenv.py +0 -66
- package/scripts/test_groq_bugteam_spec.py +0 -338
- package/skills/bugteam/SKILL_EVALS.md +0 -309
- package/skills/dream/SKILL.md +0 -118
- package/skills/ingest/SKILL.md +0 -40
- package/skills/npm-creator/SKILL.md +0 -187
- package/skills/readability-review/SKILL.md +0 -127
- package/skills/resume-review/SKILL.md +0 -261
- package/skills/rule-audit/SKILL.md +0 -307
- package/skills/rule-creator/SKILL.md +0 -150
- package/skills/searching-obsidian-vault/SKILL.md +0 -131
- package/skills/skill-writer/REFERENCE.md +0 -284
- package/skills/skill-writer/SKILL.md +0 -222
- package/skills/tdd-team/SKILL.md +0 -128
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
10
|
+
if _script_directory not in sys.path:
|
|
11
|
+
sys.path.insert(0, _script_directory)
|
|
12
|
+
|
|
13
|
+
import _claude_permissions_common as common_module
|
|
14
|
+
from _claude_permissions_common import (
|
|
15
|
+
build_permission_rule,
|
|
16
|
+
get_current_project_path,
|
|
17
|
+
path_contains_glob_metacharacters,
|
|
18
|
+
save_settings,
|
|
19
|
+
)
|
|
20
|
+
from config.claude_permissions_common_constants import DEFAULT_SETTINGS_FILE_MODE
|
|
21
|
+
import grant_project_claude_permissions as grant_module
|
|
22
|
+
import revoke_project_claude_permissions as revoke_module
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_return_normalized_path_when_cwd_contains_spaces(
|
|
26
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
27
|
+
) -> None:
|
|
28
|
+
directory_with_spaces = tmp_path / "dir with spaces"
|
|
29
|
+
directory_with_spaces.mkdir()
|
|
30
|
+
monkeypatch.chdir(directory_with_spaces)
|
|
31
|
+
returned_project_path = get_current_project_path()
|
|
32
|
+
expected_suffix = "/dir with spaces"
|
|
33
|
+
assert returned_project_path.endswith(expected_suffix)
|
|
34
|
+
assert "\\" not in returned_project_path
|
|
35
|
+
built_rule = build_permission_rule("Edit", returned_project_path)
|
|
36
|
+
assert built_rule.startswith("Edit(")
|
|
37
|
+
assert built_rule.endswith("/.claude/**)")
|
|
38
|
+
assert "dir with spaces" in built_rule
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_raise_when_cwd_contains_glob_metacharacters(
|
|
42
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
43
|
+
) -> None:
|
|
44
|
+
directory_with_star = tmp_path / "weird[dir]"
|
|
45
|
+
directory_with_star.mkdir()
|
|
46
|
+
monkeypatch.chdir(directory_with_star)
|
|
47
|
+
with pytest.raises(ValueError, match="glob metacharacters"):
|
|
48
|
+
get_current_project_path()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_flag_glob_metacharacters_in_any_position() -> None:
|
|
52
|
+
assert path_contains_glob_metacharacters("/home/user/[dir]/project")
|
|
53
|
+
assert path_contains_glob_metacharacters("/home/user/project*")
|
|
54
|
+
assert not path_contains_glob_metacharacters("/home/user/dir with spaces")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_save_settings_logs_when_temp_unlink_fails(
|
|
58
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
59
|
+
) -> None:
|
|
60
|
+
"""A swallowed unlink in the finally block must surface to stderr.
|
|
61
|
+
|
|
62
|
+
Forces a write success followed by os.replace failure so the temp file
|
|
63
|
+
survives into the finally branch, then makes Path.unlink raise.
|
|
64
|
+
"""
|
|
65
|
+
settings_path = tmp_path / "settings.json"
|
|
66
|
+
settings_path.write_text('{"existing": true}\n', encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
def failing_replace(*_args: object, **_kwargs: object) -> None:
|
|
69
|
+
raise OSError("replace blocked by AV")
|
|
70
|
+
|
|
71
|
+
def failing_unlink(self: Path, *args: object, **kwargs: object) -> None:
|
|
72
|
+
raise PermissionError("temp file held by AV")
|
|
73
|
+
|
|
74
|
+
with patch.object(common_module.os, "replace", failing_replace):
|
|
75
|
+
with patch.object(Path, "unlink", failing_unlink):
|
|
76
|
+
with pytest.raises(SystemExit):
|
|
77
|
+
save_settings(settings_path, {"new_key": "value"})
|
|
78
|
+
captured = capsys.readouterr()
|
|
79
|
+
assert ".tmp" in captured.err
|
|
80
|
+
assert "PermissionError" in captured.err or "held by AV" in captured.err
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_save_settings_finally_skips_unlink_when_no_temp_was_created(
|
|
84
|
+
tmp_path: Path,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""When this invocation never created the temp file, finally must not unlink it."""
|
|
87
|
+
settings_path = tmp_path / "settings.json"
|
|
88
|
+
settings_path.write_text('{"existing": true}\n', encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
unlink_call_paths: list[Path] = []
|
|
91
|
+
original_unlink = Path.unlink
|
|
92
|
+
|
|
93
|
+
def recording_unlink(self: Path, *args: object, **kwargs: object) -> None:
|
|
94
|
+
unlink_call_paths.append(self)
|
|
95
|
+
original_unlink(self, *args, **kwargs)
|
|
96
|
+
|
|
97
|
+
def write_raises(*_args: object, **_kwargs: object) -> None:
|
|
98
|
+
raise FileExistsError("another writer's temp")
|
|
99
|
+
|
|
100
|
+
with patch.object(common_module, "write_atomically_with_mode", write_raises):
|
|
101
|
+
with patch.object(Path, "unlink", recording_unlink):
|
|
102
|
+
with pytest.raises(SystemExit):
|
|
103
|
+
common_module.save_settings(settings_path, {"new_key": "value"})
|
|
104
|
+
assert all(
|
|
105
|
+
each_path.suffix != ".tmp"
|
|
106
|
+
for each_path in unlink_call_paths
|
|
107
|
+
), (
|
|
108
|
+
"finally must not unlink a temp file this invocation never created"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_default_settings_file_mode_used_when_settings_file_missing(
|
|
113
|
+
tmp_path: Path,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""get_mode_to_preserve must fall back to DEFAULT_SETTINGS_FILE_MODE."""
|
|
116
|
+
missing_settings_path = tmp_path / "no_such_file.json"
|
|
117
|
+
returned_mode = common_module.get_mode_to_preserve(missing_settings_path)
|
|
118
|
+
assert returned_mode == DEFAULT_SETTINGS_FILE_MODE
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_is_valid_project_root_helper_is_not_orphaned_in_common_module() -> None:
|
|
122
|
+
"""The orphan helper in the common module must be removed.
|
|
123
|
+
|
|
124
|
+
Both grant and revoke keep their own local copies and consume them from
|
|
125
|
+
module scope; the common-module copy was dead code with zero call sites.
|
|
126
|
+
"""
|
|
127
|
+
assert not hasattr(common_module, "is_valid_project_root"), (
|
|
128
|
+
"is_valid_project_root must not live in _claude_permissions_common — "
|
|
129
|
+
"neither grant nor revoke imports it from there"
|
|
130
|
+
)
|
|
131
|
+
assert callable(grant_module.is_valid_project_root)
|
|
132
|
+
assert callable(revoke_module.is_valid_project_root)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _reload_with_stale_config_cache(module_name: str) -> ModuleType:
|
|
136
|
+
fake_submodule_name = "config.claude_permissions_common_constants"
|
|
137
|
+
fake_parent_name = "config"
|
|
138
|
+
sentinel_module_a = ModuleType(fake_parent_name)
|
|
139
|
+
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
140
|
+
sys.modules[fake_parent_name] = sentinel_module_a
|
|
141
|
+
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
142
|
+
try:
|
|
143
|
+
target_module = sys.modules.get(module_name)
|
|
144
|
+
if target_module is None:
|
|
145
|
+
target_module = importlib.import_module(module_name)
|
|
146
|
+
else:
|
|
147
|
+
target_module = importlib.reload(target_module)
|
|
148
|
+
finally:
|
|
149
|
+
sys.modules.pop(fake_parent_name, None)
|
|
150
|
+
sys.modules.pop(fake_submodule_name, None)
|
|
151
|
+
return target_module
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_grant_module_import_evicts_cached_config_submodules(
|
|
155
|
+
tmp_path: Path,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""grant_project_claude_permissions must evict cached `config.*` on import.
|
|
158
|
+
|
|
159
|
+
Regression for loop1-2: without a defensive cache pop above sys.path.insert,
|
|
160
|
+
a cached `config` package shadows scripts/config/ and the from-import raises.
|
|
161
|
+
Calls the rebound `is_valid_project_root` to confirm the real implementation
|
|
162
|
+
survived the cache eviction (a stale shadow would either raise on import or
|
|
163
|
+
bind a placeholder that returns the wrong value).
|
|
164
|
+
"""
|
|
165
|
+
reloaded_module = _reload_with_stale_config_cache(
|
|
166
|
+
"grant_project_claude_permissions"
|
|
167
|
+
)
|
|
168
|
+
real_project_root = tmp_path / "project_root"
|
|
169
|
+
(real_project_root / ".claude").mkdir(parents=True)
|
|
170
|
+
bare_directory = tmp_path / "no_claude_marker"
|
|
171
|
+
bare_directory.mkdir()
|
|
172
|
+
assert reloaded_module.is_valid_project_root(real_project_root) is True
|
|
173
|
+
assert reloaded_module.is_valid_project_root(bare_directory) is False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_revoke_module_import_evicts_cached_config_submodules(
|
|
177
|
+
tmp_path: Path,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""revoke_project_claude_permissions must evict cached `config.*` on import.
|
|
180
|
+
|
|
181
|
+
Regression for loop1-3: without a defensive cache pop above sys.path.insert,
|
|
182
|
+
a cached `config` package shadows scripts/config/ and the from-import raises.
|
|
183
|
+
Calls the rebound `is_valid_project_root` to confirm the real implementation
|
|
184
|
+
survived the cache eviction (a stale shadow would either raise on import or
|
|
185
|
+
bind a placeholder that returns the wrong value).
|
|
186
|
+
"""
|
|
187
|
+
reloaded_module = _reload_with_stale_config_cache(
|
|
188
|
+
"revoke_project_claude_permissions"
|
|
189
|
+
)
|
|
190
|
+
real_project_root = tmp_path / "project_root"
|
|
191
|
+
(real_project_root / ".claude").mkdir(parents=True)
|
|
192
|
+
bare_directory = tmp_path / "no_claude_marker"
|
|
193
|
+
bare_directory.mkdir()
|
|
194
|
+
assert reloaded_module.is_valid_project_root(real_project_root) is True
|
|
195
|
+
assert reloaded_module.is_valid_project_root(bare_directory) is False
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Regression tests for grant_project_claude_permissions module-import behavior.
|
|
2
|
+
|
|
3
|
+
Pins the loop1-2 fix: a defensive cache pop above sys.path.insert evicts every
|
|
4
|
+
cached `config` and `config.<submodule>` entry so the from-import binds against
|
|
5
|
+
scripts/config/ rather than a stale parent package shadowing it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
17
|
+
if _script_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, _script_directory)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _reload_grant_with_stale_config_cache() -> ModuleType:
|
|
22
|
+
fake_submodule_name = "config.claude_permissions_common_constants"
|
|
23
|
+
fake_parent_name = "config"
|
|
24
|
+
sentinel_module_a = ModuleType(fake_parent_name)
|
|
25
|
+
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
26
|
+
sys.modules[fake_parent_name] = sentinel_module_a
|
|
27
|
+
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
28
|
+
try:
|
|
29
|
+
target_module = sys.modules.get("grant_project_claude_permissions")
|
|
30
|
+
if target_module is None:
|
|
31
|
+
target_module = importlib.import_module("grant_project_claude_permissions")
|
|
32
|
+
else:
|
|
33
|
+
target_module = importlib.reload(target_module)
|
|
34
|
+
finally:
|
|
35
|
+
sys.modules.pop(fake_parent_name, None)
|
|
36
|
+
sys.modules.pop(fake_submodule_name, None)
|
|
37
|
+
return target_module
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_grant_module_imports_when_config_is_already_cached(tmp_path: Path) -> None:
|
|
41
|
+
"""Module import must succeed even when sys.modules carries a stale `config`.
|
|
42
|
+
|
|
43
|
+
Regression for loop1-2 — invokes is_valid_project_root after reload to
|
|
44
|
+
prove the binding came from scripts/config/ rather than the sentinel.
|
|
45
|
+
"""
|
|
46
|
+
reloaded_module = _reload_grant_with_stale_config_cache()
|
|
47
|
+
not_a_project_root = tmp_path / "empty_dir"
|
|
48
|
+
not_a_project_root.mkdir()
|
|
49
|
+
assert reloaded_module.is_valid_project_root(not_a_project_root) is False, (
|
|
50
|
+
"is_valid_project_root must run normally after the reload — proof that "
|
|
51
|
+
"the from-import bound real constants, not the stale cached ones"
|
|
52
|
+
)
|
|
53
|
+
a_git_project_root = tmp_path / "git_project"
|
|
54
|
+
(a_git_project_root / ".git").mkdir(parents=True)
|
|
55
|
+
assert reloaded_module.is_valid_project_root(a_git_project_root) is True
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for probe_code_rules_enforcer_check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
12
|
+
if _script_directory not in sys.path:
|
|
13
|
+
sys.path.insert(0, _script_directory)
|
|
14
|
+
|
|
15
|
+
import probe_code_rules_enforcer_check as probe_module
|
|
16
|
+
from probe_code_rules_enforcer_check import main, run_probe
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _FakeEnforcerModule:
|
|
20
|
+
@staticmethod
|
|
21
|
+
def check_dummy(content: str, reported_path: str) -> list[str]:
|
|
22
|
+
if "trigger" in content:
|
|
23
|
+
return [f"{reported_path}: trigger detected"]
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _install_fake_loader(
|
|
28
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
29
|
+
fake_module: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
monkeypatch.setattr(probe_module, "_load_enforcer_module", lambda: fake_module)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_run_probe_returns_check_function_output(
|
|
35
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
36
|
+
) -> None:
|
|
37
|
+
_install_fake_loader(monkeypatch, _FakeEnforcerModule())
|
|
38
|
+
fixture = tmp_path / "fixture.py"
|
|
39
|
+
fixture.write_text("trigger me", encoding="utf-8")
|
|
40
|
+
issues = run_probe("check_dummy", str(fixture), "reported.py")
|
|
41
|
+
assert issues == ["reported.py: trigger detected"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_run_probe_raises_when_check_function_missing(
|
|
45
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
46
|
+
) -> None:
|
|
47
|
+
_install_fake_loader(monkeypatch, _FakeEnforcerModule())
|
|
48
|
+
fixture = tmp_path / "fixture.py"
|
|
49
|
+
fixture.write_text("no-op", encoding="utf-8")
|
|
50
|
+
with pytest.raises(AttributeError):
|
|
51
|
+
run_probe("does_not_exist", str(fixture), "reported.py")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_main_prints_each_issue(
|
|
55
|
+
tmp_path: Path,
|
|
56
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
57
|
+
capsys: pytest.CaptureFixture[str],
|
|
58
|
+
) -> None:
|
|
59
|
+
_install_fake_loader(monkeypatch, _FakeEnforcerModule())
|
|
60
|
+
fixture = tmp_path / "fixture.py"
|
|
61
|
+
fixture.write_text("trigger me", encoding="utf-8")
|
|
62
|
+
exit_code = main(
|
|
63
|
+
["probe_code_rules_enforcer_check.py", "check_dummy", str(fixture)]
|
|
64
|
+
)
|
|
65
|
+
captured = capsys.readouterr()
|
|
66
|
+
assert exit_code == 0
|
|
67
|
+
assert "trigger detected" in captured.out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_main_returns_usage_exit_code_when_argv_count_wrong(
|
|
71
|
+
capsys: pytest.CaptureFixture[str],
|
|
72
|
+
) -> None:
|
|
73
|
+
exit_code = main(["probe_code_rules_enforcer_check.py"])
|
|
74
|
+
captured = capsys.readouterr()
|
|
75
|
+
assert exit_code != 0
|
|
76
|
+
assert "usage" in captured.err.lower()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Regression tests for revoke_project_claude_permissions module-import behavior.
|
|
2
|
+
|
|
3
|
+
Pins the loop1-3 fix: a defensive cache pop above sys.path.insert evicts every
|
|
4
|
+
cached `config` and `config.<submodule>` entry so the from-import binds against
|
|
5
|
+
scripts/config/ rather than a stale parent package shadowing it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
17
|
+
if _script_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, _script_directory)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _reload_revoke_with_stale_config_cache() -> ModuleType:
|
|
22
|
+
fake_submodule_name = "config.claude_permissions_common_constants"
|
|
23
|
+
fake_parent_name = "config"
|
|
24
|
+
sentinel_module_a = ModuleType(fake_parent_name)
|
|
25
|
+
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
26
|
+
sys.modules[fake_parent_name] = sentinel_module_a
|
|
27
|
+
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
28
|
+
try:
|
|
29
|
+
target_module = sys.modules.get("revoke_project_claude_permissions")
|
|
30
|
+
if target_module is None:
|
|
31
|
+
target_module = importlib.import_module("revoke_project_claude_permissions")
|
|
32
|
+
else:
|
|
33
|
+
target_module = importlib.reload(target_module)
|
|
34
|
+
finally:
|
|
35
|
+
sys.modules.pop(fake_parent_name, None)
|
|
36
|
+
sys.modules.pop(fake_submodule_name, None)
|
|
37
|
+
return target_module
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_revoke_module_imports_when_config_is_already_cached(tmp_path: Path) -> None:
|
|
41
|
+
"""Module import must succeed even when sys.modules carries a stale `config`.
|
|
42
|
+
|
|
43
|
+
Regression for loop1-3 — invokes is_valid_project_root after reload to
|
|
44
|
+
prove the binding came from scripts/config/ rather than the sentinel.
|
|
45
|
+
"""
|
|
46
|
+
reloaded_module = _reload_revoke_with_stale_config_cache()
|
|
47
|
+
not_a_project_root = tmp_path / "empty_dir"
|
|
48
|
+
not_a_project_root.mkdir()
|
|
49
|
+
assert reloaded_module.is_valid_project_root(not_a_project_root) is False, (
|
|
50
|
+
"is_valid_project_root must run normally after the reload — proof that "
|
|
51
|
+
"the from-import bound real constants, not the stale cached ones"
|
|
52
|
+
)
|
|
53
|
+
a_claude_project_root = tmp_path / "claude_project"
|
|
54
|
+
(a_claude_project_root / ".claude").mkdir(parents=True)
|
|
55
|
+
assert reloaded_module.is_valid_project_root(a_claude_project_root) is True
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for windows_safe_rmtree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
13
|
+
if _script_directory not in sys.path:
|
|
14
|
+
sys.path.insert(0, _script_directory)
|
|
15
|
+
|
|
16
|
+
from unittest.mock import patch
|
|
17
|
+
|
|
18
|
+
from windows_safe_rmtree import _strip_read_only_and_retry, main, remove_tree
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_strip_read_only_and_retry_logs_when_retry_still_fails(
|
|
22
|
+
capsys: pytest.CaptureFixture[str],
|
|
23
|
+
tmp_path: Path,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""The chmod-then-retry handler must surface residual failures to stderr."""
|
|
26
|
+
target_path = tmp_path / "locked_file.txt"
|
|
27
|
+
target_path.write_text("payload", encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
def always_fails(_path: str) -> None:
|
|
30
|
+
raise PermissionError("file held by another process")
|
|
31
|
+
|
|
32
|
+
_strip_read_only_and_retry(always_fails, str(target_path), None, None, None)
|
|
33
|
+
captured = capsys.readouterr()
|
|
34
|
+
assert str(target_path) in captured.err
|
|
35
|
+
assert "PermissionError" in captured.err or "held by another process" in captured.err
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_remove_tree_returns_nonzero_when_residual_oserror(
|
|
39
|
+
capsys: pytest.CaptureFixture[str],
|
|
40
|
+
tmp_path: Path,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""When shutil.rmtree raises after handler retries, remove_tree must signal."""
|
|
43
|
+
target_path = tmp_path / "ghost"
|
|
44
|
+
target_path.mkdir()
|
|
45
|
+
with patch(
|
|
46
|
+
"shutil.rmtree",
|
|
47
|
+
side_effect=PermissionError("residual lock"),
|
|
48
|
+
):
|
|
49
|
+
exit_code = remove_tree(str(target_path))
|
|
50
|
+
captured = capsys.readouterr()
|
|
51
|
+
assert exit_code != 0, "remove_tree must report failure when rmtree raises"
|
|
52
|
+
assert str(target_path) in captured.err
|
|
53
|
+
assert "residual lock" in captured.err or "PermissionError" in captured.err
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_main_propagates_remove_tree_failure(
|
|
57
|
+
capsys: pytest.CaptureFixture[str],
|
|
58
|
+
tmp_path: Path,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""main must return non-zero when remove_tree could not finish cleanup."""
|
|
61
|
+
target_path = tmp_path / "stubborn"
|
|
62
|
+
target_path.mkdir()
|
|
63
|
+
with patch(
|
|
64
|
+
"shutil.rmtree",
|
|
65
|
+
side_effect=PermissionError("still locked"),
|
|
66
|
+
):
|
|
67
|
+
exit_code = main(["windows_safe_rmtree.py", str(target_path)])
|
|
68
|
+
assert exit_code != 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_remove_tree_deletes_plain_directory(tmp_path: Path) -> None:
|
|
72
|
+
target = tmp_path / "victim"
|
|
73
|
+
target.mkdir()
|
|
74
|
+
(target / "file.txt").write_text("payload", encoding="utf-8")
|
|
75
|
+
remove_tree(str(target))
|
|
76
|
+
assert not target.exists()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_remove_tree_handles_read_only_file(tmp_path: Path) -> None:
|
|
80
|
+
target = tmp_path / "victim"
|
|
81
|
+
target.mkdir()
|
|
82
|
+
locked_file = target / "locked.txt"
|
|
83
|
+
locked_file.write_text("payload", encoding="utf-8")
|
|
84
|
+
os.chmod(locked_file, stat.S_IREAD)
|
|
85
|
+
remove_tree(str(target))
|
|
86
|
+
assert not target.exists()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_remove_tree_swallows_missing_path(tmp_path: Path) -> None:
|
|
90
|
+
missing_path = tmp_path / "does-not-exist"
|
|
91
|
+
remove_tree(str(missing_path))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_main_returns_zero_on_success(tmp_path: Path) -> None:
|
|
95
|
+
target = tmp_path / "victim"
|
|
96
|
+
target.mkdir()
|
|
97
|
+
exit_code = main(["windows_safe_rmtree.py", str(target)])
|
|
98
|
+
assert exit_code == 0
|
|
99
|
+
assert not target.exists()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_main_returns_usage_exit_code_when_argv_count_wrong(
|
|
103
|
+
capsys: pytest.CaptureFixture[str],
|
|
104
|
+
) -> None:
|
|
105
|
+
exit_code = main(["windows_safe_rmtree.py"])
|
|
106
|
+
captured = capsys.readouterr()
|
|
107
|
+
assert exit_code != 0
|
|
108
|
+
assert "usage" in captured.err.lower()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Recursively remove a directory tree, stripping Windows ReadOnly attributes.
|
|
2
|
+
|
|
3
|
+
Required by ~/.claude/rules/windows-filesystem-safe.md so bugteam teardown does
|
|
4
|
+
not silently swallow Windows ReadOnly-attribute failures the way the unsafe
|
|
5
|
+
shutil ignore-errors flag does.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python windows_safe_rmtree.py <absolute-path>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import stat
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
|
|
19
|
+
from config.windows_safe_rmtree_constants import (
|
|
20
|
+
EXIT_CODE_REMOVE_TREE_FAILURE,
|
|
21
|
+
EXIT_CODE_USAGE_ERROR,
|
|
22
|
+
EXPECTED_ARGUMENT_COUNT,
|
|
23
|
+
ONEXC_PYTHON_MAJOR_VERSION,
|
|
24
|
+
ONEXC_PYTHON_MINOR_VERSION,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _strip_read_only_and_retry(
|
|
29
|
+
removal_function: Callable[[str], None],
|
|
30
|
+
target_path: str,
|
|
31
|
+
*_exc_info: object,
|
|
32
|
+
) -> None:
|
|
33
|
+
try:
|
|
34
|
+
os.chmod(target_path, os.stat(target_path).st_mode | stat.S_IWRITE)
|
|
35
|
+
removal_function(target_path)
|
|
36
|
+
except OSError as residual_error:
|
|
37
|
+
sys.stderr.write(
|
|
38
|
+
f"windows_safe_rmtree: chmod-and-retry could not remove {target_path}: "
|
|
39
|
+
f"{type(residual_error).__name__}: {residual_error}\n"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _select_handler_keyword() -> dict[str, Callable[..., None]]:
|
|
44
|
+
onexc_required_version = (
|
|
45
|
+
ONEXC_PYTHON_MAJOR_VERSION,
|
|
46
|
+
ONEXC_PYTHON_MINOR_VERSION,
|
|
47
|
+
)
|
|
48
|
+
if sys.version_info >= onexc_required_version:
|
|
49
|
+
return {"onexc": _strip_read_only_and_retry}
|
|
50
|
+
return {"onerror": _strip_read_only_and_retry}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def remove_tree(target_path: str) -> int:
|
|
54
|
+
"""Recursively remove a directory tree, handling Windows ReadOnly attributes.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
target_path: Absolute path to the directory tree to remove.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Zero when the tree was removed (or never existed). Non-zero when
|
|
61
|
+
the chmod-and-retry handler could not finish cleanup; callers must
|
|
62
|
+
treat a non-zero return as "tree may still be present".
|
|
63
|
+
"""
|
|
64
|
+
if not os.path.exists(target_path):
|
|
65
|
+
return 0
|
|
66
|
+
handler_keyword = _select_handler_keyword()
|
|
67
|
+
try:
|
|
68
|
+
shutil.rmtree(target_path, **handler_keyword)
|
|
69
|
+
except OSError as residual_error:
|
|
70
|
+
sys.stderr.write(
|
|
71
|
+
f"windows_safe_rmtree: residual failure removing {target_path}: "
|
|
72
|
+
f"{type(residual_error).__name__}: {residual_error}\n"
|
|
73
|
+
)
|
|
74
|
+
return EXIT_CODE_REMOVE_TREE_FAILURE
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _print_usage_to_stderr() -> None:
|
|
79
|
+
sys.stderr.write("usage: python windows_safe_rmtree.py <absolute-path>\n")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(all_arguments: list[str]) -> int:
|
|
83
|
+
"""Parse command-line arguments and invoke remove_tree.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
all_arguments: Command-line arguments including script name
|
|
87
|
+
and the target directory path.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Exit code 0 on success, EXIT_CODE_USAGE_ERROR on invalid usage,
|
|
91
|
+
EXIT_CODE_REMOVE_TREE_FAILURE when remove_tree could not finish.
|
|
92
|
+
"""
|
|
93
|
+
if len(all_arguments) != EXPECTED_ARGUMENT_COUNT:
|
|
94
|
+
_print_usage_to_stderr()
|
|
95
|
+
return EXIT_CODE_USAGE_ERROR
|
|
96
|
+
return remove_tree(all_arguments[1])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
sys.exit(main(sys.argv))
|
|
@@ -10,21 +10,11 @@ def _read_skill_text() -> str:
|
|
|
10
10
|
return skill_path.read_text(encoding="utf-8")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
skill_text = _read_skill_text()
|
|
15
|
-
assert "BUGTEAM_FIX_IMPLEMENTER" in skill_text
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_skill_names_default_implementer_subagent_type():
|
|
13
|
+
def test_skill_names_implementer_subagent_type():
|
|
19
14
|
skill_text = _read_skill_text()
|
|
20
15
|
assert "clean-coder" in skill_text
|
|
21
16
|
|
|
22
17
|
|
|
23
|
-
def test_skill_names_optional_groq_implementer_subagent_type():
|
|
24
|
-
skill_text = _read_skill_text()
|
|
25
|
-
assert "groq-coder" in skill_text
|
|
26
|
-
|
|
27
|
-
|
|
28
18
|
def test_skill_documents_bugbot_retrigger_flag():
|
|
29
19
|
skill_text = _read_skill_text()
|
|
30
20
|
assert "--bugbot-retrigger" in skill_text
|