claude-dev-env 1.38.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +10 -36
- package/_shared/pr-loop/audit-reply-template.md +147 -0
- package/_shared/pr-loop/fix-protocol.md +25 -4
- package/_shared/pr-loop/gh-payloads.md +37 -50
- package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
- package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +189 -0
- package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
- package/_shared/pr-loop/state-schema.md +1 -1
- package/agents/clean-coder.md +2 -2
- package/bin/install.mjs +6 -7
- package/bin/install.test.mjs +8 -0
- package/commands/doc-gist.md +16 -0
- package/commands/plan.md +0 -2
- package/commands/review-plan.md +1 -1
- package/docs/CODE_RULES.md +122 -2
- package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
- package/hooks/blocking/code_rules_enforcer.py +1236 -161
- package/hooks/blocking/convergence_gate_blocker.py +130 -0
- package/hooks/blocking/destructive_command_blocker.py +74 -0
- package/hooks/blocking/gh_body_arg_blocker.py +30 -0
- package/hooks/blocking/md_to_html_blocker.py +119 -0
- package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
- package/hooks/blocking/test_code_rules_enforcer.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
- package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
- package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
- package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
- package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
- package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
- package/hooks/blocking/test_destructive_command_blocker.py +146 -0
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
- package/hooks/blocking/test_md_to_html_blocker.py +317 -0
- package/hooks/config/any_type_config.py +7 -0
- package/hooks/config/banned_identifiers_constants.py +11 -0
- package/hooks/config/blocking_check_limits.py +38 -0
- package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
- package/hooks/config/code_rules_enforcer_constants.py +53 -0
- package/hooks/config/convergence_branch_constants.py +9 -0
- package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
- package/hooks/config/html_companion_constants.py +20 -0
- package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
- package/hooks/config/test_banned_identifiers_constants.py +17 -0
- package/hooks/hooks.json +28 -20
- package/hooks/pyproject.toml +69 -0
- package/hooks/validators/mypy_integration.py +47 -1
- package/hooks/validators/run_all_validators.py +3 -3
- package/hooks/validators/test_mypy_integration.py +50 -1
- package/hooks/workflow/doc_gist_auto_publish.py +144 -0
- package/hooks/workflow/md_to_html_companion.py +365 -0
- package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
- package/hooks/workflow/test_md_to_html_companion.py +452 -0
- package/package.json +1 -1
- package/rules/gh-body-file.md +2 -0
- package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
- package/scripts/check.ps1 +106 -0
- package/scripts/config/timing.py +11 -0
- package/scripts/sweep_empty_dirs.py +138 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_sweep_empty_dirs.py +183 -0
- package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
- package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
- package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
- package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
- package/skills/bugteam/CONSTRAINTS.md +21 -22
- package/skills/bugteam/EXAMPLES.md +3 -3
- package/skills/bugteam/PROMPTS.md +227 -67
- package/skills/bugteam/SKILL.md +114 -455
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +112 -39
- package/skills/bugteam/reference/audit-contract.md +4 -22
- package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
- package/skills/bugteam/reference/design-rationale.md +2 -2
- package/skills/bugteam/reference/github-pr-reviews.md +50 -57
- package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
- package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
- package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
- package/skills/bugteam/reference/team-setup.md +106 -9
- package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
- package/skills/bugteam/scripts/README.md +60 -0
- package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
- package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
- package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
- package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
- package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
- package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
- package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
- package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
- package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
- package/skills/bugteam/test_skill_additions.py +1 -11
- package/skills/code/SKILL.md +176 -0
- package/skills/doc-gist/SKILL.md +99 -0
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
- package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
- package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
- package/skills/doc-gist/references/examples/05-design-system.html +629 -0
- package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
- package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
- package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
- package/skills/doc-gist/references/examples/11-status-report.html +528 -0
- package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
- package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
- package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
- package/skills/doc-gist/references/examples/README.md +5 -0
- package/skills/doc-gist/scripts/config/__init__.py +0 -0
- package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
- package/skills/doc-gist/scripts/gist_upload.py +177 -0
- package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
- package/skills/findbugs/SKILL.md +68 -2
- package/skills/monitor-open-prs/SKILL.md +13 -32
- package/skills/monitor-open-prs/test_skill_contract.py +0 -11
- package/skills/pr-consistency-audit/SKILL.md +112 -0
- package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
- package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
- package/skills/pr-converge/SKILL.md +227 -23
- package/skills/pr-converge/config/__init__.py +0 -0
- package/skills/pr-converge/config/constants.py +62 -0
- package/skills/pr-converge/reference/convergence-gates.md +138 -44
- package/skills/pr-converge/reference/examples.md +43 -11
- package/skills/pr-converge/reference/fix-protocol.md +6 -5
- package/skills/pr-converge/reference/ground-rules.md +5 -3
- package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
- package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
- package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
- package/skills/pr-converge/reference/per-tick.md +90 -31
- package/skills/pr-converge/reference/state-schema.md +22 -1
- package/skills/pr-converge/reference/stop-conditions.md +9 -7
- package/skills/pr-converge/scripts/README.md +34 -46
- package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
- package/skills/pr-converge/scripts/check_convergence.py +497 -0
- package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
- package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
- package/skills/qbug/SKILL.md +132 -27
- package/skills/session-log/SKILL.md +216 -114
- package/skills/session-tidy/SKILL.md +1 -1
- package/skills/skill-builder/SKILL.md +138 -56
- package/skills/skill-builder/references/delegation-map.md +72 -113
- package/skills/skill-builder/references/progressive-disclosure.md +122 -0
- package/skills/skill-builder/references/self-audit-checklist.md +92 -0
- package/skills/skill-builder/references/skill-types.md +228 -0
- package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
- package/skills/skill-builder/templates/gap-analysis.md +15 -8
- package/skills/skill-builder/workflows/improve-skill.md +86 -57
- package/skills/skill-builder/workflows/new-skill.md +80 -168
- package/skills/skill-builder/workflows/polish-skill.md +78 -54
- package/skills/structure-prompt/SKILL.md +50 -0
- package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
- package/skills/structure-prompt/reference/block-classification.md +27 -0
- package/skills/structure-prompt/reference/canonical-case.md +48 -0
- package/skills/structure-prompt/reference/citation-depth.md +70 -0
- package/skills/structure-prompt/reference/cleanup.md +33 -0
- package/skills/structure-prompt/reference/constraints.md +33 -0
- package/skills/structure-prompt/reference/directives.md +37 -0
- package/skills/structure-prompt/reference/examples.md +72 -0
- package/skills/structure-prompt/reference/instantiation.md +51 -0
- package/skills/structure-prompt/reference/output-contract.md +72 -0
- package/skills/structure-prompt/reference/per-category.md +23 -0
- package/skills/structure-prompt/reference/persona.md +38 -0
- package/skills/structure-prompt/reference/research.md +33 -0
- package/skills/structure-prompt/reference/structure.md +28 -0
- package/agents/code-standards-agent.md +0 -93
- package/agents/groq-coder.md +0 -113
- package/agents/plan-executor.md +0 -226
- package/agents/project-docs-analyzer.md +0 -53
- package/agents/project-structure-organizer-agent.md +0 -72
- package/agents/skill-to-agent-converter.md +0 -370
- package/agents/skill-writer-agent.md +0 -470
- package/agents/user-docs-writer.md +0 -67
- package/agents/workflow-visual-documenter.md +0 -82
- package/commands/readability-review.md +0 -20
- package/hooks/mypy.ini +0 -2
- package/hooks/notification/attention_needed_notify.py +0 -71
- package/hooks/notification/claude_notification_handler.py +0 -67
- package/hooks/notification/notification_utils.py +0 -267
- package/hooks/notification/subagent_complete_notify.py +0 -381
- package/hooks/notification/test_attention_needed_notify.py +0 -47
- package/hooks/notification/test_claude_notification_handler.py +0 -54
- package/hooks/notification/test_notification_utils.py +0 -91
- package/hooks/notification/test_subagent_complete_notify.py +0 -79
- package/scripts/config/groq_bugteam_config.py +0 -230
- package/scripts/config/test_groq_bugteam_config.py +0 -83
- package/scripts/config/test_spec_implementer_prompt.py +0 -32
- package/scripts/groq_bugteam.README.md +0 -131
- package/scripts/groq_bugteam.py +0 -647
- package/scripts/groq_bugteam_dotenv.py +0 -40
- package/scripts/groq_bugteam_spec.py +0 -226
- package/scripts/test_groq_bugteam.py +0 -529
- package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
- package/scripts/test_groq_bugteam_dotenv.py +0 -66
- package/scripts/test_groq_bugteam_spec.py +0 -338
- package/skills/bugteam/SKILL_EVALS.md +0 -309
- package/skills/dream/SKILL.md +0 -118
- package/skills/ingest/SKILL.md +0 -40
- package/skills/npm-creator/SKILL.md +0 -187
- package/skills/readability-review/SKILL.md +0 -127
- package/skills/resume-review/SKILL.md +0 -261
- package/skills/rule-audit/SKILL.md +0 -307
- package/skills/rule-creator/SKILL.md +0 -150
- package/skills/searching-obsidian-vault/SKILL.md +0 -131
- package/skills/skill-writer/REFERENCE.md +0 -284
- package/skills/skill-writer/SKILL.md +0 -222
- package/skills/tdd-team/SKILL.md +0 -128
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Tests for bugteam_fix_hookspath auto-remediation.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- removes a local-scope core.hooksPath override and re-runs preflight
|
|
5
|
+
- sets global core.hooksPath when missing
|
|
6
|
+
- idempotent: second invocation produces the same final state with no errors
|
|
7
|
+
- no-op when no override exists and global is already canonical
|
|
8
|
+
- exits non-zero with a clear message when canonical hooks dir is missing
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from unittest.mock import patch
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_fix_module() -> ModuleType:
|
|
25
|
+
module_path = Path(__file__).parent / "bugteam_fix_hookspath.py"
|
|
26
|
+
spec = importlib.util.spec_from_file_location("bugteam_fix_hookspath", module_path)
|
|
27
|
+
assert spec is not None
|
|
28
|
+
assert spec.loader is not None
|
|
29
|
+
module = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(module)
|
|
31
|
+
return module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
bugteam_fix_hookspath = _load_fix_module()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _make_isolated_git_environment(home_directory: Path) -> dict[str, str]:
|
|
38
|
+
"""Build an env dict that pins git's HOME and XDG paths into a tmp directory.
|
|
39
|
+
|
|
40
|
+
Without this, real `git config --global` reads/writes hit the developer's
|
|
41
|
+
actual ~/.gitconfig — which would corrupt the host machine and make tests
|
|
42
|
+
depend on global state. Pointing HOME, USERPROFILE, and XDG_CONFIG_HOME
|
|
43
|
+
at a temp directory isolates the test fully on every supported git
|
|
44
|
+
version. GIT_CONFIG_GLOBAL would tighten the binding but requires
|
|
45
|
+
git >= 2.32 (August 2021); HOME/USERPROFILE already isolate on older git.
|
|
46
|
+
"""
|
|
47
|
+
isolated_environment = os.environ.copy()
|
|
48
|
+
isolated_environment["HOME"] = str(home_directory)
|
|
49
|
+
isolated_environment["USERPROFILE"] = str(home_directory)
|
|
50
|
+
isolated_environment["XDG_CONFIG_HOME"] = str(home_directory / ".config")
|
|
51
|
+
isolated_environment["GIT_CONFIG_NOSYSTEM"] = "1"
|
|
52
|
+
return isolated_environment
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _initialize_repository(repository_path: Path, environment: dict[str, str]) -> None:
|
|
56
|
+
repository_path.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
subprocess.run(
|
|
58
|
+
["git", "init", "--quiet", str(repository_path)],
|
|
59
|
+
check=True,
|
|
60
|
+
env=environment,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _set_local_hooks_path(
|
|
65
|
+
repository_path: Path,
|
|
66
|
+
hooks_path_value: str,
|
|
67
|
+
environment: dict[str, str],
|
|
68
|
+
) -> None:
|
|
69
|
+
subprocess.run(
|
|
70
|
+
[
|
|
71
|
+
"git",
|
|
72
|
+
"-C",
|
|
73
|
+
str(repository_path),
|
|
74
|
+
"config",
|
|
75
|
+
"--local",
|
|
76
|
+
"core.hooksPath",
|
|
77
|
+
hooks_path_value,
|
|
78
|
+
],
|
|
79
|
+
check=True,
|
|
80
|
+
env=environment,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _set_global_hooks_path(hooks_path_value: str, environment: dict[str, str]) -> None:
|
|
85
|
+
subprocess.run(
|
|
86
|
+
["git", "config", "--global", "core.hooksPath", hooks_path_value],
|
|
87
|
+
check=True,
|
|
88
|
+
env=environment,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _read_local_hooks_path(repository_path: Path, environment: dict[str, str]) -> str:
|
|
93
|
+
completed_process = subprocess.run(
|
|
94
|
+
[
|
|
95
|
+
"git",
|
|
96
|
+
"-C",
|
|
97
|
+
str(repository_path),
|
|
98
|
+
"config",
|
|
99
|
+
"--local",
|
|
100
|
+
"--get",
|
|
101
|
+
"core.hooksPath",
|
|
102
|
+
],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
check=False,
|
|
106
|
+
env=environment,
|
|
107
|
+
)
|
|
108
|
+
return completed_process.stdout.strip()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _read_global_hooks_path(environment: dict[str, str]) -> str:
|
|
112
|
+
completed_process = subprocess.run(
|
|
113
|
+
["git", "config", "--global", "--get", "core.hooksPath"],
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
check=False,
|
|
117
|
+
env=environment,
|
|
118
|
+
)
|
|
119
|
+
return completed_process.stdout.strip()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _create_canonical_hooks_directory(home_directory: Path) -> Path:
|
|
123
|
+
canonical_hooks_directory = home_directory / ".claude" / "hooks" / "git-hooks"
|
|
124
|
+
canonical_hooks_directory.mkdir(parents=True)
|
|
125
|
+
return canonical_hooks_directory
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_should_remove_local_override_and_pass_preflight(tmp_path: Path) -> None:
|
|
129
|
+
home_directory = tmp_path / "home"
|
|
130
|
+
home_directory.mkdir()
|
|
131
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
132
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
133
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
134
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
135
|
+
_initialize_repository(repository_path, environment)
|
|
136
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
137
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
138
|
+
|
|
139
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
140
|
+
["--repo-root", str(repository_path)],
|
|
141
|
+
all_environment_overrides=environment,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
assert exit_code == 0, (
|
|
145
|
+
"fix script must succeed when canonical global hooks dir exists"
|
|
146
|
+
)
|
|
147
|
+
assert _read_local_hooks_path(repository_path, environment) == "", (
|
|
148
|
+
"local core.hooksPath override must be removed"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_should_set_global_hooks_path_when_missing(tmp_path: Path) -> None:
|
|
153
|
+
home_directory = tmp_path / "home"
|
|
154
|
+
home_directory.mkdir()
|
|
155
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
156
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
157
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
158
|
+
_initialize_repository(repository_path, environment)
|
|
159
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
160
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
161
|
+
|
|
162
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
163
|
+
["--repo-root", str(repository_path)],
|
|
164
|
+
all_environment_overrides=environment,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert exit_code == 0
|
|
168
|
+
global_value_after_fix = _read_global_hooks_path(environment)
|
|
169
|
+
assert (
|
|
170
|
+
global_value_after_fix.replace("\\", "/")
|
|
171
|
+
.rstrip("/")
|
|
172
|
+
.endswith("hooks/git-hooks")
|
|
173
|
+
), (
|
|
174
|
+
"fix script must set canonical global core.hooksPath when missing; "
|
|
175
|
+
f"got '{global_value_after_fix}'"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_should_be_idempotent(tmp_path: Path) -> None:
|
|
180
|
+
home_directory = tmp_path / "home"
|
|
181
|
+
home_directory.mkdir()
|
|
182
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
183
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
184
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
185
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
186
|
+
_initialize_repository(repository_path, environment)
|
|
187
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
188
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
189
|
+
|
|
190
|
+
first_exit_code = bugteam_fix_hookspath.main(
|
|
191
|
+
["--repo-root", str(repository_path)],
|
|
192
|
+
all_environment_overrides=environment,
|
|
193
|
+
)
|
|
194
|
+
second_exit_code = bugteam_fix_hookspath.main(
|
|
195
|
+
["--repo-root", str(repository_path)],
|
|
196
|
+
all_environment_overrides=environment,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
assert first_exit_code == 0
|
|
200
|
+
assert second_exit_code == 0, "second invocation must succeed without errors"
|
|
201
|
+
assert _read_local_hooks_path(repository_path, environment) == ""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_should_no_op_when_already_clean(tmp_path: Path) -> None:
|
|
205
|
+
home_directory = tmp_path / "home"
|
|
206
|
+
home_directory.mkdir()
|
|
207
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
208
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
209
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
210
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
211
|
+
_initialize_repository(repository_path, environment)
|
|
212
|
+
|
|
213
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
214
|
+
["--repo-root", str(repository_path)],
|
|
215
|
+
all_environment_overrides=environment,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
assert exit_code == 0
|
|
219
|
+
assert _read_local_hooks_path(repository_path, environment) == ""
|
|
220
|
+
assert (
|
|
221
|
+
_read_global_hooks_path(environment)
|
|
222
|
+
.replace("\\", "/")
|
|
223
|
+
.rstrip("/")
|
|
224
|
+
.endswith("hooks/git-hooks")
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_should_exit_nonzero_when_canonical_hooks_directory_missing(
|
|
229
|
+
tmp_path: Path,
|
|
230
|
+
capsys: pytest.CaptureFixture[str],
|
|
231
|
+
) -> None:
|
|
232
|
+
home_directory = tmp_path / "home"
|
|
233
|
+
home_directory.mkdir()
|
|
234
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
235
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
236
|
+
_initialize_repository(repository_path, environment)
|
|
237
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
238
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
239
|
+
|
|
240
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
241
|
+
["--repo-root", str(repository_path)],
|
|
242
|
+
all_environment_overrides=environment,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert exit_code != 0, (
|
|
246
|
+
"fix script must fail clearly when ~/.claude/hooks/git-hooks does not exist "
|
|
247
|
+
"so the user knows to run `npx claude-dev-env .`"
|
|
248
|
+
)
|
|
249
|
+
captured_streams = capsys.readouterr()
|
|
250
|
+
assert "hooks/git-hooks" in captured_streams.err.replace("\\", "/")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_should_handle_paths_with_spaces(tmp_path: Path) -> None:
|
|
254
|
+
home_directory = tmp_path / "home with space"
|
|
255
|
+
home_directory.mkdir()
|
|
256
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
257
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
258
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
259
|
+
repository_path = tmp_path / "repo with space"
|
|
260
|
+
_initialize_repository(repository_path, environment)
|
|
261
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
262
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
263
|
+
|
|
264
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
265
|
+
["--repo-root", str(repository_path)],
|
|
266
|
+
all_environment_overrides=environment,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
assert exit_code == 0
|
|
270
|
+
assert _read_local_hooks_path(repository_path, environment) == ""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_list_local_core_hooks_path_values_raises_on_unexpected_git_failure(
|
|
274
|
+
tmp_path: Path,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""A non-empty stderr from git config must propagate as an error.
|
|
277
|
+
|
|
278
|
+
Regression for loop1-5: returning [] on every non-zero git exit collapses
|
|
279
|
+
"key unset" with "git failed for some other reason" — the caller then
|
|
280
|
+
skips the unset call, leaving a stale local override in place.
|
|
281
|
+
"""
|
|
282
|
+
repository_path = tmp_path / "repo"
|
|
283
|
+
repository_path.mkdir()
|
|
284
|
+
|
|
285
|
+
def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
|
|
286
|
+
return subprocess.CompletedProcess(
|
|
287
|
+
args=[],
|
|
288
|
+
returncode=128,
|
|
289
|
+
stdout="",
|
|
290
|
+
stderr="fatal: unable to read config file: permission denied\n",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
with patch.object(subprocess, "run", fake_run):
|
|
294
|
+
with pytest.raises(RuntimeError):
|
|
295
|
+
bugteam_fix_hookspath.list_local_core_hooks_path_values(
|
|
296
|
+
repository_path,
|
|
297
|
+
None,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_read_global_core_hooks_path_raises_on_unexpected_git_failure() -> None:
|
|
302
|
+
"""A non-empty stderr from git config must propagate as an error.
|
|
303
|
+
|
|
304
|
+
Regression for loop1-6: returning "" on every non-zero git exit conflates
|
|
305
|
+
"global hooksPath unset" with "git failed for some other reason" — the
|
|
306
|
+
caller then overwrites global git config based on a non-truthful read.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
|
|
310
|
+
return subprocess.CompletedProcess(
|
|
311
|
+
args=[],
|
|
312
|
+
returncode=128,
|
|
313
|
+
stdout="",
|
|
314
|
+
stderr="fatal: bad config line\n",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
with patch.object(subprocess, "run", fake_run):
|
|
318
|
+
with pytest.raises(RuntimeError):
|
|
319
|
+
bugteam_fix_hookspath.read_global_core_hooks_path(None)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_list_local_core_hooks_path_values_returns_empty_when_key_unset(
|
|
323
|
+
tmp_path: Path,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Genuine key-unset (exit 1 + empty stderr) must continue to return []."""
|
|
326
|
+
repository_path = tmp_path / "repo"
|
|
327
|
+
repository_path.mkdir()
|
|
328
|
+
|
|
329
|
+
def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
|
|
330
|
+
return subprocess.CompletedProcess(
|
|
331
|
+
args=[],
|
|
332
|
+
returncode=1,
|
|
333
|
+
stdout="",
|
|
334
|
+
stderr="",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
with patch.object(subprocess, "run", fake_run):
|
|
338
|
+
result = bugteam_fix_hookspath.list_local_core_hooks_path_values(
|
|
339
|
+
repository_path,
|
|
340
|
+
None,
|
|
341
|
+
)
|
|
342
|
+
assert result == []
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_read_global_core_hooks_path_returns_empty_when_key_unset() -> None:
|
|
346
|
+
"""Genuine key-unset (exit 1 + empty stderr) must continue to return ''."""
|
|
347
|
+
|
|
348
|
+
def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess:
|
|
349
|
+
return subprocess.CompletedProcess(
|
|
350
|
+
args=[],
|
|
351
|
+
returncode=1,
|
|
352
|
+
stdout="",
|
|
353
|
+
stderr="",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
with patch.object(subprocess, "run", fake_run):
|
|
357
|
+
result = bugteam_fix_hookspath.read_global_core_hooks_path(None)
|
|
358
|
+
assert result == ""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_module_import_evicts_cached_config_submodules() -> None:
|
|
362
|
+
"""Importing bugteam_fix_hookspath must evict cached `config.*` submodules.
|
|
363
|
+
|
|
364
|
+
Regression for loop1-1: without a defensive cache pop above sys.path.insert,
|
|
365
|
+
a previously-cached `config` package shadows scripts/config/ and the
|
|
366
|
+
from-import raises ModuleNotFoundError.
|
|
367
|
+
"""
|
|
368
|
+
fake_submodule_name = "config.bugteam_fix_hookspath_constants"
|
|
369
|
+
fake_parent_name = "config"
|
|
370
|
+
sentinel_module_a = ModuleType(fake_parent_name)
|
|
371
|
+
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
372
|
+
sys.modules[fake_parent_name] = sentinel_module_a
|
|
373
|
+
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
374
|
+
try:
|
|
375
|
+
_load_fix_module()
|
|
376
|
+
finally:
|
|
377
|
+
sys.modules.pop(fake_parent_name, None)
|
|
378
|
+
sys.modules.pop(fake_submodule_name, None)
|
|
379
|
+
assert sys.modules.get(fake_parent_name) is not sentinel_module_a, (
|
|
380
|
+
"parent `config` cache entry must be evicted on module import"
|
|
381
|
+
)
|
|
382
|
+
assert sys.modules.get(fake_submodule_name) is not sentinel_module_b, (
|
|
383
|
+
"cached `config.<submodule>` entries must be evicted on module import"
|
|
384
|
+
)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Tests for bugteam_preflight git hooks path verification.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- core.hooksPath unset: exits non-zero with correction message
|
|
5
|
+
- core.hooksPath pointing to the correct claude hooks dir: exits zero
|
|
6
|
+
- core.hooksPath pointing elsewhere (husky override): exits non-zero
|
|
7
|
+
- core.hooksPath with trailing slash: must still pass after normalization
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
from unittest.mock import MagicMock, patch
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_preflight_module() -> ModuleType:
|
|
23
|
+
module_path = Path(__file__).parent / "bugteam_preflight.py"
|
|
24
|
+
spec = importlib.util.spec_from_file_location("bugteam_preflight", module_path)
|
|
25
|
+
assert spec is not None
|
|
26
|
+
assert spec.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
bugteam_preflight = _load_preflight_module()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _make_completed_process(
|
|
36
|
+
stdout: str, returncode: int
|
|
37
|
+
) -> subprocess.CompletedProcess:
|
|
38
|
+
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
39
|
+
process.stdout = stdout
|
|
40
|
+
process.returncode = returncode
|
|
41
|
+
return process
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_exit_nonzero_when_core_hooks_path_unset(capsys: pytest.CaptureFixture[str]) -> None:
|
|
45
|
+
with patch("subprocess.run") as mock_run:
|
|
46
|
+
mock_run.return_value = _make_completed_process("", returncode=1)
|
|
47
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
48
|
+
assert exit_code != 0
|
|
49
|
+
captured = capsys.readouterr()
|
|
50
|
+
assert "core.hooksPath" in captured.err
|
|
51
|
+
assert "npx claude-dev-env" in captured.err or "git config" in captured.err
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_exit_zero_when_core_hooks_path_points_to_claude_hooks(tmp_path: Path) -> None:
|
|
55
|
+
claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
56
|
+
claude_hooks_path.mkdir(parents=True)
|
|
57
|
+
with patch("subprocess.run") as mock_run:
|
|
58
|
+
mock_run.return_value = _make_completed_process(
|
|
59
|
+
str(claude_hooks_path) + "\n", returncode=0
|
|
60
|
+
)
|
|
61
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
62
|
+
assert exit_code == 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_should_exit_nonzero_when_core_hooks_path_points_elsewhere(capsys: pytest.CaptureFixture[str]) -> None:
|
|
66
|
+
with patch("subprocess.run") as mock_run:
|
|
67
|
+
mock_run.return_value = _make_completed_process(
|
|
68
|
+
"/some/other/path/.husky\n", returncode=0
|
|
69
|
+
)
|
|
70
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
71
|
+
assert exit_code != 0
|
|
72
|
+
captured = capsys.readouterr()
|
|
73
|
+
assert "core.hooksPath" in captured.err
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_should_include_correction_commands_in_error_message(capsys: pytest.CaptureFixture[str]) -> None:
|
|
77
|
+
with patch("subprocess.run") as mock_run:
|
|
78
|
+
mock_run.return_value = _make_completed_process("", returncode=1)
|
|
79
|
+
bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
80
|
+
captured = capsys.readouterr()
|
|
81
|
+
assert (
|
|
82
|
+
"npx claude-dev-env" in captured.err
|
|
83
|
+
or "git config --global core.hooksPath" in captured.err
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_main_should_exit_nonzero_when_hooks_path_unset() -> None:
|
|
88
|
+
with patch("subprocess.run") as mock_run:
|
|
89
|
+
mock_run.return_value = _make_completed_process("", returncode=1)
|
|
90
|
+
exit_code = bugteam_preflight.main(["--no-pytest"])
|
|
91
|
+
assert exit_code != 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_main_should_continue_when_hooks_path_valid(tmp_path: Path) -> None:
|
|
95
|
+
claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
96
|
+
claude_hooks_path.mkdir(parents=True)
|
|
97
|
+
with patch("subprocess.run") as mock_run:
|
|
98
|
+
mock_run.return_value = _make_completed_process(
|
|
99
|
+
str(claude_hooks_path) + "\n", returncode=0
|
|
100
|
+
)
|
|
101
|
+
exit_code = bugteam_preflight.main(["--no-pytest"])
|
|
102
|
+
assert exit_code == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_should_accept_hooks_path_with_trailing_slash() -> None:
|
|
106
|
+
with patch("subprocess.run") as mock_run:
|
|
107
|
+
mock_run.return_value = _make_completed_process(
|
|
108
|
+
"/home/user/.claude/hooks/git-hooks/\n", returncode=0
|
|
109
|
+
)
|
|
110
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
111
|
+
assert exit_code == 0, (
|
|
112
|
+
"hooksPath with trailing slash must pass verification after normalization"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_should_exit_zero_when_hooks_path_set_at_repo_scope(tmp_path: Path) -> None:
|
|
117
|
+
claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
118
|
+
claude_hooks_path.mkdir(parents=True)
|
|
119
|
+
repo_root = tmp_path / "my-repo"
|
|
120
|
+
repo_root.mkdir()
|
|
121
|
+
with patch("subprocess.run") as mock_run:
|
|
122
|
+
mock_run.return_value = _make_completed_process(
|
|
123
|
+
str(claude_hooks_path) + "\n", returncode=0
|
|
124
|
+
)
|
|
125
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(repo_root)
|
|
126
|
+
assert exit_code == 0, (
|
|
127
|
+
"verify_git_hooks_path must accept a valid path returned by effective "
|
|
128
|
+
"config query (not restricted to --global scope)"
|
|
129
|
+
)
|
|
130
|
+
called_command = mock_run.call_args[0][0]
|
|
131
|
+
assert "--global" not in called_command, (
|
|
132
|
+
"verify_git_hooks_path must query effective config, not --global only"
|
|
133
|
+
)
|
|
134
|
+
assert "-C" in called_command, (
|
|
135
|
+
"verify_git_hooks_path must use git -C <repo_root> for repo-effective config"
|
|
136
|
+
)
|
|
137
|
+
dash_c_index = called_command.index("-C")
|
|
138
|
+
assert called_command[dash_c_index + 1] == str(repo_root), (
|
|
139
|
+
"git -C must receive the resolved repository root path"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_verify_git_hooks_path_accepts_none_repository_root(tmp_path: Path) -> None:
|
|
144
|
+
"""When repository_root is None, the call must use git's cwd-effective config.
|
|
145
|
+
|
|
146
|
+
Binds the documented optional contract: passing None must not raise and must
|
|
147
|
+
omit the `-C <root>` arguments so git falls back to the working directory.
|
|
148
|
+
"""
|
|
149
|
+
claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
150
|
+
claude_hooks_path.mkdir(parents=True)
|
|
151
|
+
with patch("subprocess.run") as mock_run:
|
|
152
|
+
mock_run.return_value = _make_completed_process(
|
|
153
|
+
str(claude_hooks_path) + "\n", returncode=0
|
|
154
|
+
)
|
|
155
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(None)
|
|
156
|
+
assert exit_code == 0
|
|
157
|
+
called_command = mock_run.call_args[0][0]
|
|
158
|
+
assert "-C" not in called_command, (
|
|
159
|
+
"verify_git_hooks_path(None) must omit -C so git uses cwd-effective config"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_should_accept_hooks_path_with_backslash_and_trailing_slash() -> None:
|
|
164
|
+
with patch("subprocess.run") as mock_run:
|
|
165
|
+
mock_run.return_value = _make_completed_process(
|
|
166
|
+
"C:\\Users\\user\\.claude\\hooks\\git-hooks\\\n", returncode=0
|
|
167
|
+
)
|
|
168
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
169
|
+
assert exit_code == 0, (
|
|
170
|
+
"Windows hooksPath with trailing backslash must pass after normalization"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_should_exit_nonzero_when_git_executable_not_found(
|
|
175
|
+
capsys: pytest.CaptureFixture[str],
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Preflight must not crash with a traceback when git is missing from PATH."""
|
|
178
|
+
with patch("subprocess.run", side_effect=FileNotFoundError()):
|
|
179
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
180
|
+
assert exit_code != 0, (
|
|
181
|
+
"FileNotFoundError from subprocess.run must produce a non-zero exit, "
|
|
182
|
+
"not a propagated traceback"
|
|
183
|
+
)
|
|
184
|
+
captured = capsys.readouterr()
|
|
185
|
+
assert "git" in captured.err.lower(), (
|
|
186
|
+
"Error message must mention git so the user knows what is missing"
|
|
187
|
+
)
|
|
188
|
+
assert (
|
|
189
|
+
"npx claude-dev-env" in captured.err
|
|
190
|
+
or "git config --global core.hooksPath" in captured.err
|
|
191
|
+
), "Error message must include the enforcement-absent remediation hints"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_should_exit_nonzero_when_subprocess_run_raises_os_error(
|
|
195
|
+
capsys: pytest.CaptureFixture[str],
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Preflight must surface a clean error for other OS-level git launch failures."""
|
|
198
|
+
with patch("subprocess.run", side_effect=OSError("permission denied")):
|
|
199
|
+
exit_code = bugteam_preflight.verify_git_hooks_path(Path("."))
|
|
200
|
+
assert exit_code != 0, (
|
|
201
|
+
"OSError from subprocess.run must produce a non-zero exit, "
|
|
202
|
+
"not a propagated traceback"
|
|
203
|
+
)
|
|
204
|
+
captured = capsys.readouterr()
|
|
205
|
+
assert "bugteam_preflight" in captured.err, (
|
|
206
|
+
"Error message must be prefixed with the preflight tool name for context"
|
|
207
|
+
)
|
|
208
|
+
assert "permission denied" in captured.err, (
|
|
209
|
+
"Error message must include the underlying OSError detail for diagnosis"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_module_import_evicts_cached_config_submodules() -> None:
|
|
214
|
+
"""Importing bugteam_preflight must evict cached `config.*` submodules.
|
|
215
|
+
|
|
216
|
+
Regression for loop1-4: a single `sys.modules.pop("config", None)` only
|
|
217
|
+
removes the parent key, leaving stale `config.<submodule>` entries that
|
|
218
|
+
satisfy the next from-import with the wrong bindings.
|
|
219
|
+
"""
|
|
220
|
+
fake_submodule_name = "config.bugteam_preflight_constants"
|
|
221
|
+
fake_parent_name = "config"
|
|
222
|
+
sentinel_module_a = ModuleType(fake_parent_name)
|
|
223
|
+
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
224
|
+
sys.modules[fake_parent_name] = sentinel_module_a
|
|
225
|
+
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
226
|
+
try:
|
|
227
|
+
_load_preflight_module()
|
|
228
|
+
finally:
|
|
229
|
+
sys.modules.pop(fake_parent_name, None)
|
|
230
|
+
sys.modules.pop(fake_submodule_name, None)
|
|
231
|
+
assert sys.modules.get(fake_parent_name) is not sentinel_module_a, (
|
|
232
|
+
"parent `config` cache entry must be evicted on module import"
|
|
233
|
+
)
|
|
234
|
+
assert sys.modules.get(fake_submodule_name) is not sentinel_module_b, (
|
|
235
|
+
"cached `config.<submodule>` entries must be evicted on module import"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_has_pytest_configuration_finds_pytest_ini(tmp_path: Path) -> None:
|
|
240
|
+
"""has_pytest_configuration must detect pytest.ini at the repo root.
|
|
241
|
+
|
|
242
|
+
Regression for loop1-17/loop1-18: the literals "pytest.ini",
|
|
243
|
+
"pyproject.toml", and "[tool.pytest" were inlined in production function
|
|
244
|
+
bodies; centralizing them in config and importing here pins the contract.
|
|
245
|
+
"""
|
|
246
|
+
repository_root = tmp_path / "repo"
|
|
247
|
+
repository_root.mkdir()
|
|
248
|
+
(repository_root / "pytest.ini").write_text("[pytest]\n", encoding="utf-8")
|
|
249
|
+
assert bugteam_preflight.has_pytest_configuration(repository_root) is True
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_has_pytest_configuration_finds_pyproject_pytest_section(
|
|
253
|
+
tmp_path: Path,
|
|
254
|
+
) -> None:
|
|
255
|
+
repository_root = tmp_path / "repo"
|
|
256
|
+
repository_root.mkdir()
|
|
257
|
+
(repository_root / "pyproject.toml").write_text(
|
|
258
|
+
"[tool.pytest.ini_options]\nminversion = '6.0'\n", encoding="utf-8"
|
|
259
|
+
)
|
|
260
|
+
assert bugteam_preflight.has_pytest_configuration(repository_root) is True
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_has_pytest_configuration_returns_false_without_either_file(
|
|
264
|
+
tmp_path: Path,
|
|
265
|
+
) -> None:
|
|
266
|
+
repository_root = tmp_path / "repo"
|
|
267
|
+
repository_root.mkdir()
|
|
268
|
+
assert bugteam_preflight.has_pytest_configuration(repository_root) is False
|