claude-dev-env 1.92.1 → 1.93.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 +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Behavior tests for the shared in-process hosted-hook runner.
|
|
2
|
+
|
|
3
|
+
Each test writes a real probe hook to a temp directory, runs it through
|
|
4
|
+
run_hook_capturing_output, and asserts on the captured stdout, the crash flag,
|
|
5
|
+
and the restoration of the interpreter's stdin, stdout, and argv.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
|
|
15
|
+
if _HOOKS_DIR not in sys.path:
|
|
16
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
17
|
+
|
|
18
|
+
from hooks_constants.hosted_hook_runner import HostedHookRun, run_hook_capturing_output # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _write_probe(tmp_path: Path, body: str) -> str:
|
|
22
|
+
"""Write a probe hook script and return its absolute path."""
|
|
23
|
+
probe_path = tmp_path / "probe_hook.py"
|
|
24
|
+
probe_path.write_text(body, encoding="utf-8")
|
|
25
|
+
return str(probe_path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_captures_stdout_from_a_printing_hook(tmp_path: Path) -> None:
|
|
29
|
+
"""The runner captures what a hook prints to stdout without a crash."""
|
|
30
|
+
printed_payload = json.dumps({"hookSpecificOutput": {"permissionDecision": "deny"}})
|
|
31
|
+
probe_path = _write_probe(tmp_path, f"print({printed_payload!r})\n")
|
|
32
|
+
hook_run = run_hook_capturing_output(probe_path, "{}")
|
|
33
|
+
assert hook_run.captured_stdout.strip() == printed_payload
|
|
34
|
+
assert hook_run.did_crash is False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_hook_reads_the_swapped_stdin(tmp_path: Path) -> None:
|
|
38
|
+
"""A hook reading stdin sees the payload the runner replays."""
|
|
39
|
+
probe_path = _write_probe(
|
|
40
|
+
tmp_path,
|
|
41
|
+
"import json\nimport sys\nprint(json.load(sys.stdin)['tool_name'])\n",
|
|
42
|
+
)
|
|
43
|
+
hook_run = run_hook_capturing_output(probe_path, json.dumps({"tool_name": "Bash"}))
|
|
44
|
+
assert hook_run.captured_stdout.strip() == "Bash"
|
|
45
|
+
assert hook_run.did_crash is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_sets_argv_to_the_hook_script_path(tmp_path: Path) -> None:
|
|
49
|
+
"""The runner sets argv to the hook's own path so argv-branching hooks behave."""
|
|
50
|
+
argv_result_path = tmp_path / "observed_argv.json"
|
|
51
|
+
probe_path = _write_probe(
|
|
52
|
+
tmp_path,
|
|
53
|
+
"import json\nimport sys\nfrom pathlib import Path\n"
|
|
54
|
+
f"Path({str(argv_result_path)!r}).write_text(json.dumps(sys.argv), encoding='utf-8')\n",
|
|
55
|
+
)
|
|
56
|
+
run_hook_capturing_output(probe_path, "{}")
|
|
57
|
+
observed_argv = json.loads(argv_result_path.read_text(encoding="utf-8"))
|
|
58
|
+
assert observed_argv == [probe_path]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_restores_stdin_stdout_and_argv_after_the_run(tmp_path: Path) -> None:
|
|
62
|
+
"""The runner restores stdin, stdout, and argv to their pre-call objects."""
|
|
63
|
+
probe_path = _write_probe(tmp_path, "print('anything')\n")
|
|
64
|
+
original_stdin = sys.stdin
|
|
65
|
+
original_stdout = sys.stdout
|
|
66
|
+
original_argv = sys.argv
|
|
67
|
+
run_hook_capturing_output(probe_path, "{}")
|
|
68
|
+
assert sys.stdin is original_stdin
|
|
69
|
+
assert sys.stdout is original_stdout
|
|
70
|
+
assert sys.argv is original_argv
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_crash_reports_did_crash_and_does_not_propagate(tmp_path: Path) -> None:
|
|
74
|
+
"""A hook that raises is contained: did_crash is True and no exception escapes."""
|
|
75
|
+
probe_path = _write_probe(tmp_path, "raise ValueError('boom')\n")
|
|
76
|
+
hook_run = run_hook_capturing_output(probe_path, "{}")
|
|
77
|
+
assert hook_run.did_crash is True
|
|
78
|
+
assert hook_run.captured_stdout == ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_system_exit_zero_after_output_is_captured(tmp_path: Path) -> None:
|
|
82
|
+
"""A hook that prints then exits zero returns its output and is not a crash."""
|
|
83
|
+
probe_path = _write_probe(tmp_path, "print('decided')\nimport sys\nsys.exit(0)\n")
|
|
84
|
+
hook_run = run_hook_capturing_output(probe_path, "{}")
|
|
85
|
+
assert hook_run.captured_stdout.strip() == "decided"
|
|
86
|
+
assert hook_run.did_crash is False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_system_exit_nonzero_without_output_is_not_a_crash(tmp_path: Path) -> None:
|
|
90
|
+
"""A clean nonzero SystemExit yields no output and is not treated as a crash."""
|
|
91
|
+
probe_path = _write_probe(tmp_path, "import sys\nsys.exit(2)\n")
|
|
92
|
+
hook_run = run_hook_capturing_output(probe_path, "{}")
|
|
93
|
+
assert hook_run.did_crash is False
|
|
94
|
+
assert hook_run.captured_stdout == ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_returns_a_hosted_hook_run_instance(tmp_path: Path) -> None:
|
|
98
|
+
"""The runner returns a HostedHookRun carrying the two observable fields."""
|
|
99
|
+
probe_path = _write_probe(tmp_path, "print('x')\n")
|
|
100
|
+
hook_run = run_hook_capturing_output(probe_path, "{}")
|
|
101
|
+
assert isinstance(hook_run, HostedHookRun)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Constants for the test-module dead-scaffolding checks in ``code_rules_enforcer``.
|
|
2
|
+
|
|
3
|
+
Lives under the hooks-tree ``hooks_constants`` package so module-level
|
|
4
|
+
UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
|
|
5
|
+
requirement and share a home with the other hook-tree configuration. The
|
|
6
|
+
values here drive two test-file checks that catch scaffolding a removed
|
|
7
|
+
monkeypatch line leaves behind: a dead private module constant, and an
|
|
8
|
+
unused private-helper parameter.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
PRIVATE_NAME_PREFIX: str = "_"
|
|
14
|
+
TEST_FUNCTION_MODULE_BASENAME_PATTERN: re.Pattern[str] = re.compile(r"^(test_.+|.+_test)\.py$")
|
|
15
|
+
FIXTURE_DECORATOR_MARKER: str = "fixture"
|
|
16
|
+
SELF_PARAMETER_NAME: str = "self"
|
|
17
|
+
CLASS_METHOD_FIRST_PARAMETER_NAME: str = "cls"
|
|
18
|
+
MINIMUM_CONSTANT_NAME_LENGTH: int = 2
|
|
19
|
+
MAX_TEST_LAYOUT_ISSUES: int = 50
|
|
20
|
+
DEAD_TEST_CONSTANT_GUIDANCE: str = (
|
|
21
|
+
"module-level constant is defined in this test module but read by no other"
|
|
22
|
+
" line in the file - remove the dead constant, or reference it where its"
|
|
23
|
+
" value is needed (CODE_RULES §9.8)"
|
|
24
|
+
)
|
|
25
|
+
UNUSED_TEST_HELPER_PARAMETER_GUIDANCE: str = (
|
|
26
|
+
"parameter is declared on this private test helper but read nowhere in its"
|
|
27
|
+
" body - drop the dead parameter and stop forwarding it at every call site"
|
|
28
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tests for the NAS local-identity loader used by the ssh enforcer hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
12
|
+
|
|
13
|
+
from hooks_constants import local_identity # noqa: E402
|
|
14
|
+
|
|
15
|
+
ALL_NAS_ENV_VARS = ("CLAUDE_NAS_HOST", "CLAUDE_NAS_SSH_USER", "CLAUDE_NAS_SSH_PORT")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _clear_nas_env() -> None:
|
|
19
|
+
for each_env_var in ALL_NAS_ENV_VARS:
|
|
20
|
+
os.environ.pop(each_env_var, None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestNasValuesFromEnvironment:
|
|
24
|
+
def should_read_host_user_and_port_from_the_environment(self) -> None:
|
|
25
|
+
with patch.dict(
|
|
26
|
+
os.environ,
|
|
27
|
+
{
|
|
28
|
+
"CLAUDE_NAS_HOST": "10.0.0.5",
|
|
29
|
+
"CLAUDE_NAS_SSH_USER": "tester",
|
|
30
|
+
"CLAUDE_NAS_SSH_PORT": "2200",
|
|
31
|
+
},
|
|
32
|
+
clear=False,
|
|
33
|
+
):
|
|
34
|
+
assert local_identity.nas_host() == "10.0.0.5"
|
|
35
|
+
assert local_identity.nas_ssh_user() == "tester"
|
|
36
|
+
assert local_identity.nas_ssh_port() == 2200
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestNasValuesFromLocalFile:
|
|
40
|
+
def should_read_host_user_and_port_from_the_local_identity_file(
|
|
41
|
+
self, tmp_path: Path
|
|
42
|
+
) -> None:
|
|
43
|
+
claude_home = tmp_path / ".claude"
|
|
44
|
+
claude_home.mkdir()
|
|
45
|
+
(claude_home / "local-identity.json").write_text(
|
|
46
|
+
json.dumps(
|
|
47
|
+
{"nas": {"host": "10.1.1.9", "ssh_user": "fileuser", "ssh_port": 2222}}
|
|
48
|
+
),
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
with patch.dict(os.environ, {}, clear=False):
|
|
52
|
+
_clear_nas_env()
|
|
53
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
54
|
+
assert local_identity.nas_host() == "10.1.1.9"
|
|
55
|
+
assert local_identity.nas_ssh_user() == "fileuser"
|
|
56
|
+
assert local_identity.nas_ssh_port() == 2222
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestNasValuesPlaceholderDefault:
|
|
60
|
+
def should_return_placeholders_when_no_env_and_no_file(self, tmp_path: Path) -> None:
|
|
61
|
+
with patch.dict(os.environ, {}, clear=False):
|
|
62
|
+
_clear_nas_env()
|
|
63
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
64
|
+
assert local_identity.nas_host() == "nas.example.local"
|
|
65
|
+
assert local_identity.nas_ssh_user() == "operator"
|
|
66
|
+
assert local_identity.nas_ssh_port() == 22
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestDenyMessagesQuoteTheResolvedHost:
|
|
70
|
+
def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
|
|
71
|
+
self,
|
|
72
|
+
) -> None:
|
|
73
|
+
with patch.dict(
|
|
74
|
+
os.environ,
|
|
75
|
+
{"CLAUDE_NAS_HOST": "10.0.0.5", "CLAUDE_NAS_SSH_PORT": "2200"},
|
|
76
|
+
clear=False,
|
|
77
|
+
):
|
|
78
|
+
os.environ.pop("CLAUDE_NAS_SSH_USER", None)
|
|
79
|
+
message = local_identity.bare_ssh_binary_deny_message()
|
|
80
|
+
assert "10.0.0.5" in message
|
|
81
|
+
assert "-p 2200" in message
|
|
82
|
+
assert "BatchMode=yes" in message
|
|
83
|
+
|
|
84
|
+
def should_include_the_resolved_host_in_the_missing_batch_mode_message(self) -> None:
|
|
85
|
+
with patch.dict(os.environ, {"CLAUDE_NAS_HOST": "10.0.0.5"}, clear=False):
|
|
86
|
+
message = local_identity.missing_batch_mode_deny_message()
|
|
87
|
+
assert "10.0.0.5" in message
|
|
88
|
+
assert "BatchMode=yes" in message
|
|
@@ -41,7 +41,7 @@ class TestSessionIdPatternRejects:
|
|
|
41
41
|
assert SESSION_ID_PATTERN.match("etc/passwd") is None
|
|
42
42
|
|
|
43
43
|
def test_rejects_back_slash(self) -> None:
|
|
44
|
-
assert SESSION_ID_PATTERN.match("Users\\
|
|
44
|
+
assert SESSION_ID_PATTERN.match("Users\\example") is None
|
|
45
45
|
|
|
46
46
|
def test_rejects_parent_traversal(self) -> None:
|
|
47
47
|
assert SESSION_ID_PATTERN.match("..") is None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tests for the Stop-hook dispatcher hosted-hook roster."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
|
|
9
|
+
if _HOOKS_DIR not in sys.path:
|
|
10
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
11
|
+
|
|
12
|
+
from hooks_constants.stop_dispatcher_constants import ALL_STOP_HOSTED_HOOK_PATHS # noqa: E402
|
|
13
|
+
|
|
14
|
+
_EXPECTED_STOP_ORDER = (
|
|
15
|
+
"blocking/hedging_language_blocker.py",
|
|
16
|
+
"blocking/question_to_user_enforcer.py",
|
|
17
|
+
"blocking/intent_only_ending_blocker.py",
|
|
18
|
+
"blocking/session_handoff_blocker.py",
|
|
19
|
+
"diagnostic/hook_log_stop_wrapper.py",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_roster_lists_all_stop_hooks_in_registration_order() -> None:
|
|
24
|
+
"""The roster reproduces the standalone Stop chain order exactly."""
|
|
25
|
+
assert ALL_STOP_HOSTED_HOOK_PATHS == _EXPECTED_STOP_ORDER
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_every_roster_path_points_at_an_existing_hook() -> None:
|
|
29
|
+
"""Each roster path names a hook script present on disk, catching a typo."""
|
|
30
|
+
hooks_root = Path(__file__).resolve().parent.parent
|
|
31
|
+
for each_relative_path in ALL_STOP_HOSTED_HOOK_PATHS:
|
|
32
|
+
assert (hooks_root / each_relative_path).is_file()
|
|
@@ -6,7 +6,7 @@ AST-based Python style checks for code quality enforcement.
|
|
|
6
6
|
|
|
7
7
|
1. **Imports at top** - All import statements must be at the top of the file
|
|
8
8
|
2. **No empty line after decorators** - Decorators must be directly above functions (no blank lines)
|
|
9
|
-
3. **
|
|
9
|
+
3. **Two empty lines between functions** - Exactly two blank lines between top-level functions
|
|
10
10
|
4. **View function naming** - Functions in `views.py` with `request` parameter must end with `_view`
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
@@ -39,7 +39,7 @@ import ast
|
|
|
39
39
|
from python_style_checks import (
|
|
40
40
|
check_imports_at_top,
|
|
41
41
|
check_no_empty_line_after_decorators,
|
|
42
|
-
|
|
42
|
+
check_blank_lines_between_functions,
|
|
43
43
|
check_view_function_naming,
|
|
44
44
|
)
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ tree = ast.parse(source)
|
|
|
49
49
|
# Run individual checks
|
|
50
50
|
violations = check_imports_at_top(tree, "myfile.py")
|
|
51
51
|
violations = check_no_empty_line_after_decorators(source, "myfile.py")
|
|
52
|
-
violations =
|
|
52
|
+
violations = check_blank_lines_between_functions(source, "myfile.py")
|
|
53
53
|
violations = check_view_function_naming(tree, "views.py")
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -75,7 +75,7 @@ def foo() -> None:
|
|
|
75
75
|
"""Do something."""
|
|
76
76
|
pass
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
def bar() -> None:
|
|
80
80
|
"""Another function."""
|
|
81
81
|
pass
|
|
@@ -100,8 +100,7 @@ def bar() -> None: # VIOLATION: No empty line after decorator
|
|
|
100
100
|
def baz() -> None:
|
|
101
101
|
pass
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
def qux() -> None: # VIOLATION: Expected 1 empty line, found 2
|
|
103
|
+
def qux() -> None: # VIOLATION: Expected 2 empty lines, found 1
|
|
105
104
|
pass
|
|
106
105
|
|
|
107
106
|
# View naming (in views.py)
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from .python_style_checks import (
|
|
4
4
|
Violation,
|
|
5
|
+
check_blank_lines_between_functions,
|
|
5
6
|
check_imports_at_top,
|
|
6
7
|
check_no_empty_line_after_decorators,
|
|
7
|
-
check_single_empty_line_between_functions,
|
|
8
8
|
check_view_function_naming,
|
|
9
9
|
validate_file,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
"Violation",
|
|
14
|
+
"check_blank_lines_between_functions",
|
|
14
15
|
"check_imports_at_top",
|
|
15
16
|
"check_no_empty_line_after_decorators",
|
|
16
|
-
"check_single_empty_line_between_functions",
|
|
17
17
|
"check_view_function_naming",
|
|
18
18
|
"validate_file",
|
|
19
19
|
]
|