claude-dev-env 1.28.1 → 1.29.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/agents/caveman.md +74 -0
- package/hooks/blocking/code_rules_enforcer.py +82 -7
- package/hooks/blocking/code_rules_path_utils.py +31 -0
- package/hooks/blocking/es_exe_path_rewriter.py +159 -0
- package/hooks/blocking/hedging_language_blocker.py +12 -2
- package/hooks/blocking/test_code_rules_enforcer.py +148 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +123 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_code_rules_path_utils.py +52 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +369 -0
- package/hooks/blocking/test_hedging_language_blocker.py +7 -6
- package/hooks/config/dynamic_stderr_handler.py +22 -0
- package/hooks/config/path_rewriter_constants.py +13 -0
- package/hooks/config/project_paths_reader.py +78 -0
- package/hooks/config/setup_project_paths_constants.py +41 -0
- package/hooks/config/test_dynamic_stderr_handler.py +48 -0
- package/hooks/config/test_messages.py +5 -1
- package/hooks/config/test_path_rewriter_constants.py +57 -0
- package/hooks/config/test_project_paths_reader.py +149 -0
- package/hooks/config/test_setup_project_paths_constants.py +74 -0
- package/hooks/git-hooks/test_config.py +1 -0
- package/hooks/git-hooks/test_gate_utils.py +1 -0
- package/hooks/git-hooks/test_pre_commit.py +1 -0
- package/hooks/git-hooks/test_pre_push.py +1 -0
- package/hooks/hooks.json +10 -0
- package/hooks/session/test_untracked_repo_detector.py +192 -0
- package/hooks/session/untracked_repo_detector.py +103 -0
- package/hooks/validators/exempt_paths.py +17 -14
- package/hooks/validators/test_exempt_paths.py +65 -0
- package/hooks/validators/test_git_checks.py +17 -17
- package/package.json +1 -1
- package/scripts/config/__init__.py +1 -0
- package/scripts/config/groq_bugteam_config.py +118 -0
- package/scripts/config/test_groq_bugteam_config.py +72 -0
- package/scripts/groq_bugteam.README.md +129 -0
- package/scripts/groq_bugteam.py +586 -0
- package/scripts/setup_project_paths.py +347 -0
- package/scripts/test_groq_bugteam.py +391 -0
- package/scripts/test_setup_project_paths.py +532 -0
- package/scripts/test_setup_project_paths_config.py +6 -0
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +5 -5
- package/skills/bugteam/SKILL_EVALS.md +5 -5
- package/skills/bugteam/reference/audit-and-teammates.md +3 -3
- package/skills/bugteam/reference/audit-contract.md +159 -0
- package/skills/bugteam/reference/team-setup.md +2 -2
- package/skills/bugteam/scripts/bugteam_preflight.py +66 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +189 -0
- package/skills/copilot-review/SKILL.md +145 -0
- package/skills/findbugs/SKILL.md +14 -22
- package/skills/qbug/SKILL.md +56 -13
- package/skills/qbug/test_qbug_skill_audit_schema.py +156 -0
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +103 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Load the per-user project-path registry from ~/.claude/project-paths.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from config.dynamic_stderr_handler import DynamicStderrHandler
|
|
11
|
+
from config.setup_project_paths_constants import META_KEY, UTF8_ENCODING
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger("project_paths_reader")
|
|
15
|
+
if not _logger.handlers:
|
|
16
|
+
_stderr_handler = DynamicStderrHandler()
|
|
17
|
+
_stderr_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
|
18
|
+
_logger.addHandler(_stderr_handler)
|
|
19
|
+
_logger.setLevel(logging.INFO)
|
|
20
|
+
_logger.propagate = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def registry_file_path() -> Path:
|
|
24
|
+
"""Return the canonical path to ~/.claude/project-paths.json."""
|
|
25
|
+
return Path.home() / ".claude" / "project-paths.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _default_config_path() -> Path:
|
|
29
|
+
return registry_file_path()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _normalize_path_separators(raw_path: str) -> str:
|
|
33
|
+
forward_slash_form = raw_path.replace("\\", "/")
|
|
34
|
+
return os.path.normcase(os.path.normpath(forward_slash_form)).replace("\\", "/")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_registry(config_path: Path | None = None) -> dict[str, str]:
|
|
38
|
+
"""Return the name-to-absolute-path mapping with the _meta key stripped.
|
|
39
|
+
|
|
40
|
+
Returns an empty dict when the file is missing, unreadable, malformed,
|
|
41
|
+
or otherwise invalid. Logs one line to stderr when the file cannot be
|
|
42
|
+
read or contains malformed JSON.
|
|
43
|
+
"""
|
|
44
|
+
resolved_path = config_path if config_path is not None else _default_config_path()
|
|
45
|
+
if not resolved_path.is_file():
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
raw_text = resolved_path.read_text(encoding=UTF8_ENCODING)
|
|
49
|
+
except OSError as e:
|
|
50
|
+
_logger.error("cannot read %s: %s", resolved_path, e)
|
|
51
|
+
return {}
|
|
52
|
+
try:
|
|
53
|
+
parsed = json.loads(raw_text)
|
|
54
|
+
except json.JSONDecodeError as e:
|
|
55
|
+
_logger.error("malformed JSON in %s: %s", resolved_path, e)
|
|
56
|
+
return {}
|
|
57
|
+
if not isinstance(parsed, dict):
|
|
58
|
+
return {}
|
|
59
|
+
return {
|
|
60
|
+
each_key: each_value
|
|
61
|
+
for each_key, each_value in parsed.items()
|
|
62
|
+
if each_key != META_KEY
|
|
63
|
+
and isinstance(each_key, str)
|
|
64
|
+
and isinstance(each_value, str)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def registry_contains_path(known_registry: dict[str, str], path_to_find: str) -> bool:
|
|
69
|
+
"""Return True when the given path appears as any registry value.
|
|
70
|
+
|
|
71
|
+
Normalizes both sides before comparing so Windows and POSIX separator
|
|
72
|
+
forms of the same path compare equal.
|
|
73
|
+
"""
|
|
74
|
+
normalized_target = _normalize_path_separators(path_to_find)
|
|
75
|
+
for each_registered_path in known_registry.values():
|
|
76
|
+
if _normalize_path_separators(each_registered_path) == normalized_target:
|
|
77
|
+
return True
|
|
78
|
+
return False
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Configuration constants for the setup_project_paths bootstrap script.
|
|
2
|
+
|
|
3
|
+
Shared constants consumed by two or more modules across the hook subsystem.
|
|
4
|
+
Single-use values are inlined into their consuming functions per the
|
|
5
|
+
file-global-constants use-count rule.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS = ("/ad", "folder:.git")
|
|
11
|
+
|
|
12
|
+
EXCLUDED_PATH_SEGMENTS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"temp",
|
|
15
|
+
"tmp",
|
|
16
|
+
"worktree",
|
|
17
|
+
"node_modules",
|
|
18
|
+
".cache",
|
|
19
|
+
"$recycle.bin",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
JSON_INDENT_SPACES = 2
|
|
24
|
+
|
|
25
|
+
GIT_DIRECTORY_SEGMENT_NAME = ".git"
|
|
26
|
+
|
|
27
|
+
ES_EXE_BINARY_NAME = "es.exe"
|
|
28
|
+
|
|
29
|
+
SUPPORTED_SCHEMA_VERSION = 1
|
|
30
|
+
|
|
31
|
+
META_KEY = "_meta"
|
|
32
|
+
|
|
33
|
+
UTF8_ENCODING = "utf-8"
|
|
34
|
+
|
|
35
|
+
CONFIRMATION_PROMPT_TEXT = "Write this mapping to the config file? (yes/no): "
|
|
36
|
+
|
|
37
|
+
ABORTED_NOTHING_WRITTEN_MESSAGE = "Aborted. Nothing written."
|
|
38
|
+
|
|
39
|
+
WROTE_ENTRIES_STATUS_TEMPLATE = "Wrote {entry_count} entries to {save_path}."
|
|
40
|
+
|
|
41
|
+
STDERR_TRUNCATION_LENGTH = 200
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tests for DynamicStderrHandler — resolves sys.stderr at emit time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
12
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
14
|
+
|
|
15
|
+
from config.dynamic_stderr_handler import DynamicStderrHandler
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_logger_with_handler() -> tuple[logging.Logger, DynamicStderrHandler]:
|
|
19
|
+
handler_instance = DynamicStderrHandler()
|
|
20
|
+
handler_instance.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
|
21
|
+
test_logger = logging.getLogger("test_dynamic_stderr_handler_logger")
|
|
22
|
+
for each_existing_handler in list(test_logger.handlers):
|
|
23
|
+
test_logger.removeHandler(each_existing_handler)
|
|
24
|
+
test_logger.addHandler(handler_instance)
|
|
25
|
+
test_logger.setLevel(logging.INFO)
|
|
26
|
+
test_logger.propagate = False
|
|
27
|
+
return test_logger, handler_instance
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_emit_writes_to_current_sys_stderr() -> None:
|
|
31
|
+
test_logger, _ = _make_logger_with_handler()
|
|
32
|
+
captured_stderr = StringIO()
|
|
33
|
+
with patch("sys.stderr", captured_stderr):
|
|
34
|
+
test_logger.error("hello from handler")
|
|
35
|
+
assert "hello from handler" in captured_stderr.getvalue()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_emit_resolves_stderr_at_emit_time_not_construction_time() -> None:
|
|
39
|
+
test_logger, _ = _make_logger_with_handler()
|
|
40
|
+
first_captured_stderr = StringIO()
|
|
41
|
+
second_captured_stderr = StringIO()
|
|
42
|
+
with patch("sys.stderr", first_captured_stderr):
|
|
43
|
+
test_logger.error("first message")
|
|
44
|
+
with patch("sys.stderr", second_captured_stderr):
|
|
45
|
+
test_logger.error("second message")
|
|
46
|
+
assert "first message" in first_captured_stderr.getvalue()
|
|
47
|
+
assert "second message" in second_captured_stderr.getvalue()
|
|
48
|
+
assert "second message" not in first_captured_stderr.getvalue()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Pin tests for path_rewriter_constants — values consumed by es_exe_path_rewriter."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
9
|
+
|
|
10
|
+
from config.path_rewriter_constants import (
|
|
11
|
+
BASH_TOOL_NAME,
|
|
12
|
+
HOOK_EVENT_NAME,
|
|
13
|
+
PERMISSION_ALLOW,
|
|
14
|
+
PLACEHOLDER_TOKEN_PATTERN,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_bash_tool_name_is_bash() -> None:
|
|
19
|
+
assert BASH_TOOL_NAME == "Bash"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_hook_event_name_is_pre_tool_use() -> None:
|
|
23
|
+
assert HOOK_EVENT_NAME == "PreToolUse"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_permission_allow_is_allow() -> None:
|
|
27
|
+
assert PERMISSION_ALLOW == "allow"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_placeholder_token_pattern_matches_curly_brace_form() -> None:
|
|
31
|
+
match = PLACEHOLDER_TOKEN_PATTERN.match("{my-repo}")
|
|
32
|
+
assert match is not None
|
|
33
|
+
assert match.group(1) == "my-repo"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_placeholder_token_pattern_matches_double_quoted_form() -> None:
|
|
37
|
+
match = PLACEHOLDER_TOKEN_PATTERN.match('"{my-repo}"')
|
|
38
|
+
assert match is not None
|
|
39
|
+
assert match.group(1) == "my-repo"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_placeholder_token_pattern_matches_single_quoted_form() -> None:
|
|
43
|
+
match = PLACEHOLDER_TOKEN_PATTERN.match("'{my-repo}'")
|
|
44
|
+
assert match is not None
|
|
45
|
+
assert match.group(1) == "my-repo"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_placeholder_token_pattern_does_not_match_shell_parameter_expansion() -> None:
|
|
49
|
+
assert PLACEHOLDER_TOKEN_PATTERN.search("${myrepo}") is None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_placeholder_token_pattern_does_not_match_embedded_in_flag() -> None:
|
|
53
|
+
assert PLACEHOLDER_TOKEN_PATTERN.search("--flag={my-repo}") is None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_placeholder_token_pattern_does_not_match_embedded_in_token() -> None:
|
|
57
|
+
assert PLACEHOLDER_TOKEN_PATTERN.search("foo{my-repo}bar") is None
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for project_paths_reader — config reader for ~/.claude/project-paths.json."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
11
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
13
|
+
|
|
14
|
+
from config import project_paths_reader
|
|
15
|
+
from config.project_paths_reader import (
|
|
16
|
+
load_registry,
|
|
17
|
+
registry_contains_path,
|
|
18
|
+
registry_file_path,
|
|
19
|
+
)
|
|
20
|
+
from config.setup_project_paths_constants import META_KEY
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_reader_does_not_redefine_dynamic_stderr_handler_locally() -> None:
|
|
24
|
+
"""Pin PR #230 round 3 DRY fix: handler is imported from the shared module.
|
|
25
|
+
|
|
26
|
+
Both project_paths_reader and es_exe_path_rewriter previously defined
|
|
27
|
+
identical `_DynamicStderrHandler` classes. This test fails if the
|
|
28
|
+
duplicate class reappears in project_paths_reader.
|
|
29
|
+
"""
|
|
30
|
+
assert not hasattr(project_paths_reader, "_DynamicStderrHandler")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_load_registry_returns_empty_dict_when_file_missing(tmp_path: Path) -> None:
|
|
34
|
+
missing_path = tmp_path / "nonexistent.json"
|
|
35
|
+
loaded_registry = load_registry(config_path=missing_path)
|
|
36
|
+
assert loaded_registry == {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_load_registry_returns_empty_dict_when_json_is_malformed(
|
|
40
|
+
tmp_path: Path,
|
|
41
|
+
) -> None:
|
|
42
|
+
malformed_file = tmp_path / "project-paths.json"
|
|
43
|
+
malformed_file.write_text("{ not valid json", encoding="utf-8")
|
|
44
|
+
loaded_registry = load_registry(config_path=malformed_file)
|
|
45
|
+
assert loaded_registry == {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_load_registry_strips_meta_key(tmp_path: Path) -> None:
|
|
49
|
+
registry_file = tmp_path / "project-paths.json"
|
|
50
|
+
registry_file.write_text(
|
|
51
|
+
json.dumps(
|
|
52
|
+
{
|
|
53
|
+
"_meta": {"schema_version": 1, "last_scan": "2026-01-01T00:00:00Z"},
|
|
54
|
+
"my-repo": "Y:\\Projects\\my-repo",
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
encoding="utf-8",
|
|
58
|
+
)
|
|
59
|
+
loaded_registry = load_registry(config_path=registry_file)
|
|
60
|
+
assert "_meta" not in loaded_registry
|
|
61
|
+
assert loaded_registry["my-repo"] == "Y:\\Projects\\my-repo"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_load_registry_returns_name_to_path_mapping(tmp_path: Path) -> None:
|
|
65
|
+
registry_file = tmp_path / "project-paths.json"
|
|
66
|
+
registry_file.write_text(
|
|
67
|
+
json.dumps(
|
|
68
|
+
{
|
|
69
|
+
"repo-alpha": "Y:\\Projects\\repo-alpha",
|
|
70
|
+
"repo-beta": "C:\\Dev\\repo-beta",
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
)
|
|
75
|
+
loaded_registry = load_registry(config_path=registry_file)
|
|
76
|
+
assert loaded_registry == {
|
|
77
|
+
"repo-alpha": "Y:\\Projects\\repo-alpha",
|
|
78
|
+
"repo-beta": "C:\\Dev\\repo-beta",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_load_registry_returns_empty_dict_when_top_level_is_not_object(
|
|
83
|
+
tmp_path: Path,
|
|
84
|
+
) -> None:
|
|
85
|
+
registry_file = tmp_path / "project-paths.json"
|
|
86
|
+
registry_file.write_text(json.dumps(["a", "b"]), encoding="utf-8")
|
|
87
|
+
loaded_registry = load_registry(config_path=registry_file)
|
|
88
|
+
assert loaded_registry == {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_registry_contains_path_returns_true_when_path_present(tmp_path: Path) -> None:
|
|
92
|
+
known_registry = {"my-repo": str(tmp_path)}
|
|
93
|
+
assert registry_contains_path(known_registry, str(tmp_path)) is True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_registry_contains_path_returns_false_when_path_absent(tmp_path: Path) -> None:
|
|
97
|
+
known_registry = {"other-repo": "C:\\Other\\Path"}
|
|
98
|
+
assert registry_contains_path(known_registry, str(tmp_path)) is False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_registry_contains_path_normalizes_separators(tmp_path: Path) -> None:
|
|
102
|
+
forward_slash_path = str(tmp_path).replace("\\", "/")
|
|
103
|
+
known_registry = {"my-repo": str(tmp_path)}
|
|
104
|
+
assert registry_contains_path(known_registry, forward_slash_path) is True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_registry_contains_path_treats_backslash_and_forward_slash_as_equal() -> None:
|
|
108
|
+
backslash_path = "C:\\foo\\bar"
|
|
109
|
+
forward_slash_path = "C:/foo/bar"
|
|
110
|
+
known_registry = {"my-repo": backslash_path}
|
|
111
|
+
assert registry_contains_path(known_registry, forward_slash_path) is True
|
|
112
|
+
known_registry_forward = {"my-repo": forward_slash_path}
|
|
113
|
+
assert registry_contains_path(known_registry_forward, backslash_path) is True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_registry_file_path_returns_dot_claude_project_paths_json() -> None:
|
|
117
|
+
resolved_path = registry_file_path()
|
|
118
|
+
assert resolved_path.name == "project-paths.json"
|
|
119
|
+
assert resolved_path.parent.name == ".claude"
|
|
120
|
+
assert resolved_path.parent.parent == Path.home()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_registry_file_path_is_absolute() -> None:
|
|
124
|
+
resolved_path = registry_file_path()
|
|
125
|
+
assert resolved_path.is_absolute()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_load_registry_uses_shared_meta_key_constant() -> None:
|
|
129
|
+
"""Pin: load_registry must import META_KEY from setup_project_paths_constants.
|
|
130
|
+
|
|
131
|
+
The bare string literal "_meta" must not appear in load_registry;
|
|
132
|
+
it must use the shared META_KEY constant as the single source of truth.
|
|
133
|
+
"""
|
|
134
|
+
source = inspect.getsource(project_paths_reader.load_registry)
|
|
135
|
+
assert '"_meta"' not in source, (
|
|
136
|
+
'load_registry must use META_KEY constant, not the bare literal "_meta"'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_load_registry_uses_shared_utf8_encoding_constant() -> None:
|
|
141
|
+
"""Pin fix_1: load_registry must import UTF8_ENCODING from setup_project_paths_constants.
|
|
142
|
+
|
|
143
|
+
The bare string literal "utf-8" in the read_text call must be replaced
|
|
144
|
+
with the shared constant so there is one source of truth for the encoding name.
|
|
145
|
+
"""
|
|
146
|
+
source = inspect.getsource(project_paths_reader)
|
|
147
|
+
assert 'encoding="utf-8"' not in source, (
|
|
148
|
+
'load_registry must use UTF8_ENCODING constant, not the bare literal "utf-8"'
|
|
149
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Regression-guard tests for setup_project_paths_constants module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
9
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
11
|
+
|
|
12
|
+
import config.setup_project_paths_constants as constants_module
|
|
13
|
+
from config.setup_project_paths_constants import (
|
|
14
|
+
ABORTED_NOTHING_WRITTEN_MESSAGE,
|
|
15
|
+
CONFIRMATION_PROMPT_TEXT,
|
|
16
|
+
ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS,
|
|
17
|
+
STDERR_TRUNCATION_LENGTH,
|
|
18
|
+
WROTE_ENTRIES_STATUS_TEMPLATE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_es_exe_arguments_is_immutable_tuple() -> None:
|
|
23
|
+
"""Pin PR #230 round 6: constant must be a tuple, not a mutable list.
|
|
24
|
+
|
|
25
|
+
Tuples unpack identically into subprocess.run([...]) args and prevent
|
|
26
|
+
accidental mutation of the shared constant at call sites.
|
|
27
|
+
"""
|
|
28
|
+
assert isinstance(ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS, tuple)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_es_exe_arguments_contains_folders_only_flag() -> None:
|
|
32
|
+
assert "/ad" in ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_es_exe_arguments_contains_git_folder_query() -> None:
|
|
36
|
+
assert "folder:.git" in ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_es_exe_arguments_do_not_include_name_flag() -> None:
|
|
40
|
+
assert "-name" not in ES_EXE_FOLDERS_ONLY_QUERY_ARGUMENTS
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_user_config_file_relative_parts_is_removed() -> None:
|
|
44
|
+
"""Pin PR #230 round 7: dead constant USER_CONFIG_FILE_RELATIVE_PARTS removed.
|
|
45
|
+
|
|
46
|
+
This constant had zero references after round 6 moved the canonical
|
|
47
|
+
source to registry_file_path() in project_paths_reader.py.
|
|
48
|
+
"""
|
|
49
|
+
assert not hasattr(constants_module, "USER_CONFIG_FILE_RELATIVE_PARTS")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_confirmation_prompt_text_constant_exists() -> None:
|
|
53
|
+
"""Pin PR #230 round 7: prompt string extracted from prompt_and_write magic values."""
|
|
54
|
+
assert isinstance(CONFIRMATION_PROMPT_TEXT, str)
|
|
55
|
+
assert len(CONFIRMATION_PROMPT_TEXT) > 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_aborted_nothing_written_message_constant_exists() -> None:
|
|
59
|
+
"""Pin PR #230 round 7: abort message extracted from prompt_and_write magic values."""
|
|
60
|
+
assert isinstance(ABORTED_NOTHING_WRITTEN_MESSAGE, str)
|
|
61
|
+
assert len(ABORTED_NOTHING_WRITTEN_MESSAGE) > 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_wrote_entries_status_template_constant_exists() -> None:
|
|
65
|
+
"""Pin PR #230 round 7: success message template extracted from prompt_and_write magic values."""
|
|
66
|
+
assert isinstance(WROTE_ENTRIES_STATUS_TEMPLATE, str)
|
|
67
|
+
assert "{entry_count}" in WROTE_ENTRIES_STATUS_TEMPLATE
|
|
68
|
+
assert "{save_path}" in WROTE_ENTRIES_STATUS_TEMPLATE
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_stderr_truncation_length_constant_exists() -> None:
|
|
72
|
+
"""Pin PR #230 round 9: STDERR_TRUNCATION_LENGTH required for EverythingScanError messages."""
|
|
73
|
+
assert isinstance(STDERR_TRUNCATION_LENGTH, int)
|
|
74
|
+
assert STDERR_TRUNCATION_LENGTH > 0
|
package/hooks/hooks.json
CHANGED
|
@@ -60,6 +60,11 @@
|
|
|
60
60
|
{
|
|
61
61
|
"matcher": "Bash",
|
|
62
62
|
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/es_exe_path_rewriter.py",
|
|
66
|
+
"timeout": 10
|
|
67
|
+
},
|
|
63
68
|
{
|
|
64
69
|
"type": "command",
|
|
65
70
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/destructive_command_blocker.py",
|
|
@@ -106,6 +111,11 @@
|
|
|
106
111
|
"type": "command",
|
|
107
112
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/plugin_data_dir_cleanup.py",
|
|
108
113
|
"timeout": 10
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"type": "command",
|
|
117
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/untracked_repo_detector.py",
|
|
118
|
+
"timeout": 10
|
|
109
119
|
}
|
|
110
120
|
]
|
|
111
121
|
}
|