claude-dev-env 1.92.0 → 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 +5 -39
- 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 +7 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/rules/testing.md +0 -2
- 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 +184 -0
- package/skills/orchestrator-refresh/SKILL.md +37 -0
- 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 +32 -164
- 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
- package/skills/team-advisor-refresh/SKILL.md +0 -25
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Golden-differential and unit tests for the Bash/PowerShell PreToolUse dispatcher.
|
|
2
|
+
|
|
3
|
+
The golden-differential tests run a payload through every applicable hook as its
|
|
4
|
+
own subprocess (the standalone path), compute the precedence decision the chain
|
|
5
|
+
produced, then run the dispatcher on the same payload and assert an equal
|
|
6
|
+
decision. The unit tests pin the deny>ask>allow precedence, the es_exe
|
|
7
|
+
updatedInput passthrough, and the fail-open posture on a crashed hook.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
_HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
|
|
21
|
+
_BLOCKING_DIR = Path(__file__).resolve().parent
|
|
22
|
+
if _HOOKS_DIR not in sys.path:
|
|
23
|
+
sys.path.insert(0, _HOOKS_DIR)
|
|
24
|
+
_blocking_dir_text = str(_BLOCKING_DIR)
|
|
25
|
+
if _blocking_dir_text not in sys.path:
|
|
26
|
+
sys.path.insert(0, _blocking_dir_text)
|
|
27
|
+
|
|
28
|
+
from bash_pre_tool_use_dispatcher import ( # noqa: E402
|
|
29
|
+
BashDispatcherDecision,
|
|
30
|
+
_emit_decision,
|
|
31
|
+
aggregate_bash_hook_results,
|
|
32
|
+
dispatch,
|
|
33
|
+
select_applicable_entries,
|
|
34
|
+
)
|
|
35
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
|
|
36
|
+
BASH_TOOL_NAME,
|
|
37
|
+
POWERSHELL_TOOL_NAME,
|
|
38
|
+
)
|
|
39
|
+
from hooks_constants.hosted_hook_runner import HostedHookRun # noqa: E402
|
|
40
|
+
|
|
41
|
+
_HOOKS_ROOT = _BLOCKING_DIR.parent
|
|
42
|
+
_DISPATCHER_SCRIPT = str(_BLOCKING_DIR / "bash_pre_tool_use_dispatcher.py")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _bash_payload(command: str) -> str:
|
|
46
|
+
"""Build a Bash tool payload JSON string carrying command."""
|
|
47
|
+
return json.dumps({"tool_name": BASH_TOOL_NAME, "tool_input": {"command": command}})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _decision_from_stdout(stdout_text: str) -> tuple[str, str]:
|
|
51
|
+
"""Parse a hook or dispatcher stdout into (permissionDecision, reason)."""
|
|
52
|
+
stripped = stdout_text.strip()
|
|
53
|
+
if not stripped:
|
|
54
|
+
return "", ""
|
|
55
|
+
try:
|
|
56
|
+
parsed = json.loads(stripped)
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
return "", ""
|
|
59
|
+
hook_specific = parsed.get("hookSpecificOutput", {})
|
|
60
|
+
if not isinstance(hook_specific, dict):
|
|
61
|
+
return "", ""
|
|
62
|
+
decision = hook_specific.get("permissionDecision", "")
|
|
63
|
+
reason = hook_specific.get("permissionDecisionReason", "")
|
|
64
|
+
decision_text = decision if isinstance(decision, str) else ""
|
|
65
|
+
return decision_text, (reason if isinstance(reason, str) else "")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_process(script_or_hook_path: str, payload_text: str) -> subprocess.CompletedProcess[str]:
|
|
69
|
+
"""Run a hook or the dispatcher as a subprocess with the current environment."""
|
|
70
|
+
return subprocess.run(
|
|
71
|
+
[sys.executable, script_or_hook_path],
|
|
72
|
+
check=False,
|
|
73
|
+
input=payload_text,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _expected_precedence(payload_text: str, tool_name: str) -> str:
|
|
81
|
+
"""Return the decision the standalone chain yields for payload by precedence."""
|
|
82
|
+
all_decisions = []
|
|
83
|
+
for each_entry in select_applicable_entries(tool_name):
|
|
84
|
+
hook_path = str(_HOOKS_ROOT / each_entry.script_relative_path)
|
|
85
|
+
decision, _reason = _decision_from_stdout(_run_process(hook_path, payload_text).stdout)
|
|
86
|
+
all_decisions.append(decision)
|
|
87
|
+
for each_winning in ("deny", "ask", "allow"):
|
|
88
|
+
if each_winning in all_decisions:
|
|
89
|
+
return each_winning
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_clean_command_yields_no_decision_matching_the_standalone_chain() -> None:
|
|
94
|
+
"""A benign command produces no decision from the chain and from the dispatcher."""
|
|
95
|
+
payload_text = _bash_payload("echo hello world")
|
|
96
|
+
expected_decision = _expected_precedence(payload_text, BASH_TOOL_NAME)
|
|
97
|
+
dispatcher_decision, _reason = _decision_from_stdout(
|
|
98
|
+
_run_process(_DISPATCHER_SCRIPT, payload_text).stdout
|
|
99
|
+
)
|
|
100
|
+
assert dispatcher_decision == expected_decision
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_destructive_command_asks_matching_the_standalone_chain() -> None:
|
|
104
|
+
"""A destructive command the chain asks on makes the dispatcher ask too."""
|
|
105
|
+
payload_text = _bash_payload("rm -rf /")
|
|
106
|
+
expected_decision = _expected_precedence(payload_text, BASH_TOOL_NAME)
|
|
107
|
+
dispatcher_decision, _reason = _decision_from_stdout(
|
|
108
|
+
_run_process(_DISPATCHER_SCRIPT, payload_text).stdout
|
|
109
|
+
)
|
|
110
|
+
assert dispatcher_decision == expected_decision
|
|
111
|
+
assert expected_decision == "ask"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_dispatcher_exits_zero_on_a_clean_command() -> None:
|
|
115
|
+
"""The dispatcher exits zero for a command no hook decides on."""
|
|
116
|
+
completed = _run_process(_DISPATCHER_SCRIPT, _bash_payload("echo hi"))
|
|
117
|
+
assert completed.returncode == 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _run(stdout: str) -> HostedHookRun:
|
|
121
|
+
"""Build a HostedHookRun carrying stdout with no crash."""
|
|
122
|
+
return HostedHookRun(captured_stdout=stdout, did_crash=False)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _decision_json(decision: str, reason: str) -> str:
|
|
126
|
+
"""Build a hook stdout string carrying one permission decision."""
|
|
127
|
+
return json.dumps(
|
|
128
|
+
{"hookSpecificOutput": {"permissionDecision": decision, "permissionDecisionReason": reason}}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_deny_wins_over_ask_and_allow() -> None:
|
|
133
|
+
"""A deny from any hook overrides an ask and an allow from others."""
|
|
134
|
+
all_runs = [
|
|
135
|
+
_run(_decision_json("allow", "")),
|
|
136
|
+
_run(_decision_json("ask", "please confirm")),
|
|
137
|
+
_run(_decision_json("deny", "blocked hard")),
|
|
138
|
+
]
|
|
139
|
+
aggregated = aggregate_bash_hook_results(all_runs)
|
|
140
|
+
assert aggregated.decision == "deny"
|
|
141
|
+
assert "blocked hard" in aggregated.reasons
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_ask_wins_over_allow() -> None:
|
|
145
|
+
"""An ask overrides an allow when no hook denies."""
|
|
146
|
+
all_runs = [_run(_decision_json("allow", "")), _run(_decision_json("ask", "confirm this"))]
|
|
147
|
+
aggregated = aggregate_bash_hook_results(all_runs)
|
|
148
|
+
assert aggregated.decision == "ask"
|
|
149
|
+
assert "confirm this" in aggregated.reasons
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_allow_with_updated_input_is_carried_through() -> None:
|
|
153
|
+
"""An allow carrying updatedInput surfaces that rewrite when no hook denies or asks."""
|
|
154
|
+
rewritten = json.dumps(
|
|
155
|
+
{
|
|
156
|
+
"hookSpecificOutput": {
|
|
157
|
+
"permissionDecision": "allow",
|
|
158
|
+
"updatedInput": {"command": "es.exe rewritten"},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
aggregated = aggregate_bash_hook_results([_run(rewritten)])
|
|
163
|
+
assert aggregated.decision == "allow"
|
|
164
|
+
assert aggregated.updated_input == {"command": "es.exe rewritten"}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_no_decision_when_every_hook_is_silent() -> None:
|
|
168
|
+
"""A chain where every hook emits nothing produces no dispatcher decision."""
|
|
169
|
+
aggregated = aggregate_bash_hook_results([_run(""), _run("")])
|
|
170
|
+
assert aggregated.decision == ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_a_crashed_hook_contributes_no_decision() -> None:
|
|
174
|
+
"""A crashed hook fails open: its empty output adds no deny, ask, or allow."""
|
|
175
|
+
crashed_run = HostedHookRun(captured_stdout="", did_crash=True)
|
|
176
|
+
aggregated = aggregate_bash_hook_results([crashed_run])
|
|
177
|
+
assert aggregated.decision == ""
|
|
178
|
+
assert isinstance(aggregated, BashDispatcherDecision)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_deny_preserves_system_message_and_suppress_output() -> None:
|
|
182
|
+
"""A silent-deny shape carries systemMessage and suppressOutput through aggregation."""
|
|
183
|
+
silent_deny = json.dumps(
|
|
184
|
+
{
|
|
185
|
+
"hookSpecificOutput": {
|
|
186
|
+
"permissionDecision": "deny",
|
|
187
|
+
"permissionDecisionReason": "GH-REDIRECT GATE: duplicate blocked",
|
|
188
|
+
},
|
|
189
|
+
"suppressOutput": True,
|
|
190
|
+
"systemMessage": "[gh-gate] blocked redirected gh pr create",
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
aggregated = aggregate_bash_hook_results([_run(silent_deny)])
|
|
194
|
+
assert aggregated.decision == "deny"
|
|
195
|
+
assert aggregated.should_suppress_output is True
|
|
196
|
+
assert aggregated.all_system_messages == ["[gh-gate] blocked redirected gh pr create"]
|
|
197
|
+
assert "GH-REDIRECT GATE" in aggregated.reasons[0]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_additional_context_is_collected_from_deciding_hooks() -> None:
|
|
201
|
+
"""additionalContext from hosted hooks is retained for the emitted payload."""
|
|
202
|
+
deny_with_context = json.dumps(
|
|
203
|
+
{
|
|
204
|
+
"hookSpecificOutput": {
|
|
205
|
+
"permissionDecision": "deny",
|
|
206
|
+
"permissionDecisionReason": "blocked",
|
|
207
|
+
"additionalContext": "see docs/runbook.md",
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
aggregated = aggregate_bash_hook_results([_run(deny_with_context)])
|
|
212
|
+
assert aggregated.all_additional_context == ["see docs/runbook.md"]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_powershell_selects_the_three_shared_hooks() -> None:
|
|
216
|
+
"""Selecting for PowerShell yields the three shared hooks in registration order."""
|
|
217
|
+
powershell_paths = [
|
|
218
|
+
each_entry.script_relative_path
|
|
219
|
+
for each_entry in select_applicable_entries(POWERSHELL_TOOL_NAME)
|
|
220
|
+
]
|
|
221
|
+
assert powershell_paths == [
|
|
222
|
+
"blocking/pii_prevention_blocker.py",
|
|
223
|
+
"blocking/verified_commit_gate.py",
|
|
224
|
+
"blocking/verdict_directory_write_blocker.py",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_dispatcher_imports_standalone_with_only_blocking_on_the_path() -> None:
|
|
229
|
+
"""The dispatcher's bootstrap resolves hooks_constants without hooks/ on PYTHONPATH."""
|
|
230
|
+
subprocess_environment = {**os.environ, "PYTHONPATH": str(_BLOCKING_DIR)}
|
|
231
|
+
completed = subprocess.run(
|
|
232
|
+
[sys.executable, "-c", "import bash_pre_tool_use_dispatcher; print('ok')"],
|
|
233
|
+
check=False,
|
|
234
|
+
capture_output=True,
|
|
235
|
+
text=True,
|
|
236
|
+
env=subprocess_environment,
|
|
237
|
+
)
|
|
238
|
+
assert completed.returncode == 0, completed.stderr
|
|
239
|
+
assert completed.stdout.strip() == "ok"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_emit_decision_re_emits_silent_deny_fields(
|
|
243
|
+
capsys: pytest.CaptureFixture[str],
|
|
244
|
+
) -> None:
|
|
245
|
+
"""_emit_decision writes systemMessage, suppressOutput, and additionalContext."""
|
|
246
|
+
silent_deny = BashDispatcherDecision(
|
|
247
|
+
decision="deny",
|
|
248
|
+
reasons=["GH-REDIRECT GATE: duplicate blocked"],
|
|
249
|
+
all_system_messages=["[gh-gate] blocked redirected gh pr create"],
|
|
250
|
+
all_additional_context=["see docs/runbook.md"],
|
|
251
|
+
should_suppress_output=True,
|
|
252
|
+
)
|
|
253
|
+
_emit_decision(silent_deny)
|
|
254
|
+
captured = capsys.readouterr()
|
|
255
|
+
emitted_payload = json.loads(captured.out.strip())
|
|
256
|
+
hook_specific = emitted_payload["hookSpecificOutput"]
|
|
257
|
+
assert hook_specific["permissionDecision"] == "deny"
|
|
258
|
+
assert "GH-REDIRECT GATE" in hook_specific["permissionDecisionReason"]
|
|
259
|
+
assert hook_specific["additionalContext"] == "see docs/runbook.md"
|
|
260
|
+
assert emitted_payload["systemMessage"] == "[gh-gate] blocked redirected gh pr create"
|
|
261
|
+
assert emitted_payload["suppressOutput"] is True
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_dispatch_emits_deny_immediately_and_skips_later_hooks(
|
|
265
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
266
|
+
capsys: pytest.CaptureFixture[str],
|
|
267
|
+
) -> None:
|
|
268
|
+
"""A mid-chain deny emits at once and does not invoke later hosted hooks."""
|
|
269
|
+
all_call_paths: list[str] = []
|
|
270
|
+
|
|
271
|
+
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
272
|
+
del payload_text
|
|
273
|
+
all_call_paths.append(script_path)
|
|
274
|
+
script_name = Path(script_path).name
|
|
275
|
+
if script_name == "destructive_command_blocker.py":
|
|
276
|
+
return HostedHookRun(
|
|
277
|
+
captured_stdout=_decision_json("deny", "blocked early"),
|
|
278
|
+
did_crash=False,
|
|
279
|
+
)
|
|
280
|
+
if script_name == "precommit_code_rules_gate.py":
|
|
281
|
+
raise AssertionError("later hooks must not run after an early deny")
|
|
282
|
+
return HostedHookRun(captured_stdout="", did_crash=False)
|
|
283
|
+
|
|
284
|
+
monkeypatch.setattr(
|
|
285
|
+
"bash_pre_tool_use_dispatcher.run_hook_capturing_output",
|
|
286
|
+
_fake_run_hook,
|
|
287
|
+
)
|
|
288
|
+
dispatch(_bash_payload("rm -rf /"), BASH_TOOL_NAME)
|
|
289
|
+
captured = capsys.readouterr()
|
|
290
|
+
decision, reason = _decision_from_stdout(captured.out)
|
|
291
|
+
assert decision == "deny"
|
|
292
|
+
assert "blocked early" in reason
|
|
293
|
+
assert any(path.endswith("destructive_command_blocker.py") for path in all_call_paths)
|
|
294
|
+
assert not any(path.endswith("precommit_code_rules_gate.py") for path in all_call_paths)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tests for the cross-skill duplicate-helper advisory.
|
|
2
2
|
|
|
3
|
-
PR #
|
|
3
|
+
PR #101 on example-org/example-repo copied a Chrome-launch helper from the
|
|
4
4
|
``stp-recolor`` skill's ``scripts`` directory into the ``iconize-and-recolor-stp``
|
|
5
5
|
skill's ``scripts`` directory. The two skills install on their own, so a shared
|
|
6
6
|
module would couple them and break independent install; the copy is a defensible
|
|
@@ -364,6 +364,57 @@ def test_returns_empty_list_at_file_cap(
|
|
|
364
364
|
assert issues == [], f"File cap hit must return [] (cannot prove dead), got: {issues}"
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
def _write_noise_modules(directory: Path, count: int) -> None:
|
|
368
|
+
directory.mkdir(parents=True)
|
|
369
|
+
for each_index in range(count):
|
|
370
|
+
(directory / f"mod_{each_index}.py").write_text(
|
|
371
|
+
"def noop() -> int:\n return 1\n", encoding="utf-8"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_dead_exported_constant_flagged_despite_many_noncandidate_siblings(
|
|
376
|
+
neutral_root: Path, monkeypatch: pytest.MonkeyPatch
|
|
377
|
+
) -> None:
|
|
378
|
+
monkeypatch.setattr("code_rules_dead_module_constant.MAX_SCAN_ROOT_FILE_COUNT", 3)
|
|
379
|
+
package_directory = neutral_root / "pkg"
|
|
380
|
+
package_directory.mkdir(parents=True)
|
|
381
|
+
constants_body = 'DEAD_EXPORTED = "x"\n__all__ = ["DEAD_EXPORTED"]\n'
|
|
382
|
+
constants_path = package_directory / "settings_constants.py"
|
|
383
|
+
constants_path.write_text(constants_body, encoding="utf-8")
|
|
384
|
+
_write_noise_modules(neutral_root / "noise", 10)
|
|
385
|
+
issues = _check(constants_body, str(constants_path))
|
|
386
|
+
assert any("DEAD_EXPORTED" in each_issue for each_issue in issues), (
|
|
387
|
+
"A dead exported constant must stay flagged when the widened scan meets "
|
|
388
|
+
f"many sibling modules that never import from this module, got: {issues}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_live_qualified_importer_found_despite_many_noncandidate_siblings(
|
|
393
|
+
neutral_root: Path, monkeypatch: pytest.MonkeyPatch
|
|
394
|
+
) -> None:
|
|
395
|
+
monkeypatch.setattr("code_rules_dead_module_constant.MAX_SCAN_ROOT_FILE_COUNT", 3)
|
|
396
|
+
package_directory = neutral_root / "pkg"
|
|
397
|
+
package_directory.mkdir(parents=True)
|
|
398
|
+
constants_body = 'LIVE_EXPORTED = "y"\n__all__ = ["LIVE_EXPORTED"]\n'
|
|
399
|
+
constants_path = package_directory / "settings_constants.py"
|
|
400
|
+
constants_path.write_text(constants_body, encoding="utf-8")
|
|
401
|
+
_write_noise_modules(neutral_root / "noise", 10)
|
|
402
|
+
consumer_directory = neutral_root / "app"
|
|
403
|
+
consumer_directory.mkdir(parents=True)
|
|
404
|
+
consumer_body = (
|
|
405
|
+
"from pkg.settings_constants import LIVE_EXPORTED\n"
|
|
406
|
+
"\n"
|
|
407
|
+
"def use() -> str:\n"
|
|
408
|
+
" return LIVE_EXPORTED\n"
|
|
409
|
+
)
|
|
410
|
+
(consumer_directory / "consumer.py").write_text(consumer_body, encoding="utf-8")
|
|
411
|
+
issues = _check(constants_body, str(constants_path))
|
|
412
|
+
assert issues == [], (
|
|
413
|
+
"A constant imported through a qualified from-import must stay live even "
|
|
414
|
+
f"when many non-importing siblings surround it, got: {issues}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
367
418
|
def test_widened_scan_reads_each_file_at_most_once(
|
|
368
419
|
neutral_root: Path, monkeypatch: pytest.MonkeyPatch
|
|
369
420
|
) -> None:
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
14
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
15
|
+
assert specification is not None and specification.loader is not None
|
|
16
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
17
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _strip_read_only_and_retry(
|
|
21
|
+
removal_function: Callable[[str], object],
|
|
22
|
+
target_path: str,
|
|
23
|
+
_exc_info: BaseException,
|
|
24
|
+
) -> None:
|
|
25
|
+
try:
|
|
26
|
+
os.chmod(target_path, stat.S_IWRITE)
|
|
27
|
+
removal_function(target_path)
|
|
28
|
+
except OSError:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def neutral_root() -> Iterator[Path]:
|
|
34
|
+
"""Yield a temp directory whose path carries no ``test_`` segment.
|
|
35
|
+
|
|
36
|
+
::
|
|
37
|
+
|
|
38
|
+
tmp_path (pytest default) -> embeds "test_..." in its name
|
|
39
|
+
-> is_test_file() misreads every
|
|
40
|
+
synthetic constants path
|
|
41
|
+
mkdtemp(prefix="deadconst-") -> no "test_" segment
|
|
42
|
+
-> reads as a production path ok
|
|
43
|
+
|
|
44
|
+
A ``.git`` marker is planted at the root so the cross-tree widening
|
|
45
|
+
resolves the repository root to this synthetic tree.
|
|
46
|
+
"""
|
|
47
|
+
neutral_directory = Path(tempfile.mkdtemp(prefix="deadconst-")).resolve()
|
|
48
|
+
(neutral_directory / ".git").mkdir()
|
|
49
|
+
try:
|
|
50
|
+
yield neutral_directory
|
|
51
|
+
finally:
|
|
52
|
+
shutil.rmtree(neutral_directory, onexc=_strip_read_only_and_retry)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _check(source: str, file_path: str) -> list[str]:
|
|
56
|
+
return code_rules_enforcer.check_dead_module_constants(source, file_path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _write_noise_modules(directory: Path, count: int) -> None:
|
|
60
|
+
directory.mkdir(parents=True)
|
|
61
|
+
for each_index in range(count):
|
|
62
|
+
(directory / f"mod_{each_index}.py").write_text(
|
|
63
|
+
"def noop() -> int:\n return 1\n", encoding="utf-8"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_widened_scan_bounds_total_disk_reads_on_noncandidate_flood(
|
|
68
|
+
neutral_root: Path, monkeypatch: pytest.MonkeyPatch
|
|
69
|
+
) -> None:
|
|
70
|
+
"""A read-attempt cap stops the widened pass from reading every sibling.
|
|
71
|
+
|
|
72
|
+
::
|
|
73
|
+
|
|
74
|
+
MAX_SCAN_ROOT_READ_COUNT = 3, 50 non-candidate noise files
|
|
75
|
+
ok: read_text() called a bounded number of times (< 50)
|
|
76
|
+
flag: read_text() called once per noise file (== 50)
|
|
77
|
+
|
|
78
|
+
Without the cap, ``_read_candidate_source`` opens and reads every
|
|
79
|
+
repository ``.py`` file's full text before testing it for the module
|
|
80
|
+
stem, so disk-read cost scales with repository size regardless of how
|
|
81
|
+
quickly the file cap would otherwise stop the parse-and-collect work.
|
|
82
|
+
"""
|
|
83
|
+
monkeypatch.setattr("code_rules_dead_module_constant.MAX_SCAN_ROOT_READ_COUNT", 3)
|
|
84
|
+
package_directory = neutral_root / "pkg"
|
|
85
|
+
package_directory.mkdir(parents=True)
|
|
86
|
+
constants_body = 'DEAD_EXPORTED = "x"\n__all__ = ["DEAD_EXPORTED"]\n'
|
|
87
|
+
constants_path = package_directory / "settings_constants.py"
|
|
88
|
+
constants_path.write_text(constants_body, encoding="utf-8")
|
|
89
|
+
_write_noise_modules(neutral_root / "noise", 50)
|
|
90
|
+
read_count = 0
|
|
91
|
+
original_read_text = Path.read_text
|
|
92
|
+
|
|
93
|
+
def counting_read_text(self: Path, *positional: object, **keyword: object) -> str:
|
|
94
|
+
nonlocal read_count
|
|
95
|
+
read_count += 1
|
|
96
|
+
return original_read_text(self, *positional, **keyword) # type: ignore[arg-type] # forwards positional/keyword args mypy cannot resolve
|
|
97
|
+
|
|
98
|
+
monkeypatch.setattr(Path, "read_text", counting_read_text)
|
|
99
|
+
_check(constants_body, str(constants_path))
|
|
100
|
+
assert read_count < 50, (
|
|
101
|
+
"A read-count cap must stop the widened pass from reading every "
|
|
102
|
+
f"non-candidate sibling's full text, got {read_count} reads across 50 noise files"
|
|
103
|
+
)
|
|
@@ -47,7 +47,7 @@ def test_is_hook_infrastructure_should_recognize_packages_claude_dev_env_hooks_v
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def test_is_hook_infrastructure_should_still_recognize_dot_claude_hooks():
|
|
50
|
-
assert enforcer.is_hook_infrastructure("C:/Users/
|
|
50
|
+
assert enforcer.is_hook_infrastructure("C:/Users/example/.claude/hooks/blocking/foo.py") is True
|
|
51
51
|
assert enforcer.is_hook_infrastructure("/home/user/.claude/hooks/blocking/foo.py") is True
|
|
52
52
|
|
|
53
53
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for file-global constants use-count rule (jl-cmd/claude-
|
|
1
|
+
"""Tests for file-global constants use-count rule (jl-cmd/claude-dev-env#180).
|
|
2
2
|
|
|
3
3
|
A module-level UPPER_SNAKE constant must be referenced by at least two
|
|
4
4
|
distinct functions/methods. A constant referenced by only one function
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tests for hardcoded user path detection.
|
|
2
2
|
|
|
3
|
-
Bot reviewers on PR #257 flagged 6+ instances of 'C:/Users/
|
|
3
|
+
Bot reviewers on PR #257 flagged 6+ instances of 'C:/Users/example/' embedded
|
|
4
4
|
in production source code, which breaks portability across machines.
|
|
5
5
|
The new rule flags any string literal in production code that names a
|
|
6
6
|
specific user's home directory.
|
|
@@ -35,24 +35,24 @@ HOOK_INFRASTRUCTURE_FILE_PATH = "/repo/packages/claude-dev-env/hooks/blocking/co
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def test_should_match_user_directory_without_consuming_following_separator() -> None:
|
|
38
|
-
windows = HARDCODED_USER_PATH_PATTERN.search("C:/Users/
|
|
38
|
+
windows = HARDCODED_USER_PATH_PATTERN.search("C:/Users/example/more")
|
|
39
39
|
macos = HARDCODED_USER_PATH_PATTERN.search("/Users/bob/more")
|
|
40
40
|
linux = HARDCODED_USER_PATH_PATTERN.search("/home/alice/more")
|
|
41
|
-
assert windows is not None and windows.group(0) == "C:/Users/
|
|
41
|
+
assert windows is not None and windows.group(0) == "C:/Users/example"
|
|
42
42
|
assert macos is not None and macos.group(0) == "/Users/bob"
|
|
43
43
|
assert linux is not None and linux.group(0) == "/home/alice"
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def test_should_flag_windows_user_path_with_forward_slashes() -> None:
|
|
47
|
-
source = 'def find() -> str:\n return "C:/Users/
|
|
47
|
+
source = 'def find() -> str:\n return "C:/Users/example/notes.md"\n'
|
|
48
48
|
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
49
|
-
assert any("C:/Users/
|
|
49
|
+
assert any("C:/Users/example" in each_issue for each_issue in issues), (
|
|
50
50
|
f"Expected Windows user path flagged, got: {issues}"
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def test_should_flag_windows_user_path_when_users_segment_is_not_title_case() -> None:
|
|
55
|
-
source = 'def find() -> str:\n return "c:/users/
|
|
55
|
+
source = 'def find() -> str:\n return "c:/users/example/notes.md"\n'
|
|
56
56
|
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
57
57
|
assert any("users" in each_issue.lower() for each_issue in issues), (
|
|
58
58
|
f"Expected Windows user path flagged (case-insensitive Users segment), got: {issues}"
|
|
@@ -60,7 +60,7 @@ def test_should_flag_windows_user_path_when_users_segment_is_not_title_case() ->
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def test_should_flag_windows_user_path_with_backslashes() -> None:
|
|
63
|
-
source = 'def find() -> str:\n return "C:\\\\Users\\\\
|
|
63
|
+
source = 'def find() -> str:\n return "C:\\\\Users\\\\example\\\\notes.md"\n'
|
|
64
64
|
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
65
65
|
assert any("Users" in each_issue for each_issue in issues), (
|
|
66
66
|
f"Expected Windows backslash user path flagged, got: {issues}"
|
|
@@ -106,7 +106,7 @@ def test_should_not_flag_users_directory_without_specific_user() -> None:
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def test_should_skip_test_files() -> None:
|
|
109
|
-
source = 'def test_path() -> None:\n fixture = "C:/Users/
|
|
109
|
+
source = 'def test_path() -> None:\n fixture = "C:/Users/example/scratch.txt"\n'
|
|
110
110
|
issues = check_hardcoded_user_paths(source, TEST_FILE_PATH)
|
|
111
111
|
assert issues == [], (
|
|
112
112
|
f"Test files exempt — fixtures often need real paths, got: {issues}"
|
|
@@ -114,7 +114,7 @@ def test_should_skip_test_files() -> None:
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def test_should_skip_config_files() -> None:
|
|
117
|
-
source = 'DEFAULT_PATH = "C:/Users/
|
|
117
|
+
source = 'DEFAULT_PATH = "C:/Users/example/notes.md"\n'
|
|
118
118
|
issues = check_hardcoded_user_paths(source, CONFIG_FILE_PATH)
|
|
119
119
|
assert issues == [], (
|
|
120
120
|
f"Config files exempt — that is the right place for paths, got: {issues}"
|
|
@@ -122,7 +122,7 @@ def test_should_skip_config_files() -> None:
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def test_should_include_line_number_in_issue() -> None:
|
|
125
|
-
source = '\n\ndef find() -> str:\n return "C:/Users/
|
|
125
|
+
source = '\n\ndef find() -> str:\n return "C:/Users/example/notes.md"\n'
|
|
126
126
|
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
127
127
|
assert any("Line 4" in each_issue for each_issue in issues), (
|
|
128
128
|
f"Expected line 4 reference, got: {issues}"
|
|
@@ -165,7 +165,7 @@ def test_should_skip_hook_infrastructure_files() -> None:
|
|
|
165
165
|
source = (
|
|
166
166
|
'HARDCODED_USER_PATH_PATTERN = "/Users/[^/]+|/home/[^/]+"\n'
|
|
167
167
|
'def find() -> str:\n'
|
|
168
|
-
' return "C:/Users/
|
|
168
|
+
' return "C:/Users/example/notes.md"\n'
|
|
169
169
|
)
|
|
170
170
|
issues = check_hardcoded_user_paths(source, HOOK_INFRASTRUCTURE_FILE_PATH)
|
|
171
171
|
assert issues == [], (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Tests for the naive-datetime-construction check in code_rules_imports_logging.py.
|
|
2
2
|
|
|
3
3
|
The check catches the DST-fold-ambiguity class Copilot flagged on
|
|
4
|
-
|
|
4
|
+
example-repo PR #101: ``datetime.fromtimestamp(epoch)`` builds a datetime
|
|
5
5
|
with no timezone, and a later ``.astimezone()`` on that naive value has to guess
|
|
6
6
|
the local offset. The check flags the naive-producing constructors
|
|
7
7
|
(``fromtimestamp``/``utcfromtimestamp`` without ``tz=``, and ``utcnow``) and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tests for same-file inline duplicate function-body detection.
|
|
2
2
|
|
|
3
|
-
PR #
|
|
3
|
+
PR #101 (example-org/example-repo) added ``_wait_for_content_image_to_render``,
|
|
4
4
|
a top-level async helper whose body is byte-for-structure identical to a
|
|
5
5
|
content-image-wait block already inlined inside ``_wait_for_page_after_account_switch``
|
|
6
6
|
in the same module. The cross-file duplicate-body check never compares two
|