claude-dev-env 1.82.0 → 1.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +16 -13
- package/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -4
- package/hooks/blocking/code_rules_constants_config.py +164 -1
- package/hooks/blocking/code_rules_docstrings.py +428 -15
- package/hooks/blocking/code_rules_enforcer.py +33 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +152 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -4
- package/hooks/hooks_constants/blocking_check_limits.py +46 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +5 -2
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +40 -1
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +7 -2
- package/skills/autoconverge/SKILL.md +48 -61
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +167 -106
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/everything-search/SKILL.md +5 -0
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_magic_values check module.
|
|
2
|
+
|
|
3
|
+
A diagram-first docstring may carry a bare number on an illustrative row
|
|
4
|
+
(``at line 2``). That number is prose, not a code literal, so the magic-value
|
|
5
|
+
check skips docstring rows while still flagging a real magic number in an
|
|
6
|
+
actual code statement.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
15
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
16
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
17
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
18
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
19
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
20
|
+
|
|
21
|
+
from code_rules_magic_values import check_magic_values # noqa: E402
|
|
22
|
+
|
|
23
|
+
PRODUCTION_FILE_PATH = "packages/app/rendering.py"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_magic_value_check_skips_bare_number_inside_docstring_illustration() -> None:
|
|
27
|
+
source = (
|
|
28
|
+
"def summarize(record):\n"
|
|
29
|
+
' """Summarize how a run ended.\n'
|
|
30
|
+
"\n"
|
|
31
|
+
" A finished run shows a clean outcome::\n"
|
|
32
|
+
"\n"
|
|
33
|
+
" marked in-flight at row 2\n"
|
|
34
|
+
' """\n'
|
|
35
|
+
" return record\n"
|
|
36
|
+
)
|
|
37
|
+
issues = check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
38
|
+
assert issues == [], f"Expected no magic-value issue on an illustrative row, got: {issues}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_magic_value_check_still_flags_real_magic_number_in_code_statement() -> None:
|
|
42
|
+
source = (
|
|
43
|
+
"def summarize(record):\n"
|
|
44
|
+
' """Summarize how a run ended.\n'
|
|
45
|
+
"\n"
|
|
46
|
+
" marked in-flight at row 2\n"
|
|
47
|
+
' """\n'
|
|
48
|
+
" threshold = record.score * 42\n"
|
|
49
|
+
" return threshold\n"
|
|
50
|
+
)
|
|
51
|
+
issues = check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
52
|
+
assert any(issue.endswith("Magic value 42 - extract to named constant") for issue in issues), (
|
|
53
|
+
f"Expected the real magic number 42 to stay flagged, got: {issues}"
|
|
54
|
+
)
|
|
@@ -142,12 +142,6 @@ def test_path_exempts_existing_session_env_cleanup_definition_site() -> None:
|
|
|
142
142
|
assert path_is_exempt("packages/claude-dev-env/hooks/session/session_env_cleanup.py")
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
def test_path_exempts_existing_md_to_html_test_support_definition_site() -> None:
|
|
146
|
-
assert path_is_exempt(
|
|
147
|
-
"packages/claude-dev-env/hooks/blocking/_md_to_html_blocker_test_support.py"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
145
|
def test_path_exempts_existing_teardown_worktrees_definition_site() -> None:
|
|
152
146
|
assert path_is_exempt(
|
|
153
147
|
"packages/claude-dev-env/skills/_shared/pr-loop/scripts/teardown_worktrees.py"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Unit tests: persistent-agent state keys leave both pr-converge hooks unchanged.
|
|
2
|
+
|
|
3
|
+
pr-converge-state.json carries `agents_session_id` and `persistent_agents`
|
|
4
|
+
(the persistent per-step agent map with a `fix_executor` entry). Neither the
|
|
5
|
+
enforcer (`pr_converge_bugteam_enforcer`) nor the tracker
|
|
6
|
+
(`pr_converge_bugteam_skill_tracker`) reads these keys, so their allow/block
|
|
7
|
+
decisions and stamped fields match a state file without them, and the tracker
|
|
8
|
+
preserves the keys unchanged on its stamp write.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import pathlib
|
|
17
|
+
import sys
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import Any
|
|
20
|
+
from unittest import mock
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
_BLOCKING_DIR = pathlib.Path(__file__).parent
|
|
25
|
+
_HOOKS_TREE = _BLOCKING_DIR.parent
|
|
26
|
+
_LIFECYCLE_DIR = _HOOKS_TREE / "lifecycle"
|
|
27
|
+
for each_path in (str(_BLOCKING_DIR), str(_HOOKS_TREE)):
|
|
28
|
+
if each_path not in sys.path:
|
|
29
|
+
sys.path.insert(0, each_path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_hook_module(module_name: str, module_path: pathlib.Path) -> ModuleType:
|
|
33
|
+
hook_spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
34
|
+
assert hook_spec is not None
|
|
35
|
+
assert hook_spec.loader is not None
|
|
36
|
+
loaded_module = importlib.util.module_from_spec(hook_spec)
|
|
37
|
+
hook_spec.loader.exec_module(loaded_module)
|
|
38
|
+
return loaded_module
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
enforcer_module = _load_hook_module(
|
|
42
|
+
"pr_converge_bugteam_enforcer_state_tolerance_target",
|
|
43
|
+
_BLOCKING_DIR / "pr_converge_bugteam_enforcer.py",
|
|
44
|
+
)
|
|
45
|
+
tracker_module = _load_hook_module(
|
|
46
|
+
"pr_converge_bugteam_skill_tracker_state_tolerance_target",
|
|
47
|
+
_LIFECYCLE_DIR / "pr_converge_bugteam_skill_tracker.py",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from hooks_constants.pr_converge_bugteam_enforcer_constants import (
|
|
51
|
+
BUGTEAM_PHASE,
|
|
52
|
+
CLAUDE_JOB_DIR_ENV_VAR,
|
|
53
|
+
PR_CONVERGE_STATE_FILENAME,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_HEAD_SHA_CURRENT = "abc123def456abc123def456abc123def456abcd"
|
|
57
|
+
_TICK_CURRENT = 7
|
|
58
|
+
_AUDIT_PROMPT = "Run the bugteam audit and report findings against the A-J categories."
|
|
59
|
+
_AGENTS_SESSION_ID = "session-0123456789abcdef"
|
|
60
|
+
_FIX_EXECUTOR_AGENT_ID = "prc-fix-42"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _persistent_agent_state_keys() -> dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"agents_session_id": _AGENTS_SESSION_ID,
|
|
66
|
+
"persistent_agents": {
|
|
67
|
+
"fix_executor": {
|
|
68
|
+
"agent_id": _FIX_EXECUTOR_AGENT_ID,
|
|
69
|
+
"created_tick": 2,
|
|
70
|
+
"last_used_tick": 6,
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _bugteam_phase_state(**overrides: Any) -> dict[str, Any]:
|
|
77
|
+
baseline_state: dict[str, Any] = {
|
|
78
|
+
"phase": BUGTEAM_PHASE,
|
|
79
|
+
"current_head": _HEAD_SHA_CURRENT,
|
|
80
|
+
"tick_count": _TICK_CURRENT,
|
|
81
|
+
"bugteam_skill_invoked_at_head": None,
|
|
82
|
+
"bugteam_skill_invoked_at_tick": None,
|
|
83
|
+
}
|
|
84
|
+
baseline_state.update(_persistent_agent_state_keys())
|
|
85
|
+
baseline_state.update(overrides)
|
|
86
|
+
return baseline_state
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _write_state(state_directory: pathlib.Path, state: dict[str, Any]) -> pathlib.Path:
|
|
90
|
+
state_path = state_directory / PR_CONVERGE_STATE_FILENAME
|
|
91
|
+
state_path.write_text(json.dumps(state), encoding="utf-8")
|
|
92
|
+
return state_path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_state(state_directory: pathlib.Path) -> dict[str, Any]:
|
|
96
|
+
state_path = state_directory / PR_CONVERGE_STATE_FILENAME
|
|
97
|
+
return json.loads(state_path.read_text(encoding="utf-8"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _clean_coder_audit_payload() -> dict[str, Any]:
|
|
101
|
+
return {
|
|
102
|
+
"tool_name": "Agent",
|
|
103
|
+
"tool_input": {
|
|
104
|
+
"subagent_type": "clean-coder",
|
|
105
|
+
"prompt": _AUDIT_PROMPT,
|
|
106
|
+
"description": "Audit the PR",
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _bugteam_skill_payload() -> dict[str, Any]:
|
|
112
|
+
return {
|
|
113
|
+
"tool_name": "Skill",
|
|
114
|
+
"tool_input": {"skill": "bugteam", "args": "https://github.com/o/r/pull/1"},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run_enforcer_main_with_io(input_text: str) -> str:
|
|
119
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
120
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO) as captured_stdout:
|
|
121
|
+
try:
|
|
122
|
+
enforcer_module.main()
|
|
123
|
+
except SystemExit:
|
|
124
|
+
pass
|
|
125
|
+
return captured_stdout.getvalue()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run_tracker_main_with_io(input_text: str) -> None:
|
|
129
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
130
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO):
|
|
131
|
+
with mock.patch("sys.stderr", new_callable=io.StringIO):
|
|
132
|
+
try:
|
|
133
|
+
tracker_module.main()
|
|
134
|
+
except SystemExit:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.fixture()
|
|
139
|
+
def claude_job_directory(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
|
|
140
|
+
monkeypatch.setenv(CLAUDE_JOB_DIR_ENV_VAR, str(tmp_path))
|
|
141
|
+
return tmp_path
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_should_block_enforcer_when_persistent_agent_keys_present_without_skill(
|
|
145
|
+
claude_job_directory: pathlib.Path,
|
|
146
|
+
) -> None:
|
|
147
|
+
_write_state(claude_job_directory, _bugteam_phase_state())
|
|
148
|
+
captured_output = _run_enforcer_main_with_io(json.dumps(_clean_coder_audit_payload()))
|
|
149
|
+
deny_payload = json.loads(captured_output)
|
|
150
|
+
assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_allow_enforcer_when_persistent_agent_keys_present_with_skill_recorded(
|
|
154
|
+
claude_job_directory: pathlib.Path,
|
|
155
|
+
) -> None:
|
|
156
|
+
_write_state(
|
|
157
|
+
claude_job_directory,
|
|
158
|
+
_bugteam_phase_state(
|
|
159
|
+
bugteam_skill_invoked_at_head=_HEAD_SHA_CURRENT,
|
|
160
|
+
bugteam_skill_invoked_at_tick=_TICK_CURRENT,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
captured_output = _run_enforcer_main_with_io(json.dumps(_clean_coder_audit_payload()))
|
|
164
|
+
assert captured_output == ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_should_stamp_tracker_fields_when_persistent_agent_keys_present(
|
|
168
|
+
claude_job_directory: pathlib.Path,
|
|
169
|
+
) -> None:
|
|
170
|
+
_write_state(claude_job_directory, _bugteam_phase_state())
|
|
171
|
+
_run_tracker_main_with_io(json.dumps(_bugteam_skill_payload()))
|
|
172
|
+
updated_state = _read_state(claude_job_directory)
|
|
173
|
+
assert updated_state["bugteam_skill_invoked_at_head"] == _HEAD_SHA_CURRENT
|
|
174
|
+
assert updated_state["bugteam_skill_invoked_at_tick"] == _TICK_CURRENT
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_should_preserve_persistent_agent_keys_on_tracker_stamp_write(
|
|
178
|
+
claude_job_directory: pathlib.Path,
|
|
179
|
+
) -> None:
|
|
180
|
+
_write_state(claude_job_directory, _bugteam_phase_state())
|
|
181
|
+
_run_tracker_main_with_io(json.dumps(_bugteam_skill_payload()))
|
|
182
|
+
updated_state = _read_state(claude_job_directory)
|
|
183
|
+
assert updated_state["agents_session_id"] == _AGENTS_SESSION_ID
|
|
184
|
+
assert updated_state["persistent_agents"] == _persistent_agent_state_keys()["persistent_agents"]
|
|
@@ -638,10 +638,16 @@ def test_dispatcher_write_applies_both_groups() -> None:
|
|
|
638
638
|
|
|
639
639
|
|
|
640
640
|
def test_dispatcher_edit_applies_both_groups() -> None:
|
|
641
|
-
"""Edit
|
|
641
|
+
"""Edit triggers Group A, Group B, and the Edit-scoped entry through the
|
|
642
|
+
dispatcher.
|
|
643
|
+
"""
|
|
642
644
|
all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
+
all_edit_script_paths = {each_entry.script_relative_path for each_entry in all_edit_entries}
|
|
646
|
+
assert "blocking/stale_comment_reference_blocker.py" in all_edit_script_paths, (
|
|
647
|
+
"stale_comment_reference_blocker belongs in the Edit applicable set"
|
|
648
|
+
)
|
|
649
|
+
assert len(all_edit_entries) == 20, (
|
|
650
|
+
f"expected 20 Edit entries, got {len(all_edit_entries)}"
|
|
645
651
|
)
|
|
646
652
|
|
|
647
653
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Unit tests for reviewer_spawn_gate PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
13
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
15
|
+
|
|
16
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
17
|
+
"reviewer_spawn_gate",
|
|
18
|
+
_HOOK_DIR / "reviewer_spawn_gate.py",
|
|
19
|
+
)
|
|
20
|
+
assert hook_spec is not None
|
|
21
|
+
assert hook_spec.loader is not None
|
|
22
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
23
|
+
hook_spec.loader.exec_module(hook_module)
|
|
24
|
+
|
|
25
|
+
_matches_copilot_trigger = hook_module._matches_copilot_trigger
|
|
26
|
+
_matches_bugbot_trigger = hook_module._matches_bugbot_trigger
|
|
27
|
+
_reviewer_trigger = hook_module._reviewer_trigger
|
|
28
|
+
|
|
29
|
+
_SENTINEL_MARKER = hook_module.GATE_SENTINEL_MARKER
|
|
30
|
+
_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME = hook_module.AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME
|
|
31
|
+
|
|
32
|
+
_COPILOT_TRIGGER_COMMAND = (
|
|
33
|
+
f"{_SENTINEL_MARKER} gh api repos/o/r/pulls/1/requested_reviewers "
|
|
34
|
+
f'-f "reviewers[]=copilot-pull-request-reviewer[bot]"'
|
|
35
|
+
)
|
|
36
|
+
_BUGBOT_TRIGGER_COMMAND = f'{_SENTINEL_MARKER} python post_fix_reply.py --body "bugbot run"'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_fake_availability_script(tmp_path: pathlib.Path, exit_code: int) -> pathlib.Path:
|
|
40
|
+
fake_script_path = tmp_path / "fake_reviewer_availability.py"
|
|
41
|
+
fake_script_path.write_text(f"import sys\nsys.exit({exit_code})\n", encoding="utf-8")
|
|
42
|
+
return fake_script_path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_hook_with_stdin_text(stdin_text: str) -> tuple[str, str, int]:
|
|
46
|
+
captured_stdout = io.StringIO()
|
|
47
|
+
captured_stderr = io.StringIO()
|
|
48
|
+
exit_code = 0
|
|
49
|
+
sys.stdin = io.StringIO(stdin_text)
|
|
50
|
+
try:
|
|
51
|
+
with redirect_stdout(captured_stdout), redirect_stderr(captured_stderr):
|
|
52
|
+
try:
|
|
53
|
+
hook_module.main()
|
|
54
|
+
except SystemExit as exit_signal:
|
|
55
|
+
exit_code = exit_signal.code if isinstance(exit_signal.code, int) else 0
|
|
56
|
+
finally:
|
|
57
|
+
sys.stdin = sys.__stdin__
|
|
58
|
+
return captured_stdout.getvalue(), captured_stderr.getvalue(), exit_code
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_hook(hook_input: dict) -> tuple[str, int]:
|
|
62
|
+
stdout_text, _stderr_text, exit_code = _run_hook_with_stdin_text(json.dumps(hook_input))
|
|
63
|
+
return stdout_text, exit_code
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_matches_copilot_trigger_true_for_request_reviewers_command() -> None:
|
|
67
|
+
assert _matches_copilot_trigger(_COPILOT_TRIGGER_COMMAND)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_matches_copilot_trigger_false_for_unrelated_command() -> None:
|
|
71
|
+
assert not _matches_copilot_trigger("gh pr list")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_matches_bugbot_trigger_true_for_rerun_comment_command() -> None:
|
|
75
|
+
assert _matches_bugbot_trigger(_BUGBOT_TRIGGER_COMMAND)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_matches_bugbot_trigger_false_for_unrelated_command() -> None:
|
|
79
|
+
assert not _matches_bugbot_trigger("python post_fix_reply.py --body 'looks good'")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_reviewer_trigger_returns_none_for_unrecognized_command() -> None:
|
|
83
|
+
assert _reviewer_trigger("gh pr list") is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_main_allows_copilot_shaped_command_with_no_sentinel_marker(
|
|
87
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
88
|
+
) -> None:
|
|
89
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
90
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
91
|
+
stdout_text, exit_code = _run_hook(
|
|
92
|
+
{
|
|
93
|
+
"tool_name": "Bash",
|
|
94
|
+
"tool_input": {
|
|
95
|
+
"command": (
|
|
96
|
+
"gh api repos/o/r/pulls/1/requested_reviewers "
|
|
97
|
+
'-f "reviewers[]=copilot-pull-request-reviewer[bot]"'
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
assert exit_code == 0
|
|
103
|
+
assert stdout_text == ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_main_allows_copilot_trigger_when_available(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
108
|
+
) -> None:
|
|
109
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=0)
|
|
110
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
111
|
+
stdout_text, exit_code = _run_hook(
|
|
112
|
+
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
113
|
+
)
|
|
114
|
+
assert exit_code == 0
|
|
115
|
+
assert stdout_text == ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_main_denies_copilot_trigger_when_down(
|
|
119
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
120
|
+
) -> None:
|
|
121
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
122
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
123
|
+
stdout_text, exit_code = _run_hook(
|
|
124
|
+
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
125
|
+
)
|
|
126
|
+
assert exit_code == 0
|
|
127
|
+
response_payload = json.loads(stdout_text)
|
|
128
|
+
decision_block = response_payload["hookSpecificOutput"]
|
|
129
|
+
assert decision_block["permissionDecision"] == "deny"
|
|
130
|
+
assert "GitHub Copilot" in decision_block["permissionDecisionReason"]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_main_allows_copilot_trigger_on_unexpected_exit_code(
|
|
134
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
135
|
+
) -> None:
|
|
136
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=2)
|
|
137
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
138
|
+
stdout_text, exit_code = _run_hook(
|
|
139
|
+
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
140
|
+
)
|
|
141
|
+
assert exit_code == 0
|
|
142
|
+
assert stdout_text == ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_main_allows_copilot_trigger_when_availability_script_crashes(
|
|
146
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
147
|
+
) -> None:
|
|
148
|
+
fake_script_path = tmp_path / "fake_reviewer_availability.py"
|
|
149
|
+
fake_script_path.write_text(
|
|
150
|
+
"import module_that_does_not_exist_anywhere\n", encoding="utf-8"
|
|
151
|
+
)
|
|
152
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
153
|
+
stdout_text, exit_code = _run_hook(
|
|
154
|
+
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
155
|
+
)
|
|
156
|
+
assert exit_code == 0
|
|
157
|
+
assert stdout_text == ""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_main_allows_bugbot_trigger_when_available(
|
|
161
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
162
|
+
) -> None:
|
|
163
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=0)
|
|
164
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
165
|
+
stdout_text, exit_code = _run_hook(
|
|
166
|
+
{"tool_name": "Bash", "tool_input": {"command": _BUGBOT_TRIGGER_COMMAND}}
|
|
167
|
+
)
|
|
168
|
+
assert exit_code == 0
|
|
169
|
+
assert stdout_text == ""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_main_denies_bugbot_trigger_when_opted_out(
|
|
173
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
174
|
+
) -> None:
|
|
175
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
176
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
177
|
+
stdout_text, exit_code = _run_hook(
|
|
178
|
+
{"tool_name": "Bash", "tool_input": {"command": _BUGBOT_TRIGGER_COMMAND}}
|
|
179
|
+
)
|
|
180
|
+
assert exit_code == 0
|
|
181
|
+
response_payload = json.loads(stdout_text)
|
|
182
|
+
decision_block = response_payload["hookSpecificOutput"]
|
|
183
|
+
assert decision_block["permissionDecision"] == "deny"
|
|
184
|
+
assert "Cursor Bugbot" in decision_block["permissionDecisionReason"]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_main_allows_marker_command_with_no_recognized_trigger(
|
|
188
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
189
|
+
) -> None:
|
|
190
|
+
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
191
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
192
|
+
stdout_text, exit_code = _run_hook(
|
|
193
|
+
{"tool_name": "Bash", "tool_input": {"command": f"{_SENTINEL_MARKER} gh pr list"}}
|
|
194
|
+
)
|
|
195
|
+
assert exit_code == 0
|
|
196
|
+
assert stdout_text == ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_main_allows_when_availability_script_missing(
|
|
200
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
201
|
+
) -> None:
|
|
202
|
+
missing_script_path = tmp_path / "does_not_exist.py"
|
|
203
|
+
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(missing_script_path))
|
|
204
|
+
stdout_text, exit_code = _run_hook(
|
|
205
|
+
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
206
|
+
)
|
|
207
|
+
assert exit_code == 0
|
|
208
|
+
assert stdout_text == ""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_main_allows_non_bash_tool() -> None:
|
|
212
|
+
stdout_text, exit_code = _run_hook(
|
|
213
|
+
{"tool_name": "Write", "tool_input": {"content": _COPILOT_TRIGGER_COMMAND}}
|
|
214
|
+
)
|
|
215
|
+
assert exit_code == 0
|
|
216
|
+
assert stdout_text == ""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_main_with_empty_stdin_exits_silently() -> None:
|
|
220
|
+
stdout_text, stderr_text, exit_code = _run_hook_with_stdin_text("")
|
|
221
|
+
assert exit_code == 0
|
|
222
|
+
assert stdout_text == ""
|
|
223
|
+
assert stderr_text == ""
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_main_with_invalid_json_stdin_exits_silently() -> None:
|
|
227
|
+
stdout_text, stderr_text, exit_code = _run_hook_with_stdin_text("{broken")
|
|
228
|
+
assert exit_code == 0
|
|
229
|
+
assert stdout_text == ""
|
|
230
|
+
assert stderr_text == ""
|
|
@@ -10,11 +10,11 @@ each real hook script through its production ``__main__`` stdin path over a
|
|
|
10
10
|
corpus that pins those fail-soft edges plus a representative allow payload, so a
|
|
11
11
|
conversion that changes any decision is caught.
|
|
12
12
|
|
|
13
|
-
The deterministic deny
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
The deterministic deny payload for the Write/Edit blocker whose trigger
|
|
14
|
+
needs no filesystem or state setup (``open_questions_in_plans_blocker``) is
|
|
15
|
+
exercised here too; each remaining hook's full deny coverage stays in its own
|
|
16
|
+
suite, which also drives the real ``main()`` and so re-proves the decision
|
|
17
|
+
after the parser swap.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import json
|
|
@@ -28,7 +28,6 @@ import pytest
|
|
|
28
28
|
_BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
29
29
|
|
|
30
30
|
ALL_CONVERTED_HOOK_FILENAMES = (
|
|
31
|
-
"md_to_html_blocker.py",
|
|
32
31
|
"open_questions_in_plans_blocker.py",
|
|
33
32
|
"claude_md_orphan_file_blocker.py",
|
|
34
33
|
"pr_converge_bugteam_enforcer.py",
|
|
@@ -83,30 +82,6 @@ def test_fail_soft_payload_exits_zero_with_no_decision(hook_filename: str, stdin
|
|
|
83
82
|
)
|
|
84
83
|
|
|
85
84
|
|
|
86
|
-
def test_md_to_html_blocker_still_denies_relative_markdown_write() -> None:
|
|
87
|
-
payload = json.dumps(
|
|
88
|
-
{
|
|
89
|
-
"tool_name": "Write",
|
|
90
|
-
"tool_input": {"file_path": "notes/topic.md", "content": "# Topic"},
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
completed = _run_hook_script("md_to_html_blocker.py", payload)
|
|
94
|
-
assert completed.returncode == 0
|
|
95
|
-
assert _decision_from_stdout(completed) == "deny"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_md_to_html_blocker_still_allows_non_markdown_write() -> None:
|
|
99
|
-
payload = json.dumps(
|
|
100
|
-
{
|
|
101
|
-
"tool_name": "Write",
|
|
102
|
-
"tool_input": {"file_path": "notes/topic.txt", "content": "plain"},
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
completed = _run_hook_script("md_to_html_blocker.py", payload)
|
|
106
|
-
assert completed.returncode == 0
|
|
107
|
-
assert _decision_from_stdout(completed) is None
|
|
108
|
-
|
|
109
|
-
|
|
110
85
|
def test_open_questions_blocker_still_denies_plan_with_open_questions(
|
|
111
86
|
tmp_path: Path,
|
|
112
87
|
) -> None:
|