claude-dev-env 1.83.0 → 1.84.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/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +0 -4
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +116 -6
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +11 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/autoconverge/SKILL.md +38 -57
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +70 -73
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
"""TDD-pair tests for the underscore-prefixed _bugteam_permissions_common module.
|
|
2
|
-
|
|
3
|
-
The TDD enforcer matches a production filename ``X.py`` to ``test_X.py``;
|
|
4
|
-
``_bugteam_permissions_common.py`` carries a leading underscore that the
|
|
5
|
-
enforcer treats as part of the name. This file's tests are the canonical
|
|
6
|
-
match. The broader behavioral suite continues to live alongside, in
|
|
7
|
-
``test_bugteam_permissions_common.py``.
|
|
8
|
-
"""
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from unittest.mock import patch
|
|
14
|
-
|
|
15
|
-
import pytest
|
|
16
|
-
|
|
17
|
-
_script_directory = str(Path(__file__).resolve().parent)
|
|
18
|
-
if _script_directory not in sys.path:
|
|
19
|
-
sys.path.insert(0, _script_directory)
|
|
20
|
-
|
|
21
|
-
import _bugteam_permissions_common as common_module
|
|
22
|
-
import grant_project_claude_permissions as grant_module
|
|
23
|
-
import revoke_project_claude_permissions as revoke_module
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_is_valid_project_root_not_defined_on_common_module() -> None:
|
|
27
|
-
"""The common module must not expose ``is_valid_project_root``.
|
|
28
|
-
|
|
29
|
-
Both grant and revoke define their own local copy. A shared copy on
|
|
30
|
-
the common module would be a third parallel definition with no
|
|
31
|
-
production caller, so its absence is the asserted contract.
|
|
32
|
-
"""
|
|
33
|
-
assert not hasattr(common_module, "is_valid_project_root")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_grant_is_valid_project_root_detects_git_marker(tmp_path: Path) -> None:
|
|
37
|
-
git_project_root = tmp_path / "git_project"
|
|
38
|
-
(git_project_root / ".git").mkdir(parents=True)
|
|
39
|
-
assert grant_module.is_valid_project_root(git_project_root) is True
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_grant_is_valid_project_root_detects_claude_marker(tmp_path: Path) -> None:
|
|
43
|
-
claude_project_root = tmp_path / "claude_project"
|
|
44
|
-
(claude_project_root / ".claude").mkdir(parents=True)
|
|
45
|
-
assert grant_module.is_valid_project_root(claude_project_root) is True
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_grant_is_valid_project_root_rejects_unmarked_directory(tmp_path: Path) -> None:
|
|
49
|
-
unmarked_directory = tmp_path / "no_marker"
|
|
50
|
-
unmarked_directory.mkdir()
|
|
51
|
-
assert grant_module.is_valid_project_root(unmarked_directory) is False
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_revoke_is_valid_project_root_detects_git_marker(tmp_path: Path) -> None:
|
|
55
|
-
git_project_root = tmp_path / "git_project"
|
|
56
|
-
(git_project_root / ".git").mkdir(parents=True)
|
|
57
|
-
assert revoke_module.is_valid_project_root(git_project_root) is True
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def test_revoke_is_valid_project_root_detects_claude_marker(tmp_path: Path) -> None:
|
|
61
|
-
claude_project_root = tmp_path / "claude_project"
|
|
62
|
-
(claude_project_root / ".claude").mkdir(parents=True)
|
|
63
|
-
assert revoke_module.is_valid_project_root(claude_project_root) is True
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_revoke_is_valid_project_root_rejects_unmarked_directory(tmp_path: Path) -> None:
|
|
67
|
-
unmarked_directory = tmp_path / "no_marker"
|
|
68
|
-
unmarked_directory.mkdir()
|
|
69
|
-
assert revoke_module.is_valid_project_root(unmarked_directory) is False
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_write_atomically_with_mode_releases_fd_when_fdopen_raises(
|
|
73
|
-
tmp_path: Path,
|
|
74
|
-
) -> None:
|
|
75
|
-
"""A failure inside os.fdopen must close the raw file descriptor."""
|
|
76
|
-
target_path = tmp_path / "settings.json.tmp"
|
|
77
|
-
with patch.object(
|
|
78
|
-
common_module.os, "fdopen", side_effect=MemoryError("fdopen failure")
|
|
79
|
-
):
|
|
80
|
-
with pytest.raises(MemoryError):
|
|
81
|
-
common_module.write_atomically_with_mode(
|
|
82
|
-
target_path, "payload", file_mode=0o600
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_get_mode_to_preserve_returns_existing_file_mode(
|
|
87
|
-
tmp_path: Path,
|
|
88
|
-
) -> None:
|
|
89
|
-
"""When the file exists, the actual filesystem mode must be returned (not the default)."""
|
|
90
|
-
target_path = tmp_path / "settings.json"
|
|
91
|
-
target_path.write_text("{}", encoding="utf-8")
|
|
92
|
-
actual_filesystem_mode = target_path.stat().st_mode & 0o777
|
|
93
|
-
returned_mode = common_module.get_mode_to_preserve(target_path)
|
|
94
|
-
assert returned_mode == actual_filesystem_mode
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_write_atomically_with_mode_raises_oserror_when_open_fails(
|
|
98
|
-
tmp_path: Path,
|
|
99
|
-
) -> None:
|
|
100
|
-
"""OSError from os.open must propagate (no fd leak path to test here)."""
|
|
101
|
-
target_path = tmp_path / "subdirectory" / "missing" / "settings.json.tmp"
|
|
102
|
-
with pytest.raises(OSError):
|
|
103
|
-
common_module.write_atomically_with_mode(
|
|
104
|
-
target_path, "payload", file_mode=0o600
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_write_atomically_unlinks_temp_when_fdopen_raises(
|
|
109
|
-
tmp_path: Path,
|
|
110
|
-
) -> None:
|
|
111
|
-
"""A failure inside os.fdopen must remove the on-disk temp file.
|
|
112
|
-
|
|
113
|
-
Regression for loop1-9: the file existed the moment os.open returned,
|
|
114
|
-
but the OSError/MemoryError handler only closed the raw FD — leaving an
|
|
115
|
-
empty .tmp sibling on disk after the exception propagated up to
|
|
116
|
-
save_settings, where the unlink in the finally block was skipped because
|
|
117
|
-
`is_temp_owned_by_this_invocation` had not yet been set.
|
|
118
|
-
"""
|
|
119
|
-
target_path = tmp_path / "settings.json.tmp"
|
|
120
|
-
with patch.object(
|
|
121
|
-
common_module.os, "fdopen", side_effect=MemoryError("fdopen failure")
|
|
122
|
-
):
|
|
123
|
-
with pytest.raises(MemoryError):
|
|
124
|
-
common_module.write_atomically_with_mode(
|
|
125
|
-
target_path, "payload", file_mode=0o600
|
|
126
|
-
)
|
|
127
|
-
assert not target_path.exists(), (
|
|
128
|
-
"the temp file created by os.open must be unlinked before re-raising"
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_save_settings_uses_pid_keyed_temp_suffix(tmp_path: Path) -> None:
|
|
133
|
-
"""Concurrent save_settings invocations must not race on a deterministic temp name.
|
|
134
|
-
|
|
135
|
-
Regression for loop1-10: building the temp path as `settings.json.tmp` is
|
|
136
|
-
deterministic and unkeyed by PID, so two parallel /bugteam invocations
|
|
137
|
-
racing on Step 0 (grant) or Step 5 (revoke) collide on O_CREAT|O_EXCL —
|
|
138
|
-
the second caller hits FileExistsError and the OSError handler hard-exits,
|
|
139
|
-
silently dropping that PR's permission grant.
|
|
140
|
-
"""
|
|
141
|
-
settings_path = tmp_path / "settings.json"
|
|
142
|
-
settings_path.write_text("{}", encoding="utf-8")
|
|
143
|
-
captured_temp_paths: list[str] = []
|
|
144
|
-
real_open = common_module.os.open
|
|
145
|
-
|
|
146
|
-
def capturing_open(target: str, *args: object, **kwargs: object) -> int:
|
|
147
|
-
captured_temp_paths.append(target)
|
|
148
|
-
return real_open(target, *args, **kwargs)
|
|
149
|
-
|
|
150
|
-
with patch.object(common_module.os, "open", side_effect=capturing_open):
|
|
151
|
-
common_module.save_settings(settings_path, {"first": True})
|
|
152
|
-
common_module.save_settings(settings_path, {"second": True})
|
|
153
|
-
assert len(captured_temp_paths) == 2
|
|
154
|
-
assert all(
|
|
155
|
-
str(common_module.os.getpid()) in each_temp_path
|
|
156
|
-
for each_temp_path in captured_temp_paths
|
|
157
|
-
), (
|
|
158
|
-
"temp filename must include os.getpid() so concurrent runs do not "
|
|
159
|
-
f"collide on a deterministic name; saw: {captured_temp_paths!r}"
|
|
160
|
-
)
|
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the agent-config carve-out and stale-trust-entry purge.
|
|
2
|
-
|
|
3
|
-
Covers two Bugbot findings on PR #467:
|
|
4
|
-
- Deny rules must be written to permissions.deny so agent-config edits
|
|
5
|
-
require explicit per-edit user approval.
|
|
6
|
-
- Trust entries in autoMode.environment must be purged on grant
|
|
7
|
-
(preventing accumulation across template revisions) and removed on
|
|
8
|
-
revoke regardless of the exact template wording.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import sys
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
|
-
import pytest
|
|
19
|
-
|
|
20
|
-
_script_directory = str(Path(__file__).resolve().parent)
|
|
21
|
-
if _script_directory not in sys.path:
|
|
22
|
-
sys.path.insert(0, _script_directory)
|
|
23
|
-
|
|
24
|
-
import _bugteam_permissions_common as common_module
|
|
25
|
-
import grant_project_claude_permissions as grant_module
|
|
26
|
-
import revoke_project_claude_permissions as revoke_module
|
|
27
|
-
from bugteam_scripts_constants.claude_permissions_common_constants import (
|
|
28
|
-
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
29
|
-
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
30
|
-
ALL_PERMISSION_ALLOW_TOOLS,
|
|
31
|
-
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _make_fake_project(tmp_path: Path) -> Path:
|
|
36
|
-
fake_project_root = tmp_path / "fake_project"
|
|
37
|
-
(fake_project_root / ".claude").mkdir(parents=True)
|
|
38
|
-
return fake_project_root
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _project_path_as_posix(fake_project_root: Path) -> str:
|
|
42
|
-
return str(fake_project_root).replace("\\", "/")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _redirect_settings_to_fake_path(
|
|
46
|
-
fake_settings_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
47
|
-
) -> None:
|
|
48
|
-
fake_home_directory = fake_settings_path.parent.parent
|
|
49
|
-
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: fake_home_directory))
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _prepare_fake_home(tmp_path: Path) -> Path:
|
|
53
|
-
fake_home_directory = tmp_path / "fake_home"
|
|
54
|
-
claude_settings_directory = fake_home_directory / ".claude"
|
|
55
|
-
claude_settings_directory.mkdir(parents=True)
|
|
56
|
-
return claude_settings_directory / "settings.json"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _seed_grant_then_run(
|
|
60
|
-
fake_settings_path: Path,
|
|
61
|
-
fake_project_root: Path,
|
|
62
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
63
|
-
pre_existing_settings: dict[str, Any],
|
|
64
|
-
) -> None:
|
|
65
|
-
fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
|
|
66
|
-
_redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
|
|
67
|
-
monkeypatch.chdir(fake_project_root)
|
|
68
|
-
grant_module.grant_permissions_for_current_directory()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _seed_revoke_then_run(
|
|
72
|
-
fake_settings_path: Path,
|
|
73
|
-
fake_project_root: Path,
|
|
74
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
75
|
-
pre_existing_settings: dict[str, Any],
|
|
76
|
-
) -> None:
|
|
77
|
-
fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
|
|
78
|
-
_redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
|
|
79
|
-
monkeypatch.chdir(fake_project_root)
|
|
80
|
-
revoke_module.revoke_permissions_for_current_directory()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_grant_writes_deny_rules_for_every_tool_and_pattern(
|
|
84
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
85
|
-
) -> None:
|
|
86
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
87
|
-
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
88
|
-
_seed_grant_then_run(
|
|
89
|
-
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
90
|
-
)
|
|
91
|
-
capsys.readouterr()
|
|
92
|
-
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
93
|
-
deny_list = written_settings["permissions"]["deny"]
|
|
94
|
-
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
95
|
-
for each_tool in ALL_AGENT_CONFIG_DENY_TOOLS:
|
|
96
|
-
for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
97
|
-
expected_rule = f"{each_tool}({project_path_posix}/.claude/{each_pattern})"
|
|
98
|
-
assert expected_rule in deny_list, (
|
|
99
|
-
f"deny list missing expected rule {expected_rule!r}"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def test_grant_writes_glob_deny_rules_for_every_agent_config_pattern(
|
|
104
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
105
|
-
) -> None:
|
|
106
|
-
"""Glob must be in the deny tuple so agent-config paths require approval.
|
|
107
|
-
|
|
108
|
-
The AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE promises Edit/Write/Read/Glob
|
|
109
|
-
trust EXCEPT for agent-config files. Glob deny rules are how the EXCEPT
|
|
110
|
-
clause is honored for the Glob tool.
|
|
111
|
-
"""
|
|
112
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
113
|
-
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
114
|
-
_seed_grant_then_run(
|
|
115
|
-
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
116
|
-
)
|
|
117
|
-
capsys.readouterr()
|
|
118
|
-
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
119
|
-
deny_list = written_settings["permissions"]["deny"]
|
|
120
|
-
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
121
|
-
assert "Glob" in ALL_AGENT_CONFIG_DENY_TOOLS
|
|
122
|
-
assert "Glob" not in ALL_PERMISSION_ALLOW_TOOLS
|
|
123
|
-
for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
124
|
-
expected_glob_rule = f"Glob({project_path_posix}/.claude/{each_pattern})"
|
|
125
|
-
assert expected_glob_rule in deny_list, (
|
|
126
|
-
f"deny list missing expected Glob rule {expected_glob_rule!r}"
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_grant_purges_stale_trust_entries_then_writes_current_template(
|
|
131
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
132
|
-
) -> None:
|
|
133
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
134
|
-
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
135
|
-
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
136
|
-
stale_entry_a = (
|
|
137
|
-
f"Trusted local workspace: {project_path_posix}/.claude/** old wording form A"
|
|
138
|
-
)
|
|
139
|
-
stale_entry_b = (
|
|
140
|
-
f"Trusted local workspace: {project_path_posix}/.claude/** "
|
|
141
|
-
f"different earlier wording"
|
|
142
|
-
)
|
|
143
|
-
unrelated_entry = "Some unrelated environment hint"
|
|
144
|
-
pre_existing_settings: dict[str, Any] = {
|
|
145
|
-
"autoMode": {
|
|
146
|
-
"environment": [stale_entry_a, stale_entry_b, unrelated_entry],
|
|
147
|
-
},
|
|
148
|
-
}
|
|
149
|
-
_seed_grant_then_run(
|
|
150
|
-
fake_settings_path,
|
|
151
|
-
fake_project_root,
|
|
152
|
-
monkeypatch,
|
|
153
|
-
pre_existing_settings=pre_existing_settings,
|
|
154
|
-
)
|
|
155
|
-
captured = capsys.readouterr()
|
|
156
|
-
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
157
|
-
environment_list = written_settings["autoMode"]["environment"]
|
|
158
|
-
assert stale_entry_a not in environment_list
|
|
159
|
-
assert stale_entry_b not in environment_list
|
|
160
|
-
assert unrelated_entry in environment_list
|
|
161
|
-
matching_trust_entries = [
|
|
162
|
-
each_entry
|
|
163
|
-
for each_entry in environment_list
|
|
164
|
-
if isinstance(each_entry, str)
|
|
165
|
-
and each_entry.startswith("Trusted local workspace:")
|
|
166
|
-
and f"{project_path_posix}/.claude/**" in each_entry
|
|
167
|
-
]
|
|
168
|
-
assert len(matching_trust_entries) == 1
|
|
169
|
-
assert "Stale auto-mode environment entries purged" in captured.out
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def test_revoke_removes_deny_rules(
|
|
173
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
174
|
-
) -> None:
|
|
175
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
176
|
-
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
177
|
-
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
178
|
-
all_deny_rules = common_module.build_agent_config_deny_rules(
|
|
179
|
-
project_path_posix,
|
|
180
|
-
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
181
|
-
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
182
|
-
)
|
|
183
|
-
pre_existing_settings: dict[str, Any] = {
|
|
184
|
-
"permissions": {
|
|
185
|
-
"deny": list(all_deny_rules),
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
|
-
_seed_revoke_then_run(
|
|
189
|
-
fake_settings_path,
|
|
190
|
-
fake_project_root,
|
|
191
|
-
monkeypatch,
|
|
192
|
-
pre_existing_settings=pre_existing_settings,
|
|
193
|
-
)
|
|
194
|
-
capsys.readouterr()
|
|
195
|
-
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
196
|
-
permissions_section = written_settings.get("permissions", {})
|
|
197
|
-
remaining_deny_list = permissions_section.get("deny", [])
|
|
198
|
-
for each_rule in all_deny_rules:
|
|
199
|
-
assert each_rule not in remaining_deny_list
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def test_revoke_removes_every_legacy_trust_entry_for_project(
|
|
203
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
204
|
-
) -> None:
|
|
205
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
206
|
-
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
207
|
-
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
208
|
-
legacy_entry_a = (
|
|
209
|
-
f"Trusted local workspace: {project_path_posix}/.claude/** template revision A"
|
|
210
|
-
)
|
|
211
|
-
legacy_entry_b = (
|
|
212
|
-
f"Trusted local workspace: {project_path_posix}/.claude/** template revision B"
|
|
213
|
-
)
|
|
214
|
-
unrelated_other_project_entry = (
|
|
215
|
-
"Trusted local workspace: /some/other/project/.claude/** still valid"
|
|
216
|
-
)
|
|
217
|
-
pre_existing_settings: dict[str, Any] = {
|
|
218
|
-
"autoMode": {
|
|
219
|
-
"environment": [
|
|
220
|
-
legacy_entry_a,
|
|
221
|
-
legacy_entry_b,
|
|
222
|
-
unrelated_other_project_entry,
|
|
223
|
-
],
|
|
224
|
-
},
|
|
225
|
-
}
|
|
226
|
-
_seed_revoke_then_run(
|
|
227
|
-
fake_settings_path,
|
|
228
|
-
fake_project_root,
|
|
229
|
-
monkeypatch,
|
|
230
|
-
pre_existing_settings=pre_existing_settings,
|
|
231
|
-
)
|
|
232
|
-
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
233
|
-
environment_list = written_settings.get("autoMode", {}).get("environment", [])
|
|
234
|
-
assert legacy_entry_a not in environment_list
|
|
235
|
-
assert legacy_entry_b not in environment_list
|
|
236
|
-
assert unrelated_other_project_entry in environment_list
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def test_template_constant_documents_agent_config_carveout() -> None:
|
|
240
|
-
assert "agent-config files always require explicit per-edit user approval" in (
|
|
241
|
-
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def test_is_trust_entry_for_project_predicate_filters_by_prefix_and_project_path() -> (
|
|
246
|
-
None
|
|
247
|
-
):
|
|
248
|
-
project_path_posix = "/fake/proj"
|
|
249
|
-
trust_prefix = "Trusted local workspace:"
|
|
250
|
-
non_string_value: object = 42
|
|
251
|
-
assert (
|
|
252
|
-
common_module.is_trust_entry_for_project(
|
|
253
|
-
non_string_value, project_path_posix, trust_prefix
|
|
254
|
-
)
|
|
255
|
-
is False
|
|
256
|
-
)
|
|
257
|
-
wrong_prefix_entry = (
|
|
258
|
-
f"Something else: {project_path_posix}/.claude/** with marker token"
|
|
259
|
-
)
|
|
260
|
-
assert (
|
|
261
|
-
common_module.is_trust_entry_for_project(
|
|
262
|
-
wrong_prefix_entry, project_path_posix, trust_prefix
|
|
263
|
-
)
|
|
264
|
-
is False
|
|
265
|
-
)
|
|
266
|
-
different_project_entry = (
|
|
267
|
-
"Trusted local workspace: /other/project/.claude/** unrelated"
|
|
268
|
-
)
|
|
269
|
-
assert (
|
|
270
|
-
common_module.is_trust_entry_for_project(
|
|
271
|
-
different_project_entry, project_path_posix, trust_prefix
|
|
272
|
-
)
|
|
273
|
-
is False
|
|
274
|
-
)
|
|
275
|
-
matching_entry = (
|
|
276
|
-
f"Trusted local workspace: {project_path_posix}/.claude/** any wording form"
|
|
277
|
-
)
|
|
278
|
-
assert (
|
|
279
|
-
common_module.is_trust_entry_for_project(
|
|
280
|
-
matching_entry, project_path_posix, trust_prefix
|
|
281
|
-
)
|
|
282
|
-
is True
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
|
|
287
|
-
"""When the project_path is a path suffix of an unrelated entry's path,
|
|
288
|
-
the predicate must reject the unrelated entry (the boundary anchor case)."""
|
|
289
|
-
short_project_path = "/projects/foo"
|
|
290
|
-
trust_prefix = "Trusted local workspace:"
|
|
291
|
-
longer_unrelated_path_entry = (
|
|
292
|
-
"Trusted local workspace: /Users/jon/projects/foo/.claude/** unrelated path"
|
|
293
|
-
)
|
|
294
|
-
assert (
|
|
295
|
-
common_module.is_trust_entry_for_project(
|
|
296
|
-
longer_unrelated_path_entry, short_project_path, trust_prefix
|
|
297
|
-
)
|
|
298
|
-
is False
|
|
299
|
-
)
|
|
300
|
-
quoted_matching_entry = (
|
|
301
|
-
f'Trusted local workspace: "{short_project_path}/.claude/**" quoted form'
|
|
302
|
-
)
|
|
303
|
-
assert (
|
|
304
|
-
common_module.is_trust_entry_for_project(
|
|
305
|
-
quoted_matching_entry, short_project_path, trust_prefix
|
|
306
|
-
)
|
|
307
|
-
is True
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def test_second_grant_is_idempotent_when_no_other_settings_changed(
|
|
312
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
313
|
-
) -> None:
|
|
314
|
-
"""Running grant twice in a row must perform zero changes the second time.
|
|
315
|
-
|
|
316
|
-
On the second call the existing trust entry is byte-identical to the
|
|
317
|
-
freshly-formatted current entry, so purge_stale_trust_entries treats it as
|
|
318
|
-
protected and does not remove it; add_auto_mode_environment_entry then
|
|
319
|
-
no-ops because the entry is already present.
|
|
320
|
-
"""
|
|
321
|
-
fake_project_root = _make_fake_project(tmp_path)
|
|
322
|
-
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
323
|
-
_seed_grant_then_run(
|
|
324
|
-
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
325
|
-
)
|
|
326
|
-
first_run_output = capsys.readouterr()
|
|
327
|
-
assert "No changes needed" not in first_run_output.out
|
|
328
|
-
_redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
|
|
329
|
-
monkeypatch.chdir(fake_project_root)
|
|
330
|
-
grant_module.grant_permissions_for_current_directory()
|
|
331
|
-
second_run_output = capsys.readouterr()
|
|
332
|
-
assert "No changes needed; settings file left untouched." in second_run_output.out
|
|
333
|
-
assert "Stale auto-mode environment entries purged" not in second_run_output.out
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def test_template_derives_human_readable_pattern_list_from_pattern_tuple() -> None:
|
|
337
|
-
"""Every pattern in ALL_AGENT_CONFIG_PATH_PATTERNS must surface in the
|
|
338
|
-
rendered template through its derived human-readable form, and the
|
|
339
|
-
template must still expose the {project_path} placeholder for .format()
|
|
340
|
-
substitution at runtime."""
|
|
341
|
-
template_text: str = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
342
|
-
assert "{project_path}" in template_text
|
|
343
|
-
for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
344
|
-
if each_pattern.endswith("/**"):
|
|
345
|
-
directory_name = each_pattern[: -len("/**")]
|
|
346
|
-
expected_phrase = f"anything under {directory_name}/"
|
|
347
|
-
elif each_pattern == "mcp.json":
|
|
348
|
-
expected_phrase = "the mcp.json file"
|
|
349
|
-
else:
|
|
350
|
-
expected_phrase = each_pattern
|
|
351
|
-
assert expected_phrase in template_text, (
|
|
352
|
-
f"template missing derived phrase for pattern {each_pattern!r}: "
|
|
353
|
-
f"expected {expected_phrase!r}"
|
|
354
|
-
)
|
|
355
|
-
rendered_template_text = template_text.format(project_path="/tmp/x")
|
|
356
|
-
assert "/tmp/x" in rendered_template_text
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
_script_directory = str(Path(__file__).resolve().parent)
|
|
8
|
-
if _script_directory not in sys.path:
|
|
9
|
-
sys.path.insert(0, _script_directory)
|
|
10
|
-
|
|
11
|
-
import _bugteam_permissions_common as common_module
|
|
12
|
-
from _bugteam_permissions_common import (
|
|
13
|
-
build_permission_rule,
|
|
14
|
-
get_current_project_path,
|
|
15
|
-
path_contains_glob_metacharacters,
|
|
16
|
-
save_settings,
|
|
17
|
-
)
|
|
18
|
-
from bugteam_scripts_constants.claude_permissions_common_constants import DEFAULT_SETTINGS_FILE_MODE
|
|
19
|
-
import grant_project_claude_permissions as grant_module
|
|
20
|
-
import revoke_project_claude_permissions as revoke_module
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_return_normalized_path_when_cwd_contains_spaces(
|
|
24
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
25
|
-
) -> None:
|
|
26
|
-
directory_with_spaces = tmp_path / "dir with spaces"
|
|
27
|
-
directory_with_spaces.mkdir()
|
|
28
|
-
monkeypatch.chdir(directory_with_spaces)
|
|
29
|
-
returned_project_path = get_current_project_path()
|
|
30
|
-
expected_suffix = "/dir with spaces"
|
|
31
|
-
assert returned_project_path.endswith(expected_suffix)
|
|
32
|
-
assert "\\" not in returned_project_path
|
|
33
|
-
built_rule = build_permission_rule("Edit", returned_project_path)
|
|
34
|
-
assert built_rule.startswith("Edit(")
|
|
35
|
-
assert built_rule.endswith("/.claude/**)")
|
|
36
|
-
assert "dir with spaces" in built_rule
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_raise_when_cwd_contains_glob_metacharacters(
|
|
40
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
41
|
-
) -> None:
|
|
42
|
-
directory_with_star = tmp_path / "weird[dir]"
|
|
43
|
-
directory_with_star.mkdir()
|
|
44
|
-
monkeypatch.chdir(directory_with_star)
|
|
45
|
-
with pytest.raises(ValueError, match="glob metacharacters"):
|
|
46
|
-
get_current_project_path()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_flag_glob_metacharacters_in_any_position() -> None:
|
|
50
|
-
assert path_contains_glob_metacharacters("/home/user/[dir]/project")
|
|
51
|
-
assert path_contains_glob_metacharacters("/home/user/project*")
|
|
52
|
-
assert not path_contains_glob_metacharacters("/home/user/dir with spaces")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_save_settings_logs_when_temp_unlink_fails(
|
|
56
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
57
|
-
) -> None:
|
|
58
|
-
"""A swallowed unlink in the finally block must surface to stderr.
|
|
59
|
-
|
|
60
|
-
Forces a write success followed by os.replace failure so the temp file
|
|
61
|
-
survives into the finally branch, then makes Path.unlink raise.
|
|
62
|
-
"""
|
|
63
|
-
settings_path = tmp_path / "settings.json"
|
|
64
|
-
settings_path.write_text('{"existing": true}\n', encoding="utf-8")
|
|
65
|
-
|
|
66
|
-
def failing_replace(*_args: object, **_kwargs: object) -> None:
|
|
67
|
-
raise OSError("replace blocked by AV")
|
|
68
|
-
|
|
69
|
-
def failing_unlink(self: Path, *args: object, **kwargs: object) -> None:
|
|
70
|
-
raise PermissionError("temp file held by AV")
|
|
71
|
-
|
|
72
|
-
with patch.object(common_module.os, "replace", failing_replace):
|
|
73
|
-
with patch.object(Path, "unlink", failing_unlink):
|
|
74
|
-
with pytest.raises(SystemExit):
|
|
75
|
-
save_settings(settings_path, {"new_key": "value"})
|
|
76
|
-
captured = capsys.readouterr()
|
|
77
|
-
assert ".tmp" in captured.err
|
|
78
|
-
assert "PermissionError" in captured.err or "held by AV" in captured.err
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def test_save_settings_finally_skips_unlink_when_no_temp_was_created(
|
|
82
|
-
tmp_path: Path,
|
|
83
|
-
) -> None:
|
|
84
|
-
"""When this invocation never created the temp file, finally must not unlink it."""
|
|
85
|
-
settings_path = tmp_path / "settings.json"
|
|
86
|
-
settings_path.write_text('{"existing": true}\n', encoding="utf-8")
|
|
87
|
-
|
|
88
|
-
unlink_call_paths: list[Path] = []
|
|
89
|
-
original_unlink = Path.unlink
|
|
90
|
-
|
|
91
|
-
def recording_unlink(self: Path, *args: object, **kwargs: object) -> None:
|
|
92
|
-
unlink_call_paths.append(self)
|
|
93
|
-
original_unlink(self, *args, **kwargs)
|
|
94
|
-
|
|
95
|
-
def write_raises(*_args: object, **_kwargs: object) -> None:
|
|
96
|
-
raise FileExistsError("another writer's temp")
|
|
97
|
-
|
|
98
|
-
with patch.object(common_module, "write_atomically_with_mode", write_raises):
|
|
99
|
-
with patch.object(Path, "unlink", recording_unlink):
|
|
100
|
-
with pytest.raises(SystemExit):
|
|
101
|
-
common_module.save_settings(settings_path, {"new_key": "value"})
|
|
102
|
-
assert all(
|
|
103
|
-
each_path.suffix != ".tmp"
|
|
104
|
-
for each_path in unlink_call_paths
|
|
105
|
-
), (
|
|
106
|
-
"finally must not unlink a temp file this invocation never created"
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_default_settings_file_mode_used_when_settings_file_missing(
|
|
111
|
-
tmp_path: Path,
|
|
112
|
-
) -> None:
|
|
113
|
-
"""get_mode_to_preserve must fall back to DEFAULT_SETTINGS_FILE_MODE."""
|
|
114
|
-
missing_settings_path = tmp_path / "no_such_file.json"
|
|
115
|
-
returned_mode = common_module.get_mode_to_preserve(missing_settings_path)
|
|
116
|
-
assert returned_mode == DEFAULT_SETTINGS_FILE_MODE
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_is_valid_project_root_exported_from_consumer_modules(
|
|
120
|
-
tmp_path: Path,
|
|
121
|
-
) -> None:
|
|
122
|
-
"""is_valid_project_root behaviour matches across both consumers.
|
|
123
|
-
|
|
124
|
-
Grant and revoke each define their own local copy of the helper, so
|
|
125
|
-
both copies must agree on the .git / .claude marker contract.
|
|
126
|
-
"""
|
|
127
|
-
git_marker_project_root = tmp_path / "git_project"
|
|
128
|
-
(git_marker_project_root / ".git").mkdir(parents=True)
|
|
129
|
-
claude_marker_project_root = tmp_path / "claude_project"
|
|
130
|
-
(claude_marker_project_root / ".claude").mkdir(parents=True)
|
|
131
|
-
bare_directory = tmp_path / "no_marker"
|
|
132
|
-
bare_directory.mkdir()
|
|
133
|
-
assert grant_module.is_valid_project_root(git_marker_project_root) is True
|
|
134
|
-
assert grant_module.is_valid_project_root(claude_marker_project_root) is True
|
|
135
|
-
assert grant_module.is_valid_project_root(bare_directory) is False
|
|
136
|
-
assert revoke_module.is_valid_project_root(git_marker_project_root) is True
|
|
137
|
-
assert revoke_module.is_valid_project_root(claude_marker_project_root) is True
|
|
138
|
-
assert revoke_module.is_valid_project_root(bare_directory) is False
|
|
139
|
-
|
|
140
|
-
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# doc-gist
|
|
2
|
-
|
|
3
|
-
Designs fresh HTML artifacts and publishes them as secret GitHub gists with a shareable htmlpreview URL. Triggered by `/doc-gist`, `publish this`, `share as a gist`, `open this as a webpage`, `make me a writeup`, or any request ending in a shareable HTML preview URL.
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
The skill ships transport, not templates. For each request the model designs a fresh HTML artifact suited to the work, marks it with `<!-- @publish-as-gist -->`, and writes it to disk. A PostToolUse hook (`hooks/workflow/doc_gist_auto_publish.py`) spots the marker, runs `packages/claude-dev-env/skills/doc-gist/scripts/gist_upload.py`, and prints the gist and htmlpreview URLs into the tool output for the user to click.
|
|
8
|
-
|
|
9
|
-
## Key files
|
|
10
|
-
|
|
11
|
-
| File | Purpose |
|
|
12
|
-
|---|---|
|
|
13
|
-
| `SKILL.md` | Full instructions: the auto-publish hook flow, the transport script CLI, a gallery-to-artifact-type mapping table, and gotchas (`gh` auth, marker syntax, self-contained HTML only, secret-not-private note). |
|
|
14
|
-
|
|
15
|
-
## Subdirectories
|
|
16
|
-
|
|
17
|
-
| Directory | Role |
|
|
18
|
-
|---|---|
|
|
19
|
-
| `references/` | Reference material — the example HTML gallery and related docs. |
|
|
20
|
-
| `scripts/` | The `gist_upload.py` transport script and its constants package. |
|
|
21
|
-
|
|
22
|
-
## Auto-publish vs manual
|
|
23
|
-
|
|
24
|
-
- **Auto:** write HTML with `<!-- @publish-as-gist -->` anywhere in the file; the hook handles the rest.
|
|
25
|
-
- **Manual:** run `python scripts/gist_upload.py --input <path>` for an existing file or when the marker route does not apply.
|